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  }