k8s.io/kubernetes@v1.29.3/pkg/auth/authorizer/abac/abac.go (about) 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 }