mirror of https://github.com/k3s-io/k3s
274 lines
7.1 KiB
Go
274 lines
7.1 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
|
|
|
|
// Policy authorizes Kubernetes API actions using an Attribute-based access
|
|
// control scheme.
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"k8s.io/klog"
|
|
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apiserver/pkg/authentication/user"
|
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
|
"k8s.io/kubernetes/pkg/apis/abac"
|
|
_ "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)
|
|
}
|
|
|
|
type policyList []*abac.Policy
|
|
|
|
// 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
|
|
}
|
|
|
|
// Authorizer implements authorizer.Authorize
|
|
func (pl policyList) Authorize(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.
|
|
}
|
|
|
|
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{"*"}
|
|
}
|