add field and label selectors to authorization attributes

Co-authored-by: Jordan Liggitt <liggitt@google.com>
This commit is contained in:
David Eads
2024-05-23 15:12:26 -04:00
committed by Jordan Liggitt
parent f5e5bef2e0
commit 92e3445e9d
25 changed files with 2388 additions and 226 deletions

View File

@@ -17,14 +17,24 @@ limitations under the License.
package util
import (
"fmt"
"strings"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/selection"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer"
genericfeatures "k8s.io/apiserver/pkg/features"
utilfeature "k8s.io/apiserver/pkg/util/feature"
authorizationapi "k8s.io/kubernetes/pkg/apis/authorization"
)
// ResourceAttributesFrom combines the API object information and the user.Info from the context to build a full authorizer.AttributesRecord for resource access
func ResourceAttributesFrom(user user.Info, in authorizationapi.ResourceAttributes) authorizer.AttributesRecord {
return authorizer.AttributesRecord{
ret := authorizer.AttributesRecord{
User: user,
Verb: in.Verb,
Namespace: in.Namespace,
@@ -35,6 +45,129 @@ func ResourceAttributesFrom(user user.Info, in authorizationapi.ResourceAttribut
Name: in.Name,
ResourceRequest: true,
}
if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.AuthorizeWithSelectors) {
if in.LabelSelector != nil {
if len(in.LabelSelector.RawSelector) > 0 {
labelSelector, err := labels.Parse(in.LabelSelector.RawSelector)
if err != nil {
ret.LabelSelectorRequirements, ret.LabelSelectorParsingErr = nil, err
} else {
requirements, _ /*selectable*/ := labelSelector.Requirements()
ret.LabelSelectorRequirements, ret.LabelSelectorParsingErr = requirements, nil
}
}
if len(in.LabelSelector.Requirements) > 0 {
ret.LabelSelectorRequirements, ret.LabelSelectorParsingErr = labelSelectorAsSelector(in.LabelSelector.Requirements)
}
}
if in.FieldSelector != nil {
if len(in.FieldSelector.RawSelector) > 0 {
fieldSelector, err := fields.ParseSelector(in.FieldSelector.RawSelector)
if err != nil {
ret.FieldSelectorRequirements, ret.FieldSelectorParsingErr = nil, err
} else {
ret.FieldSelectorRequirements, ret.FieldSelectorParsingErr = fieldSelector.Requirements(), nil
}
}
if len(in.FieldSelector.Requirements) > 0 {
ret.FieldSelectorRequirements, ret.FieldSelectorParsingErr = fieldSelectorAsSelector(in.FieldSelector.Requirements)
}
}
}
return ret
}
var labelSelectorOpToSelectionOp = map[metav1.LabelSelectorOperator]selection.Operator{
metav1.LabelSelectorOpIn: selection.In,
metav1.LabelSelectorOpNotIn: selection.NotIn,
metav1.LabelSelectorOpExists: selection.Exists,
metav1.LabelSelectorOpDoesNotExist: selection.DoesNotExist,
}
func labelSelectorAsSelector(requirements []metav1.LabelSelectorRequirement) (labels.Requirements, error) {
if len(requirements) == 0 {
return nil, nil
}
reqs := make([]labels.Requirement, 0, len(requirements))
var errs []error
for _, expr := range requirements {
op, ok := labelSelectorOpToSelectionOp[expr.Operator]
if !ok {
errs = append(errs, fmt.Errorf("%q is not a valid label selector operator", expr.Operator))
continue
}
values := expr.Values
if len(values) == 0 {
values = nil
}
req, err := labels.NewRequirement(expr.Key, op, values)
if err != nil {
errs = append(errs, err)
continue
}
reqs = append(reqs, *req)
}
// If this happens, it means all requirements ended up getting skipped.
// Return nil rather than [].
if len(reqs) == 0 {
reqs = nil
}
// Return any accumulated errors along with any accumulated requirements, so recognized / valid requirements can be considered by authorization.
// This is safe because requirements are ANDed together so dropping unknown / invalid ones results in a strictly broader authorization check.
return labels.Requirements(reqs), utilerrors.NewAggregate(errs)
}
func fieldSelectorAsSelector(requirements []metav1.FieldSelectorRequirement) (fields.Requirements, error) {
if len(requirements) == 0 {
return nil, nil
}
reqs := make([]fields.Requirement, 0, len(requirements))
var errs []error
for _, expr := range requirements {
if len(expr.Values) > 1 {
errs = append(errs, fmt.Errorf("fieldSelectors do not yet support multiple values"))
continue
}
switch expr.Operator {
case metav1.FieldSelectorOpIn:
if len(expr.Values) != 1 {
errs = append(errs, fmt.Errorf("fieldSelectors in must have one value"))
continue
}
// when converting to fields.Requirement, use Equals to match how parsed field selectors behave
reqs = append(reqs, fields.Requirement{Field: expr.Key, Operator: selection.Equals, Value: expr.Values[0]})
case metav1.FieldSelectorOpNotIn:
if len(expr.Values) != 1 {
errs = append(errs, fmt.Errorf("fieldSelectors not in must have one value"))
continue
}
// when converting to fields.Requirement, use NotEquals to match how parsed field selectors behave
reqs = append(reqs, fields.Requirement{Field: expr.Key, Operator: selection.NotEquals, Value: expr.Values[0]})
case metav1.FieldSelectorOpExists, metav1.FieldSelectorOpDoesNotExist:
errs = append(errs, fmt.Errorf("fieldSelectors do not yet support %v", expr.Operator))
continue
default:
errs = append(errs, fmt.Errorf("%q is not a valid field selector operator", expr.Operator))
continue
}
}
// If this happens, it means all requirements ended up getting skipped.
// Return nil rather than [].
if len(reqs) == 0 {
reqs = nil
}
// Return any accumulated errors along with any accumulated requirements, so recognized / valid requirements can be considered by authorization.
// This is safe because requirements are ANDed together so dropping unknown / invalid ones results in a strictly broader authorization check.
return fields.Requirements(reqs), utilerrors.NewAggregate(errs)
}
// NonResourceAttributesFrom combines the API object information and the user.Info from the context to build a full authorizer.AttributesRecord for non resource access
@@ -85,3 +218,27 @@ func matchAllVersionIfEmpty(version string) string {
}
return version
}
// BuildEvaluationError constructs the evaluation error string to include in *SubjectAccessReview status
// based on the authorizer evaluation error and any field and label selector parse errors.
func BuildEvaluationError(evaluationError error, attrs authorizer.AttributesRecord) string {
var evaluationErrors []string
if evaluationError != nil {
evaluationErrors = append(evaluationErrors, evaluationError.Error())
}
if reqs, err := attrs.GetFieldSelector(); err != nil {
if len(reqs) > 0 {
evaluationErrors = append(evaluationErrors, "spec.resourceAttributes.fieldSelector partially ignored due to parse error")
} else {
evaluationErrors = append(evaluationErrors, "spec.resourceAttributes.fieldSelector ignored due to parse error")
}
}
if reqs, err := attrs.GetLabelSelector(); err != nil {
if len(reqs) > 0 {
evaluationErrors = append(evaluationErrors, "spec.resourceAttributes.labelSelector partially ignored due to parse error")
} else {
evaluationErrors = append(evaluationErrors, "spec.resourceAttributes.labelSelector ignored due to parse error")
}
}
return strings.Join(evaluationErrors, "; ")
}