github.com/choria-io/go-choria@v0.28.1-0.20240416190746-b3bf9c7d5a45/providers/agent/mcorpc/authz_jwt_test.go (about)

     1  // Copyright (c) 2022, R.I. Pienaar and the Choria Project contributors
     2  //
     3  // SPDX-License-Identifier: Apache-2.0
     4  
     5  package mcorpc
     6  
     7  import (
     8  	"crypto/ed25519"
     9  	"encoding/json"
    10  	"fmt"
    11  	"os"
    12  	"time"
    13  
    14  	imock "github.com/choria-io/go-choria/inter/imocks"
    15  	iu "github.com/choria-io/go-choria/internal/util"
    16  	"github.com/choria-io/go-choria/protocol"
    17  	"github.com/choria-io/go-choria/server/agents"
    18  	"github.com/choria-io/tokens"
    19  	"github.com/golang/mock/gomock"
    20  	. "github.com/onsi/ginkgo/v2"
    21  	. "github.com/onsi/gomega"
    22  	"github.com/onsi/gomega/gbytes"
    23  	"github.com/sirupsen/logrus"
    24  )
    25  
    26  var _ = Describe("McoRPC/JWTAuthorizer", func() {
    27  	var log *logrus.Entry
    28  	var req *Request
    29  	var claims *tokens.ClientIDClaims
    30  
    31  	readFixture := func(f string) string {
    32  		c, err := os.ReadFile(f)
    33  		if err != nil {
    34  			panic(err)
    35  		}
    36  
    37  		return string(c)
    38  	}
    39  
    40  	BeforeEach(func() {
    41  		logger := logrus.New()
    42  		logger.Out = GinkgoWriter
    43  		logger.Level = logrus.DebugLevel
    44  		log = logrus.NewEntry(logger)
    45  		claims = &tokens.ClientIDClaims{}
    46  
    47  		req = &Request{
    48  			Agent:      "myco",
    49  			Action:     "deploy",
    50  			Data:       json.RawMessage(`{"component":"frontend"}`),
    51  			SenderID:   "some.node",
    52  			Collective: "ginkgo",
    53  			TTL:        60,
    54  			Time:       time.Now(),
    55  			Filter:     protocol.NewFilter(),
    56  		}
    57  	})
    58  
    59  	Describe("aaasvcPolicyAuthorize", func() {
    60  		var agent *Agent
    61  		var logBuff *gbytes.Buffer
    62  		var pubk ed25519.PublicKey
    63  		var prik ed25519.PrivateKey
    64  		var err error
    65  
    66  		BeforeEach(func() {
    67  			logBuff = gbytes.NewBuffer()
    68  			mockctl := gomock.NewController(GinkgoT())
    69  			DeferCleanup(func() {
    70  				mockctl.Finish()
    71  			})
    72  
    73  			fw, cfg := imock.NewFrameworkForTests(mockctl, logBuff)
    74  			log = fw.Logger("ginkgo")
    75  			log.Logger.SetLevel(logrus.DebugLevel)
    76  
    77  			agent = &Agent{
    78  				meta:   &agents.Metadata{Name: "myco"},
    79  				Log:    log,
    80  				Config: cfg,
    81  				Choria: fw,
    82  			}
    83  
    84  			pubk, prik, err = iu.Ed25519KeyPair()
    85  			Expect(err).ToNot(HaveOccurred())
    86  		})
    87  
    88  		It("Should fail for no caller public data", func() {
    89  			allowed, err := aaasvcPolicyAuthorize(req, agent, log)
    90  			Expect(err).To(MatchError("no policy received in request"))
    91  			Expect(allowed).To(BeFalse())
    92  		})
    93  
    94  		It("Should handle invalid tokens", func() {
    95  			req.CallerPublicData = "blah"
    96  			allowed, err := aaasvcPolicyAuthorize(req, agent, log)
    97  			Expect(err).To(MatchError("invalid token in request: token contains an invalid number of segments"))
    98  			Expect(allowed).To(BeFalse())
    99  		})
   100  
   101  		It("Should allow discovery agent", func() {
   102  			claims, err = tokens.NewClientIDClaims("ginkgo", nil, "choria", nil, "", "", time.Hour, nil, pubk)
   103  			Expect(err).ToNot(HaveOccurred())
   104  			req.CallerPublicData, err = tokens.SignToken(claims, prik)
   105  			Expect(err).ToNot(HaveOccurred())
   106  
   107  			req.Agent = "discovery"
   108  			allowed, err := aaasvcPolicyAuthorize(req, agent, log)
   109  			Expect(err).ToNot(HaveOccurred())
   110  			Expect(allowed).To(BeTrue())
   111  			Expect(logBuff).To(gbytes.Say("Allowing discovery request"))
   112  		})
   113  
   114  		It("Should require a policy", func() {
   115  			claims, err = tokens.NewClientIDClaims("ginkgo", nil, "choria", nil, "", "", time.Hour, nil, pubk)
   116  			Expect(err).ToNot(HaveOccurred())
   117  			req.CallerPublicData, err = tokens.SignToken(claims, prik)
   118  			Expect(err).ToNot(HaveOccurred())
   119  
   120  			allowed, err := aaasvcPolicyAuthorize(req, agent, log)
   121  			Expect(err).To(MatchError("no policy received in token"))
   122  			Expect(allowed).To(BeFalse())
   123  		})
   124  
   125  		Context("Allowed Agents", func() {
   126  			It("Should handle failures", func() {
   127  				claims, err = tokens.NewClientIDClaims("ginkgo", []string{"fail"}, "choria", nil, "", "", time.Hour, nil, pubk)
   128  				Expect(err).ToNot(HaveOccurred())
   129  				req.CallerPublicData, err = tokens.SignToken(claims, prik)
   130  				Expect(err).ToNot(HaveOccurred())
   131  
   132  				allowed, err := aaasvcPolicyAuthorize(req, agent, log)
   133  				Expect(err).To(MatchError("invalid agent policy: fail"))
   134  				Expect(allowed).To(BeFalse())
   135  			})
   136  
   137  			It("Should allow valid requests", func() {
   138  				claims, err = tokens.NewClientIDClaims("ginkgo", []string{"myco.deploy"}, "choria", nil, "", "", time.Hour, nil, pubk)
   139  				Expect(err).ToNot(HaveOccurred())
   140  				req.CallerPublicData, err = tokens.SignToken(claims, prik)
   141  				Expect(err).ToNot(HaveOccurred())
   142  
   143  				allowed, err := aaasvcPolicyAuthorize(req, agent, log)
   144  				Expect(err).ToNot(HaveOccurred())
   145  				Expect(allowed).To(BeTrue())
   146  			})
   147  		})
   148  
   149  		Context("OPA Policy", func() {
   150  			It("Should handle failures", func() {
   151  				claims, err = tokens.NewClientIDClaims("ginkgo", nil, "choria", nil, "invalid rego", "", time.Hour, nil, pubk)
   152  				Expect(err).ToNot(HaveOccurred())
   153  				req.CallerPublicData, err = tokens.SignToken(claims, prik)
   154  				Expect(err).ToNot(HaveOccurred())
   155  
   156  				allowed, err := aaasvcPolicyAuthorize(req, agent, log)
   157  				Expect(err).To(HaveOccurred())
   158  				Expect(err.Error()).To(MatchRegexp("could not initialize opa evaluator"))
   159  				Expect(allowed).To(BeFalse())
   160  			})
   161  
   162  			It("Should allow valid requests", func() {
   163  				claims, err = tokens.NewClientIDClaims("ginkgo", nil, "choria", nil, readFixture("testdata/policies/rego/aaa_scenario1.rego"), "", time.Hour, nil, pubk)
   164  				Expect(err).ToNot(HaveOccurred())
   165  				req.CallerPublicData, err = tokens.SignToken(claims, prik)
   166  				Expect(err).ToNot(HaveOccurred())
   167  
   168  				allowed, err := aaasvcPolicyAuthorize(req, agent, log)
   169  				Expect(err).ToNot(HaveOccurred())
   170  				Expect(allowed).To(BeTrue())
   171  			})
   172  		})
   173  	})
   174  
   175  	Describe("EvaluateAgentListPolicy", func() {
   176  		It("Should support '*' agents", func() {
   177  			ok, err := EvaluateAgentListPolicy("agent", "action", []string{"*"}, log)
   178  			Expect(ok).To(BeTrue())
   179  			Expect(err).ToNot(HaveOccurred())
   180  		})
   181  
   182  		It("Should support action wildcards", func() {
   183  			ok, err := EvaluateAgentListPolicy("rpcutil", "action", []string{"rpcutil.*"}, log)
   184  			Expect(ok).To(BeTrue())
   185  			Expect(err).ToNot(HaveOccurred())
   186  
   187  			ok, err = EvaluateAgentListPolicy("other", "action", []string{"rpcutil.*"}, log)
   188  			Expect(err).ToNot(HaveOccurred())
   189  			Expect(ok).To(BeFalse())
   190  		})
   191  
   192  		It("Should support specific agent.action", func() {
   193  			ok, err := EvaluateAgentListPolicy("rpcutil", "ping", []string{"rpcutil.ping"}, log)
   194  			Expect(ok).To(BeTrue())
   195  			Expect(err).ToNot(HaveOccurred())
   196  
   197  			ok, err = EvaluateAgentListPolicy("rpcutil", "other", []string{"rpcutil.ping"}, log)
   198  			Expect(err).ToNot(HaveOccurred())
   199  			Expect(ok).To(BeFalse())
   200  
   201  			ok, err = EvaluateAgentListPolicy("other", "action", []string{"rpcutil.ping"}, log)
   202  			Expect(err).ToNot(HaveOccurred())
   203  			Expect(ok).To(BeFalse())
   204  		})
   205  
   206  		It("Should handle invalid policies", func() {
   207  			ok, err := EvaluateAgentListPolicy("rpcutil", "ping", []string{"rpcutil"}, log)
   208  			Expect(ok).To(BeFalse())
   209  			Expect(err).To(MatchError("invalid agent policy: rpcutil"))
   210  
   211  		})
   212  	})
   213  
   214  	Describe("EvaluateOpenPolicyAgentPolicy", func() {
   215  		It("Should allow common scenarios", func() {
   216  			req.Filter.AddClassFilter("apache")
   217  			req.Filter.AddIdentityFilter("some.node")
   218  			req.Filter.AddFactFilter("country", "==", "mt")
   219  
   220  			claims.CallerID = "up=bob"
   221  			claims.UserProperties = map[string]string{
   222  				"group": "admins",
   223  			}
   224  
   225  			for r := 1; r <= 5; r++ {
   226  				policy := readFixture(fmt.Sprintf("testdata/policies/rego/aaa_scenario%d.rego", r))
   227  				claims.OPAPolicy = policy
   228  
   229  				allowed, err := EvaluateOpenPolicyAgentPolicy(req, policy, claims, "ginkgo", log)
   230  				Expect(err).ToNot(HaveOccurred())
   231  				Expect(allowed).To(BeTrue())
   232  			}
   233  		})
   234  
   235  		It("Should fail on all common scenarios", func() {
   236  			policy := readFixture("testdata/policies/rego/aaa_scenario5.rego")
   237  			claims.OPAPolicy = policy
   238  			claims.CallerID = "up=bob"
   239  			claims.UserProperties = map[string]string{
   240  				"group": "admins",
   241  			}
   242  
   243  			req.Filter.AddClassFilter("apache")
   244  			req.Filter.AddIdentityFilter("some.node")
   245  			req.Filter.AddFactFilter("country", "==", "mt")
   246  
   247  			allowed, err := EvaluateOpenPolicyAgentPolicy(req, policy, claims, "ginkgo", log)
   248  			Expect(err).ToNot(HaveOccurred())
   249  			Expect(allowed).To(BeTrue())
   250  
   251  			allowed, err = EvaluateOpenPolicyAgentPolicy(req, policy, claims, "x", log)
   252  			Expect(err).ToNot(HaveOccurred())
   253  			Expect(allowed).To(BeFalse())
   254  		})
   255  	})
   256  })