mirror of
				https://github.com/k3s-io/kubernetes.git
				synced 2025-10-31 05:40:42 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			279 lines
		
	
	
		
			7.4 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			279 lines
		
	
	
		
			7.4 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| /*
 | |
| Copyright 2014 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 abac authorizes Kubernetes API actions using an Attribute-based access control scheme.
 | |
| package abac
 | |
| 
 | |
| import (
 | |
| 	"bufio"
 | |
| 	"context"
 | |
| 	"fmt"
 | |
| 	"os"
 | |
| 	"strings"
 | |
| 
 | |
| 	"k8s.io/klog/v2"
 | |
| 
 | |
| 	"k8s.io/apimachinery/pkg/runtime"
 | |
| 	"k8s.io/apiserver/pkg/authentication/user"
 | |
| 	"k8s.io/apiserver/pkg/authorization/authorizer"
 | |
| 	"k8s.io/kubernetes/pkg/apis/abac"
 | |
| 
 | |
| 	// Import latest API for init/side-effects
 | |
| 	_ "k8s.io/kubernetes/pkg/apis/abac/latest"
 | |
| 	"k8s.io/kubernetes/pkg/apis/abac/v0"
 | |
| )
 | |
| 
 | |
| type policyLoadError struct {
 | |
| 	path string
 | |
| 	line int
 | |
| 	data []byte
 | |
| 	err  error
 | |
| }
 | |
| 
 | |
| func (p policyLoadError) Error() string {
 | |
| 	if p.line >= 0 {
 | |
| 		return fmt.Sprintf("error reading policy file %s, line %d: %s: %v", p.path, p.line, string(p.data), p.err)
 | |
| 	}
 | |
| 	return fmt.Sprintf("error reading policy file %s: %v", p.path, p.err)
 | |
| }
 | |
| 
 | |
| // PolicyList is simply a slice of Policy structs.
 | |
| type PolicyList []*abac.Policy
 | |
| 
 | |
| // NewFromFile attempts to create a policy list from the given file.
 | |
| //
 | |
| // TODO: Have policies be created via an API call and stored in REST storage.
 | |
| func NewFromFile(path string) (PolicyList, error) {
 | |
| 	// File format is one map per line.  This allows easy concatenation of files,
 | |
| 	// comments in files, and identification of errors by line number.
 | |
| 	file, err := os.Open(path)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	defer file.Close()
 | |
| 
 | |
| 	scanner := bufio.NewScanner(file)
 | |
| 	pl := make(PolicyList, 0)
 | |
| 
 | |
| 	decoder := abac.Codecs.UniversalDecoder()
 | |
| 
 | |
| 	i := 0
 | |
| 	unversionedLines := 0
 | |
| 	for scanner.Scan() {
 | |
| 		i++
 | |
| 		p := &abac.Policy{}
 | |
| 		b := scanner.Bytes()
 | |
| 
 | |
| 		// skip comment lines and blank lines
 | |
| 		trimmed := strings.TrimSpace(string(b))
 | |
| 		if len(trimmed) == 0 || strings.HasPrefix(trimmed, "#") {
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		decodedObj, _, err := decoder.Decode(b, nil, nil)
 | |
| 		if err != nil {
 | |
| 			if !(runtime.IsMissingVersion(err) || runtime.IsMissingKind(err) || runtime.IsNotRegisteredError(err)) {
 | |
| 				return nil, policyLoadError{path, i, b, err}
 | |
| 			}
 | |
| 			unversionedLines++
 | |
| 			// Migrate unversioned policy object
 | |
| 			oldPolicy := &v0.Policy{}
 | |
| 			if err := runtime.DecodeInto(decoder, b, oldPolicy); err != nil {
 | |
| 				return nil, policyLoadError{path, i, b, err}
 | |
| 			}
 | |
| 			if err := abac.Scheme.Convert(oldPolicy, p, nil); err != nil {
 | |
| 				return nil, policyLoadError{path, i, b, err}
 | |
| 			}
 | |
| 			pl = append(pl, p)
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		decodedPolicy, ok := decodedObj.(*abac.Policy)
 | |
| 		if !ok {
 | |
| 			return nil, policyLoadError{path, i, b, fmt.Errorf("unrecognized object: %#v", decodedObj)}
 | |
| 		}
 | |
| 		pl = append(pl, decodedPolicy)
 | |
| 	}
 | |
| 
 | |
| 	if unversionedLines > 0 {
 | |
| 		klog.Warningf("Policy file %s contained unversioned rules. See docs/admin/authorization.md#abac-mode for ABAC file format details.", path)
 | |
| 	}
 | |
| 
 | |
| 	if err := scanner.Err(); err != nil {
 | |
| 		return nil, policyLoadError{path, -1, nil, err}
 | |
| 	}
 | |
| 	return pl, nil
 | |
| }
 | |
| 
 | |
| func matches(p abac.Policy, a authorizer.Attributes) bool {
 | |
| 	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) {
 | |
| 				return true
 | |
| 			}
 | |
| 			if nonResourceMatches(p, a) {
 | |
| 				return true
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	return false
 | |
| }
 | |
| 
 | |
| // subjectMatches returns true if specified user and group properties in the policy match the attributes
 | |
| func subjectMatches(p abac.Policy, user user.Info) bool {
 | |
| 	matched := false
 | |
| 
 | |
| 	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 {
 | |
| 		if p.Spec.User == "*" {
 | |
| 			matched = true
 | |
| 		} else {
 | |
| 			matched = p.Spec.User == username
 | |
| 			if !matched {
 | |
| 				return false
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// If the policy specified a group, ensure it matches
 | |
| 	if len(p.Spec.Group) > 0 {
 | |
| 		if p.Spec.Group == "*" {
 | |
| 			matched = true
 | |
| 		} else {
 | |
| 			matched = false
 | |
| 			for _, group := range groups {
 | |
| 				if p.Spec.Group == group {
 | |
| 					matched = true
 | |
| 				}
 | |
| 			}
 | |
| 			if !matched {
 | |
| 				return false
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return matched
 | |
| }
 | |
| 
 | |
| func verbMatches(p abac.Policy, a authorizer.Attributes) bool {
 | |
| 	// TODO: match on verb
 | |
| 
 | |
| 	// All policies allow read only requests
 | |
| 	if a.IsReadOnly() {
 | |
| 		return true
 | |
| 	}
 | |
| 
 | |
| 	// Allow if policy is not readonly
 | |
| 	if !p.Spec.Readonly {
 | |
| 		return true
 | |
| 	}
 | |
| 
 | |
| 	return false
 | |
| }
 | |
| 
 | |
| func nonResourceMatches(p abac.Policy, a authorizer.Attributes) bool {
 | |
| 	// A non-resource policy cannot match a resource request
 | |
| 	if !a.IsResourceRequest() {
 | |
| 		// Allow wildcard match
 | |
| 		if p.Spec.NonResourcePath == "*" {
 | |
| 			return true
 | |
| 		}
 | |
| 		// Allow exact match
 | |
| 		if p.Spec.NonResourcePath == a.GetPath() {
 | |
| 			return true
 | |
| 		}
 | |
| 		// Allow a trailing * subpath match
 | |
| 		if strings.HasSuffix(p.Spec.NonResourcePath, "*") && strings.HasPrefix(a.GetPath(), strings.TrimRight(p.Spec.NonResourcePath, "*")) {
 | |
| 			return true
 | |
| 		}
 | |
| 	}
 | |
| 	return false
 | |
| }
 | |
| 
 | |
| func resourceMatches(p abac.Policy, a authorizer.Attributes) bool {
 | |
| 	// A resource policy cannot match a non-resource request
 | |
| 	if a.IsResourceRequest() {
 | |
| 		if p.Spec.Namespace == "*" || p.Spec.Namespace == a.GetNamespace() {
 | |
| 			if p.Spec.Resource == "*" || p.Spec.Resource == a.GetResource() {
 | |
| 				if p.Spec.APIGroup == "*" || p.Spec.APIGroup == a.GetAPIGroup() {
 | |
| 					return true
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	return false
 | |
| }
 | |
| 
 | |
| // Authorize implements authorizer.Authorize
 | |
| func (pl PolicyList) Authorize(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
 | |
| 	for _, p := range pl {
 | |
| 		if matches(*p, a) {
 | |
| 			return authorizer.DecisionAllow, "", nil
 | |
| 		}
 | |
| 	}
 | |
| 	return authorizer.DecisionNoOpinion, "No policy matched.", nil
 | |
| 	// TODO: Benchmark how much time policy matching takes with a medium size
 | |
| 	// policy file, compared to other steps such as encoding/decoding.
 | |
| 	// Then, add Caching only if needed.
 | |
| }
 | |
| 
 | |
| // RulesFor returns rules for the given user and namespace.
 | |
| 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{"*"}
 | |
| }
 |