1/*
2Copyright 2016 The Kubernetes Authors.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8    http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17package validation
18
19import (
20	"k8s.io/apimachinery/pkg/api/validation/path"
21	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
22	unversionedvalidation "k8s.io/apimachinery/pkg/apis/meta/v1/validation"
23	"k8s.io/apimachinery/pkg/util/validation/field"
24	"k8s.io/kubernetes/pkg/apis/core/validation"
25	"k8s.io/kubernetes/pkg/apis/rbac"
26)
27
28// ValidateRBACName is exported to allow types outside of the RBAC API group to reuse this validation logic
29// Minimal validation of names for roles and bindings. Identical to the validation for Openshift. See:
30// * https://github.com/kubernetes/kubernetes/blob/60db507b279ce45bd16ea3db49bf181f2aeb3c3d/pkg/api/validation/name.go
31// * https://github.com/openshift/origin/blob/388478c40e751c4295dcb9a44dd69e5ac65d0e3b/pkg/api/helpers.go
32func ValidateRBACName(name string, prefix bool) []string {
33	return path.IsValidPathSegmentName(name)
34}
35
36func ValidateRole(role *rbac.Role) field.ErrorList {
37	allErrs := field.ErrorList{}
38	allErrs = append(allErrs, validation.ValidateObjectMeta(&role.ObjectMeta, true, ValidateRBACName, field.NewPath("metadata"))...)
39
40	for i, rule := range role.Rules {
41		if err := ValidatePolicyRule(rule, true, field.NewPath("rules").Index(i)); err != nil {
42			allErrs = append(allErrs, err...)
43		}
44	}
45	if len(allErrs) != 0 {
46		return allErrs
47	}
48	return nil
49}
50
51func ValidateRoleUpdate(role *rbac.Role, oldRole *rbac.Role) field.ErrorList {
52	allErrs := ValidateRole(role)
53	allErrs = append(allErrs, validation.ValidateObjectMetaUpdate(&role.ObjectMeta, &oldRole.ObjectMeta, field.NewPath("metadata"))...)
54
55	return allErrs
56}
57
58func ValidateClusterRole(role *rbac.ClusterRole) field.ErrorList {
59	allErrs := field.ErrorList{}
60	allErrs = append(allErrs, validation.ValidateObjectMeta(&role.ObjectMeta, false, ValidateRBACName, field.NewPath("metadata"))...)
61
62	for i, rule := range role.Rules {
63		if err := ValidatePolicyRule(rule, false, field.NewPath("rules").Index(i)); err != nil {
64			allErrs = append(allErrs, err...)
65		}
66	}
67
68	if role.AggregationRule != nil {
69		if len(role.AggregationRule.ClusterRoleSelectors) == 0 {
70			allErrs = append(allErrs, field.Required(field.NewPath("aggregationRule", "clusterRoleSelectors"), "at least one clusterRoleSelector required if aggregationRule is non-nil"))
71		}
72		for i, selector := range role.AggregationRule.ClusterRoleSelectors {
73			fieldPath := field.NewPath("aggregationRule", "clusterRoleSelectors").Index(i)
74			allErrs = append(allErrs, unversionedvalidation.ValidateLabelSelector(&selector, fieldPath)...)
75
76			selector, err := metav1.LabelSelectorAsSelector(&selector)
77			if err != nil {
78				allErrs = append(allErrs, field.Invalid(fieldPath, selector, "invalid label selector."))
79			}
80		}
81	}
82
83	if len(allErrs) != 0 {
84		return allErrs
85	}
86	return nil
87}
88
89func ValidateClusterRoleUpdate(role *rbac.ClusterRole, oldRole *rbac.ClusterRole) field.ErrorList {
90	allErrs := ValidateClusterRole(role)
91	allErrs = append(allErrs, validation.ValidateObjectMetaUpdate(&role.ObjectMeta, &oldRole.ObjectMeta, field.NewPath("metadata"))...)
92
93	return allErrs
94}
95
96// ValidatePolicyRule is exported to allow types outside of the RBAC API group to embed a rbac.PolicyRule and reuse this validation logic
97func ValidatePolicyRule(rule rbac.PolicyRule, isNamespaced bool, fldPath *field.Path) field.ErrorList {
98	allErrs := field.ErrorList{}
99	if len(rule.Verbs) == 0 {
100		allErrs = append(allErrs, field.Required(fldPath.Child("verbs"), "verbs must contain at least one value"))
101	}
102
103	if len(rule.NonResourceURLs) > 0 {
104		if isNamespaced {
105			allErrs = append(allErrs, field.Invalid(fldPath.Child("nonResourceURLs"), rule.NonResourceURLs, "namespaced rules cannot apply to non-resource URLs"))
106		}
107		if len(rule.APIGroups) > 0 || len(rule.Resources) > 0 || len(rule.ResourceNames) > 0 {
108			allErrs = append(allErrs, field.Invalid(fldPath.Child("nonResourceURLs"), rule.NonResourceURLs, "rules cannot apply to both regular resources and non-resource URLs"))
109		}
110		return allErrs
111	}
112
113	if len(rule.APIGroups) == 0 {
114		allErrs = append(allErrs, field.Required(fldPath.Child("apiGroups"), "resource rules must supply at least one api group"))
115	}
116	if len(rule.Resources) == 0 {
117		allErrs = append(allErrs, field.Required(fldPath.Child("resources"), "resource rules must supply at least one resource"))
118	}
119	return allErrs
120}
121
122func ValidateRoleBinding(roleBinding *rbac.RoleBinding) field.ErrorList {
123	allErrs := field.ErrorList{}
124	allErrs = append(allErrs, validation.ValidateObjectMeta(&roleBinding.ObjectMeta, true, ValidateRBACName, field.NewPath("metadata"))...)
125
126	// TODO allow multiple API groups.  For now, restrict to one, but I can envision other experimental roles in other groups taking
127	// advantage of the binding infrastructure
128	if roleBinding.RoleRef.APIGroup != rbac.GroupName {
129		allErrs = append(allErrs, field.NotSupported(field.NewPath("roleRef", "apiGroup"), roleBinding.RoleRef.APIGroup, []string{rbac.GroupName}))
130	}
131
132	switch roleBinding.RoleRef.Kind {
133	case "Role", "ClusterRole":
134	default:
135		allErrs = append(allErrs, field.NotSupported(field.NewPath("roleRef", "kind"), roleBinding.RoleRef.Kind, []string{"Role", "ClusterRole"}))
136
137	}
138
139	if len(roleBinding.RoleRef.Name) == 0 {
140		allErrs = append(allErrs, field.Required(field.NewPath("roleRef", "name"), ""))
141	} else {
142		for _, msg := range ValidateRBACName(roleBinding.RoleRef.Name, false) {
143			allErrs = append(allErrs, field.Invalid(field.NewPath("roleRef", "name"), roleBinding.RoleRef.Name, msg))
144		}
145	}
146
147	subjectsPath := field.NewPath("subjects")
148	for i, subject := range roleBinding.Subjects {
149		allErrs = append(allErrs, ValidateRoleBindingSubject(subject, true, subjectsPath.Index(i))...)
150	}
151
152	return allErrs
153}
154
155func ValidateRoleBindingUpdate(roleBinding *rbac.RoleBinding, oldRoleBinding *rbac.RoleBinding) field.ErrorList {
156	allErrs := ValidateRoleBinding(roleBinding)
157	allErrs = append(allErrs, validation.ValidateObjectMetaUpdate(&roleBinding.ObjectMeta, &oldRoleBinding.ObjectMeta, field.NewPath("metadata"))...)
158
159	if oldRoleBinding.RoleRef != roleBinding.RoleRef {
160		allErrs = append(allErrs, field.Invalid(field.NewPath("roleRef"), roleBinding.RoleRef, "cannot change roleRef"))
161	}
162
163	return allErrs
164}
165
166func ValidateClusterRoleBinding(roleBinding *rbac.ClusterRoleBinding) field.ErrorList {
167	allErrs := field.ErrorList{}
168	allErrs = append(allErrs, validation.ValidateObjectMeta(&roleBinding.ObjectMeta, false, ValidateRBACName, field.NewPath("metadata"))...)
169
170	// TODO allow multiple API groups.  For now, restrict to one, but I can envision other experimental roles in other groups taking
171	// advantage of the binding infrastructure
172	if roleBinding.RoleRef.APIGroup != rbac.GroupName {
173		allErrs = append(allErrs, field.NotSupported(field.NewPath("roleRef", "apiGroup"), roleBinding.RoleRef.APIGroup, []string{rbac.GroupName}))
174	}
175
176	switch roleBinding.RoleRef.Kind {
177	case "ClusterRole":
178	default:
179		allErrs = append(allErrs, field.NotSupported(field.NewPath("roleRef", "kind"), roleBinding.RoleRef.Kind, []string{"ClusterRole"}))
180
181	}
182
183	if len(roleBinding.RoleRef.Name) == 0 {
184		allErrs = append(allErrs, field.Required(field.NewPath("roleRef", "name"), ""))
185	} else {
186		for _, msg := range ValidateRBACName(roleBinding.RoleRef.Name, false) {
187			allErrs = append(allErrs, field.Invalid(field.NewPath("roleRef", "name"), roleBinding.RoleRef.Name, msg))
188		}
189	}
190
191	subjectsPath := field.NewPath("subjects")
192	for i, subject := range roleBinding.Subjects {
193		allErrs = append(allErrs, ValidateRoleBindingSubject(subject, false, subjectsPath.Index(i))...)
194	}
195
196	return allErrs
197}
198
199func ValidateClusterRoleBindingUpdate(roleBinding *rbac.ClusterRoleBinding, oldRoleBinding *rbac.ClusterRoleBinding) field.ErrorList {
200	allErrs := ValidateClusterRoleBinding(roleBinding)
201	allErrs = append(allErrs, validation.ValidateObjectMetaUpdate(&roleBinding.ObjectMeta, &oldRoleBinding.ObjectMeta, field.NewPath("metadata"))...)
202
203	if oldRoleBinding.RoleRef != roleBinding.RoleRef {
204		allErrs = append(allErrs, field.Invalid(field.NewPath("roleRef"), roleBinding.RoleRef, "cannot change roleRef"))
205	}
206
207	return allErrs
208}
209
210// ValidateRoleBindingSubject is exported to allow types outside of the RBAC API group to embed a rbac.Subject and reuse this validation logic
211func ValidateRoleBindingSubject(subject rbac.Subject, isNamespaced bool, fldPath *field.Path) field.ErrorList {
212	allErrs := field.ErrorList{}
213
214	if len(subject.Name) == 0 {
215		allErrs = append(allErrs, field.Required(fldPath.Child("name"), ""))
216	}
217
218	switch subject.Kind {
219	case rbac.ServiceAccountKind:
220		if len(subject.Name) > 0 {
221			for _, msg := range validation.ValidateServiceAccountName(subject.Name, false) {
222				allErrs = append(allErrs, field.Invalid(fldPath.Child("name"), subject.Name, msg))
223			}
224		}
225		if len(subject.APIGroup) > 0 {
226			allErrs = append(allErrs, field.NotSupported(fldPath.Child("apiGroup"), subject.APIGroup, []string{""}))
227		}
228		if !isNamespaced && len(subject.Namespace) == 0 {
229			allErrs = append(allErrs, field.Required(fldPath.Child("namespace"), ""))
230		}
231
232	case rbac.UserKind:
233		// TODO(ericchiang): What other restrictions on user name are there?
234		if subject.APIGroup != rbac.GroupName {
235			allErrs = append(allErrs, field.NotSupported(fldPath.Child("apiGroup"), subject.APIGroup, []string{rbac.GroupName}))
236		}
237
238	case rbac.GroupKind:
239		// TODO(ericchiang): What other restrictions on group name are there?
240		if subject.APIGroup != rbac.GroupName {
241			allErrs = append(allErrs, field.NotSupported(fldPath.Child("apiGroup"), subject.APIGroup, []string{rbac.GroupName}))
242		}
243
244	default:
245		allErrs = append(allErrs, field.NotSupported(fldPath.Child("kind"), subject.Kind, []string{rbac.ServiceAccountKind, rbac.UserKind, rbac.GroupKind}))
246	}
247
248	return allErrs
249}
250