github.com/choria-io/go-choria@v0.28.1-0.20240416190746-b3bf9c7d5a45/providers/agent/mcorpc/authz_actionpolicy.go (about) 1 // Copyright (c) 2020-2021, R.I. Pienaar and the Choria Project contributors 2 // 3 // SPDX-License-Identifier: Apache-2.0 4 5 package mcorpc 6 7 import ( 8 "bufio" 9 "fmt" 10 "os" 11 "path/filepath" 12 "regexp" 13 "strings" 14 15 "github.com/choria-io/go-choria/config" 16 "github.com/choria-io/go-choria/filter" 17 "github.com/choria-io/go-choria/filter/classes" 18 "github.com/choria-io/go-choria/filter/facts" 19 "github.com/choria-io/go-choria/internal/util" 20 21 "github.com/sirupsen/logrus" 22 ) 23 24 func actionPolicyAuthorize(req *Request, agent *Agent, log *logrus.Entry) bool { 25 logger := log.WithFields(logrus.Fields{ 26 "authorizer": "actionpolicy", 27 "agent": agent.Name(), 28 "request": req.RequestID, 29 }) 30 31 authz := &actionPolicy{ 32 cfg: agent.Config, 33 req: req, 34 agent: agent, 35 matcher: &actionPolicyPolicy{log: logger}, 36 groups: make(map[string][]string), 37 log: logger, 38 } 39 40 err := authz.parseGroupFile("") 41 if err != nil { 42 authz.log.Errorf("failed to parse groups file: %s", err) 43 } 44 45 return authz.authorize() 46 } 47 48 type actionPolicy struct { 49 cfg *config.Config 50 req *Request 51 agent *Agent 52 log *logrus.Entry 53 matcher *actionPolicyPolicy 54 groups map[string][]string 55 } 56 57 func (a *actionPolicy) authorize() bool { 58 policyFile, err := a.lookupPolicyFile() 59 if err != nil { 60 a.log.Errorf("Could not lookup policy files: %s", err) 61 return false 62 } 63 64 if policyFile == "" { 65 if a.allowUnconfigured() { 66 a.log.Infof("Allowing unconfigured agent request after failing to find any suitable policy file") 67 return true 68 } 69 70 a.log.Infof("Denying unconfigured agent request after failing to find any suitable policy file") 71 return false 72 } 73 74 allowed, reason, err := a.evaluatePolicy(policyFile) 75 if err != nil { 76 a.log.Errorf("Authorizing request %s failed: %s", a.req.RequestID, err) 77 return false 78 } 79 80 if !allowed { 81 a.log.Infof("Denying request %s: %s", a.req.RequestID, reason) 82 return false 83 } 84 85 return true 86 } 87 88 func (a *actionPolicy) evaluatePolicy(f string) (allowed bool, denyreason string, err error) { 89 a.log.Debugf("Parsing policy %s", f) 90 a.matcher.SetFile(f) 91 92 pf, err := os.Open(f) 93 if err != nil { 94 return false, "", err 95 } 96 defer pf.Close() 97 98 commentRe := regexp.MustCompile(`^(#.*|\s*)$`) 99 defaultRe := regexp.MustCompile(`^policy\s+default\s+(\w+)`) 100 policyRe := regexp.MustCompile(`^(allow|deny)\t+(.+?)\t+(.+?)\t+(.+?)(\t+(.+?))*$`) 101 allowed = a.allowUnconfigured() 102 103 scanner := bufio.NewScanner(pf) 104 for scanner.Scan() { 105 line := scanner.Text() 106 107 if commentRe.MatchString(line) { 108 continue 109 } 110 111 if defaultRe.MatchString(line) { 112 matched := defaultRe.FindStringSubmatch(line) 113 if matched[1] == "allow" { 114 a.log.Debugf("found default allow line: %s", line) 115 allowed = true 116 } else { 117 a.log.Debugf("found default deny line: %s", line) 118 allowed = false 119 } 120 121 } else if policyRe.MatchString(line) { 122 matched := policyRe.FindStringSubmatch(line) 123 if a.matcher.IsCompound(matched[4]) || a.matcher.IsCompound(matched[6]) { 124 a.log.Warnf("Compound policy statements are not supported, skipping line: %s", line) 125 continue 126 } 127 128 a.matcher.Set(matched[2], matched[3], matched[4], matched[6], a.groups) 129 pmatch, err := a.checkRequestAgainstPolicy() 130 if err != nil { 131 return false, "", err 132 } 133 134 if pmatch { 135 if matched[1] == "allow" { 136 return true, "", nil 137 } 138 139 return false, fmt.Sprintf("Denying based on explicit 'deny' policy in %s", filepath.Base(f)), nil 140 } 141 142 } else { 143 a.log.Warnf("invalid policy line: %s", line) 144 continue 145 } 146 } 147 148 err = scanner.Err() 149 if err != nil { 150 return false, "", err 151 } 152 153 if allowed { 154 return allowed, "", nil 155 } 156 157 return allowed, fmt.Sprintf("Denying based on default policy in %s", filepath.Base(f)), nil 158 } 159 160 func (a *actionPolicy) checkRequestAgainstPolicy() (bool, error) { 161 pol := a.matcher 162 163 if !pol.MatchesCallerID(a.req.CallerID) { 164 return false, nil 165 } 166 167 if !pol.MatchesAction(a.req.Action) { 168 return false, nil 169 } 170 171 factsMatched, err := pol.MatchesFacts(a.agent.Config, a.log) 172 if err != nil { 173 return false, err 174 } 175 176 classesMatched, err := pol.MatchesClasses(a.cfg.ClassesFile, a.log) 177 if err != nil { 178 return false, err 179 } 180 181 return classesMatched && factsMatched, nil 182 } 183 184 func (a *actionPolicy) allowUnconfigured() bool { 185 unconfigured, err := util.StrToBool(a.cfg.Option("plugin.actionpolicy.allow_unconfigured", "n")) 186 if err != nil { 187 return false 188 } 189 190 return unconfigured 191 } 192 193 func (a *actionPolicy) shouldUseDefault() bool { 194 enabled, err := util.StrToBool(a.cfg.Option("plugin.actionpolicy.enable_default", "n")) 195 if err != nil { 196 return false 197 } 198 199 return enabled 200 } 201 202 func (a *actionPolicy) defaultPolicyFileName() string { 203 return a.cfg.Option("plugin.actionpolicy.default_name", "default") 204 } 205 206 func (a *actionPolicy) lookupPolicyFile() (string, error) { 207 agentPolicy := filepath.Join(filepath.Dir(a.cfg.ConfigFile), "policies", a.agent.Name()+".policy") 208 209 a.log.Debugf("Looking up agent policy in %s", agentPolicy) 210 if util.FileExist(agentPolicy) { 211 return agentPolicy, nil 212 } 213 214 if a.shouldUseDefault() { 215 defaultPolicy := filepath.Join(filepath.Dir(a.cfg.ConfigFile), "policies", a.defaultPolicyFileName()+".policy") 216 if util.FileExist(defaultPolicy) { 217 return defaultPolicy, nil 218 } 219 } 220 221 return "", fmt.Errorf("no policy found for %s", a.agent.Name()) 222 } 223 224 func (a *actionPolicy) parseGroupFile(gfile string) error { 225 if gfile == "" { 226 gfile = filepath.Join(filepath.Dir(a.cfg.ConfigFile), "policies", "groups") 227 } 228 229 if !util.FileExist(gfile) { 230 return nil 231 } 232 233 gf, err := os.Open(gfile) 234 if err != nil { 235 return err 236 } 237 defer gf.Close() 238 239 commentRe := regexp.MustCompile(`^(#.*|\s*)$`) 240 groupRe := regexp.MustCompile(`^([\w\.\-]+)$`) 241 242 scanner := bufio.NewScanner(gf) 243 for scanner.Scan() { 244 line := scanner.Text() 245 246 if commentRe.MatchString(line) { 247 continue 248 } 249 250 parts := strings.Split(line, " ") 251 if len(parts) < 2 { 252 a.log.Errorf("invalid group line in %s: %s", gfile, line) 253 continue 254 } 255 256 if !groupRe.MatchString(parts[0]) { 257 a.log.Errorf("invalid group name in %s: %s", gfile, parts[0]) 258 continue 259 } 260 261 a.groups[parts[0]] = parts[1:] 262 } 263 264 err = scanner.Err() 265 if err != nil { 266 return err 267 } 268 269 return nil 270 } 271 272 type actionPolicyPolicy struct { 273 caller string 274 actions string 275 facts string 276 classes string 277 groups map[string][]string 278 log *logrus.Entry 279 file string 280 } 281 282 func (p *actionPolicyPolicy) Set(caller string, actions string, facts string, classes string, groups map[string][]string) { 283 p.caller = caller 284 p.actions = actions 285 p.facts = facts 286 p.classes = classes 287 p.groups = groups 288 } 289 290 func (p *actionPolicyPolicy) MatchesFacts(cfg *config.Config, log *logrus.Entry) (bool, error) { 291 if p.facts == "" { 292 return false, fmt.Errorf("empty fact policy found") 293 } 294 295 if p.facts == "*" { 296 return true, nil 297 } 298 299 if p.IsCompound(p.facts) { 300 return false, fmt.Errorf("compound statements are not supported") 301 } 302 303 matches := [][3]string{} 304 305 for _, f := range strings.Split(p.facts, " ") { 306 filter, err := filter.ParseFactFilterString(f) 307 if err != nil { 308 return false, fmt.Errorf("invalid fact matcher: %s", err) 309 } 310 311 matches = append(matches, [3]string{filter.Fact, filter.Operator, filter.Value}) 312 } 313 314 if facts.MatchFile(matches, cfg.FactSourceFile, log) { 315 return true, nil 316 } 317 318 return false, nil 319 } 320 321 func (p *actionPolicyPolicy) MatchesClasses(classesFile string, log *logrus.Entry) (bool, error) { 322 if p.classes == "*" { 323 return true, nil 324 } 325 326 if p.classes == "" { 327 return false, fmt.Errorf("empty classes policy found") 328 } 329 330 if classesFile == "" { 331 return false, fmt.Errorf("do not know how to resolve classes") 332 } 333 334 if p.IsCompound(p.classes) { 335 return false, fmt.Errorf("compound statements are not supported") 336 } 337 338 factMatcher := regexp.MustCompile(`(.+)(<|>|=|<=|>=)(.+)`) 339 for _, c := range strings.Split(p.classes, " ") { 340 if factMatcher.MatchString(c) { 341 return false, fmt.Errorf("fact found where class was expected") 342 } 343 } 344 345 return classes.MatchFile(strings.Split(p.classes, " "), classesFile, log), nil 346 } 347 348 func (p *actionPolicyPolicy) MatchesAction(act string) bool { 349 if p.actions == "" { 350 return false 351 } 352 353 if p.actions == "*" { 354 return true 355 } 356 357 for _, a := range strings.Split(p.actions, " ") { 358 if act == a { 359 return true 360 } 361 } 362 363 return false 364 } 365 366 func (p *actionPolicyPolicy) MatchesCallerID(id string) bool { 367 if p.caller == "" { 368 return false 369 } 370 371 if p.caller == "*" { 372 return true 373 } 374 375 if p.isCallerInGroups(id) { 376 return true 377 } 378 379 regexIdsMatcher := regexp.MustCompile("^/(.+)/$") 380 381 for _, c := range strings.Split(p.caller, " ") { 382 if c == id { 383 return true 384 } 385 386 if strings.HasPrefix(c, "/") { 387 if !regexIdsMatcher.MatchString(c) { 388 p.log.Errorf("Invalid CallerID matcher '%s' found in policy file %s", c, p.file) 389 return false 390 } 391 392 matched := regexIdsMatcher.FindStringSubmatch(c) 393 394 re, err := regexp.Compile(matched[1]) 395 if err != nil { 396 p.log.Errorf("Could not compile regex found in CallerID '%s' in policy file %s: %s", c, p.file, err) 397 return false 398 } 399 400 if re.MatchString(id) { 401 return true 402 } 403 } 404 } 405 406 return false 407 } 408 409 // SetFile sets the file being parsed for errors and logging purposes 410 func (p *actionPolicyPolicy) SetFile(f string) { 411 p.file = f 412 } 413 414 // IsCompound checks if the string is a compound statement 415 func (p *actionPolicyPolicy) IsCompound(line string) bool { 416 matcher := regexp.MustCompile(`^!|^not$|^or$|^and$|\(.+\)`) 417 418 for _, l := range strings.Split(line, " ") { 419 if matcher.MatchString(l) { 420 return true 421 } 422 } 423 424 return false 425 } 426 427 func (p *actionPolicyPolicy) isCallerInGroups(id string) bool { 428 for _, g := range strings.Split(p.caller, " ") { 429 group, ok := p.groups[g] 430 if !ok { 431 continue 432 } 433 434 for _, member := range group { 435 if member == id { 436 return true 437 } 438 } 439 } 440 441 return false 442 }