...

Source file src/k8s.io/kubernetes/pkg/auth/authorizer/abac/abac.go

Documentation: k8s.io/kubernetes/pkg/auth/authorizer/abac

     1  /*
     2  Copyright 2014 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  // Package abac authorizes Kubernetes API actions using an Attribute-based access control scheme.
    18  package abac
    19  
    20  import (
    21  	"bufio"
    22  	"context"
    23  	"fmt"
    24  	"os"
    25  	"strings"
    26  
    27  	"k8s.io/klog/v2"
    28  
    29  	"k8s.io/apimachinery/pkg/runtime"
    30  	"k8s.io/apiserver/pkg/authentication/user"
    31  	"k8s.io/apiserver/pkg/authorization/authorizer"
    32  	"k8s.io/kubernetes/pkg/apis/abac"
    33  
    34  	// Import latest API for init/side-effects
    35  	_ "k8s.io/kubernetes/pkg/apis/abac/latest"
    36  	"k8s.io/kubernetes/pkg/apis/abac/v0"
    37  )
    38  
    39  type policyLoadError struct {
    40  	path string
    41  	line int
    42  	data []byte
    43  	err  error
    44  }
    45  
    46  func (p policyLoadError) Error() string {
    47  	if p.line >= 0 {
    48  		return fmt.Sprintf("error reading policy file %s, line %d: %s: %v", p.path, p.line, string(p.data), p.err)
    49  	}
    50  	return fmt.Sprintf("error reading policy file %s: %v", p.path, p.err)
    51  }
    52  
    53  // PolicyList is simply a slice of Policy structs.
    54  type PolicyList []*abac.Policy
    55  
    56  // NewFromFile attempts to create a policy list from the given file.
    57  //
    58  // TODO: Have policies be created via an API call and stored in REST storage.
    59  func NewFromFile(path string) (PolicyList, error) {
    60  	// File format is one map per line.  This allows easy concatenation of files,
    61  	// comments in files, and identification of errors by line number.
    62  	file, err := os.Open(path)
    63  	if err != nil {
    64  		return nil, err
    65  	}
    66  	defer file.Close()
    67  
    68  	scanner := bufio.NewScanner(file)
    69  	pl := make(PolicyList, 0)
    70  
    71  	decoder := abac.Codecs.UniversalDecoder()
    72  
    73  	i := 0
    74  	unversionedLines := 0
    75  	for scanner.Scan() {
    76  		i++
    77  		p := &abac.Policy{}
    78  		b := scanner.Bytes()
    79  
    80  		// skip comment lines and blank lines
    81  		trimmed := strings.TrimSpace(string(b))
    82  		if len(trimmed) == 0 || strings.HasPrefix(trimmed, "#") {
    83  			continue
    84  		}
    85  
    86  		decodedObj, _, err := decoder.Decode(b, nil, nil)
    87  		if err != nil {
    88  			if !(runtime.IsMissingVersion(err) || runtime.IsMissingKind(err) || runtime.IsNotRegisteredError(err)) {
    89  				return nil, policyLoadError{path, i, b, err}
    90  			}
    91  			unversionedLines++
    92  			// Migrate unversioned policy object
    93  			oldPolicy := &v0.Policy{}
    94  			if err := runtime.DecodeInto(decoder, b, oldPolicy); err != nil {
    95  				return nil, policyLoadError{path, i, b, err}
    96  			}
    97  			if err := abac.Scheme.Convert(oldPolicy, p, nil); err != nil {
    98  				return nil, policyLoadError{path, i, b, err}
    99  			}
   100  			pl = append(pl, p)
   101  			continue
   102  		}
   103  
   104  		decodedPolicy, ok := decodedObj.(*abac.Policy)
   105  		if !ok {
   106  			return nil, policyLoadError{path, i, b, fmt.Errorf("unrecognized object: %#v", decodedObj)}
   107  		}
   108  		pl = append(pl, decodedPolicy)
   109  	}
   110  
   111  	if unversionedLines > 0 {
   112  		klog.Warningf("Policy file %s contained unversioned rules. See docs/admin/authorization.md#abac-mode for ABAC file format details.", path)
   113  	}
   114  
   115  	if err := scanner.Err(); err != nil {
   116  		return nil, policyLoadError{path, -1, nil, err}
   117  	}
   118  	return pl, nil
   119  }
   120  
   121  func matches(p abac.Policy, a authorizer.Attributes) bool {
   122  	if subjectMatches(p, a.GetUser()) {
   123  		if verbMatches(p, a) {
   124  			// Resource and non-resource requests are mutually exclusive, at most one will match a policy
   125  			if resourceMatches(p, a) {
   126  				return true
   127  			}
   128  			if nonResourceMatches(p, a) {
   129  				return true
   130  			}
   131  		}
   132  	}
   133  	return false
   134  }
   135  
   136  // subjectMatches returns true if specified user and group properties in the policy match the attributes
   137  func subjectMatches(p abac.Policy, user user.Info) bool {
   138  	matched := false
   139  
   140  	if user == nil {
   141  		return false
   142  	}
   143  	username := user.GetName()
   144  	groups := user.GetGroups()
   145  
   146  	// If the policy specified a user, ensure it matches
   147  	if len(p.Spec.User) > 0 {
   148  		if p.Spec.User == "*" {
   149  			matched = true
   150  		} else {
   151  			matched = p.Spec.User == username
   152  			if !matched {
   153  				return false
   154  			}
   155  		}
   156  	}
   157  
   158  	// If the policy specified a group, ensure it matches
   159  	if len(p.Spec.Group) > 0 {
   160  		if p.Spec.Group == "*" {
   161  			matched = true
   162  		} else {
   163  			matched = false
   164  			for _, group := range groups {
   165  				if p.Spec.Group == group {
   166  					matched = true
   167  					break
   168  				}
   169  			}
   170  			if !matched {
   171  				return false
   172  			}
   173  		}
   174  	}
   175  
   176  	return matched
   177  }
   178  
   179  func verbMatches(p abac.Policy, a authorizer.Attributes) bool {
   180  	// TODO: match on verb
   181  
   182  	// All policies allow read only requests
   183  	if a.IsReadOnly() {
   184  		return true
   185  	}
   186  
   187  	// Allow if policy is not readonly
   188  	if !p.Spec.Readonly {
   189  		return true
   190  	}
   191  
   192  	return false
   193  }
   194  
   195  func nonResourceMatches(p abac.Policy, a authorizer.Attributes) bool {
   196  	// A non-resource policy cannot match a resource request
   197  	if !a.IsResourceRequest() {
   198  		// Allow wildcard match
   199  		if p.Spec.NonResourcePath == "*" {
   200  			return true
   201  		}
   202  		// Allow exact match
   203  		if p.Spec.NonResourcePath == a.GetPath() {
   204  			return true
   205  		}
   206  		// Allow a trailing * subpath match
   207  		if strings.HasSuffix(p.Spec.NonResourcePath, "*") && strings.HasPrefix(a.GetPath(), strings.TrimRight(p.Spec.NonResourcePath, "*")) {
   208  			return true
   209  		}
   210  	}
   211  	return false
   212  }
   213  
   214  func resourceMatches(p abac.Policy, a authorizer.Attributes) bool {
   215  	// A resource policy cannot match a non-resource request
   216  	if a.IsResourceRequest() {
   217  		if p.Spec.Namespace == "*" || p.Spec.Namespace == a.GetNamespace() {
   218  			if p.Spec.Resource == "*" || p.Spec.Resource == a.GetResource() {
   219  				if p.Spec.APIGroup == "*" || p.Spec.APIGroup == a.GetAPIGroup() {
   220  					return true
   221  				}
   222  			}
   223  		}
   224  	}
   225  	return false
   226  }
   227  
   228  // Authorize implements authorizer.Authorize
   229  func (pl PolicyList) Authorize(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
   230  	for _, p := range pl {
   231  		if matches(*p, a) {
   232  			return authorizer.DecisionAllow, "", nil
   233  		}
   234  	}
   235  	return authorizer.DecisionNoOpinion, "No policy matched.", nil
   236  	// TODO: Benchmark how much time policy matching takes with a medium size
   237  	// policy file, compared to other steps such as encoding/decoding.
   238  	// Then, add Caching only if needed.
   239  }
   240  
   241  // RulesFor returns rules for the given user and namespace.
   242  func (pl PolicyList) RulesFor(user user.Info, namespace string) ([]authorizer.ResourceRuleInfo, []authorizer.NonResourceRuleInfo, bool, error) {
   243  	var (
   244  		resourceRules    []authorizer.ResourceRuleInfo
   245  		nonResourceRules []authorizer.NonResourceRuleInfo
   246  	)
   247  
   248  	for _, p := range pl {
   249  		if subjectMatches(*p, user) {
   250  			if p.Spec.Namespace == "*" || p.Spec.Namespace == namespace {
   251  				if len(p.Spec.Resource) > 0 {
   252  					r := authorizer.DefaultResourceRuleInfo{
   253  						Verbs:     getVerbs(p.Spec.Readonly),
   254  						APIGroups: []string{p.Spec.APIGroup},
   255  						Resources: []string{p.Spec.Resource},
   256  					}
   257  					var resourceRule authorizer.ResourceRuleInfo = &r
   258  					resourceRules = append(resourceRules, resourceRule)
   259  				}
   260  				if len(p.Spec.NonResourcePath) > 0 {
   261  					r := authorizer.DefaultNonResourceRuleInfo{
   262  						Verbs:           getVerbs(p.Spec.Readonly),
   263  						NonResourceURLs: []string{p.Spec.NonResourcePath},
   264  					}
   265  					var nonResourceRule authorizer.NonResourceRuleInfo = &r
   266  					nonResourceRules = append(nonResourceRules, nonResourceRule)
   267  				}
   268  			}
   269  		}
   270  	}
   271  	return resourceRules, nonResourceRules, false, nil
   272  }
   273  
   274  func getVerbs(isReadOnly bool) []string {
   275  	if isReadOnly {
   276  		return []string{"get", "list", "watch"}
   277  	}
   278  	return []string{"*"}
   279  }
   280  

View as plain text