github.com/choria-io/go-choria@v0.28.1-0.20240416190746-b3bf9c7d5a45/providers/agent/mcorpc/authz_rego_test.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  	"context"
     9  	"encoding/json"
    10  	"fmt"
    11  	"os"
    12  	"time"
    13  
    14  	"github.com/choria-io/go-choria/config"
    15  	"github.com/choria-io/go-choria/filter/classes"
    16  	"github.com/choria-io/go-choria/inter"
    17  	imock "github.com/choria-io/go-choria/inter/imocks"
    18  	"github.com/choria-io/go-choria/protocol"
    19  	"github.com/choria-io/go-choria/server/agents"
    20  	"github.com/golang/mock/gomock"
    21  	. "github.com/onsi/ginkgo/v2"
    22  	. "github.com/onsi/gomega"
    23  )
    24  
    25  var _ = Describe("RegoPolicy", func() {
    26  	var (
    27  		mockctl  *gomock.Controller
    28  		fw       *imock.MockFramework
    29  		cfg      *config.Config
    30  		requests = make(chan inter.ConnectorMessage)
    31  		authz    *regoPolicy
    32  		conn     *imock.MockConnector
    33  		connInfo *imock.MockConnectorInfo
    34  		srvInfo  *MockServerInfoSource
    35  		ctx      context.Context
    36  		am       *agents.Manager
    37  		facts    json.RawMessage
    38  		err      error
    39  	)
    40  
    41  	BeforeEach(func() {
    42  		mockctl = gomock.NewController(GinkgoT())
    43  		fw, cfg = imock.NewFrameworkForTests(mockctl, GinkgoWriter)
    44  		fw.EXPECT().ProvisionMode().Return(false).AnyTimes()
    45  	})
    46  
    47  	AfterEach(func() {
    48  		mockctl.Finish()
    49  	})
    50  
    51  	Describe(" tests", func() {
    52  		BeforeEach(func() {
    53  			cfg.ClassesFile = "testdata/policies/rego/classes.txt"
    54  			cfg.FactSourceFile = "testdata/policies/rego/facts.json"
    55  			cfg.ConfigFile = "testdata/server.conf"
    56  			cfg.DisableSecurityProviderVerify = true
    57  
    58  			conn = imock.NewMockConnector(mockctl)
    59  			conn.EXPECT().QueueSubscribe(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
    60  			conn.EXPECT().AgentBroadcastTarget(gomock.AssignableToTypeOf("collective"), gomock.AssignableToTypeOf("agent")).DoAndReturn(func(c, a string) string {
    61  				return fmt.Sprintf("broadcast.%s.%s", c, a)
    62  			}).AnyTimes()
    63  
    64  			connInfo = imock.NewMockConnectorInfo(mockctl)
    65  
    66  			facts, err = os.ReadFile(cfg.FactSourceFile)
    67  			Expect(err).ToNot(HaveOccurred())
    68  			Expect(facts).ToNot(BeEmpty())
    69  
    70  			klasses, err := classes.ReadClasses(cfg.ClassesFile)
    71  			Expect(err).ToNot(HaveOccurred())
    72  
    73  			srvInfo = NewMockServerInfoSource(mockctl)
    74  			srvInfo.EXPECT().Classes().Return(klasses).AnyTimes()
    75  			srvInfo.EXPECT().KnownAgents().Return([]string{"ginkgo", "stub_agent", "buts_agent"}).AnyTimes()
    76  			srvInfo.EXPECT().Facts().DoAndReturn(func() json.RawMessage { return facts })
    77  
    78  			ctx = context.Background()
    79  
    80  			am = agents.New(requests, fw, connInfo, srvInfo, fw.Logger("test"))
    81  			testAgents := []string{"stub_agent", "buts_agent"}
    82  
    83  			// Additional agents for testing comparisons in rego files
    84  			for i := range testAgents {
    85  				metadata := &agents.Metadata{
    86  					Name:    testAgents[i],
    87  					Author:  "stub@example.com",
    88  					License: "Apache-2.0",
    89  					Timeout: 10,
    90  					URL:     "https://choria.io",
    91  					Version: "1.0.0",
    92  				}
    93  
    94  				agent := New(metadata.Name, metadata, am.Choria(), fw.Logger("test"))
    95  				action := func(ctx context.Context, req *Request, reply *Reply, agent *Agent, conn inter.ConnectorInfo) {}
    96  				agent.MustRegisterAction("boop", action)
    97  				Expect(err).ToNot(HaveOccurred())
    98  
    99  				agent.SetServerInfo(srvInfo)
   100  
   101  				err = am.RegisterAgent(ctx, metadata.Name, agent, conn)
   102  				// err = cn.ServerInstance().RegisterAgent(ctx, agent.Name(), agent)
   103  				Expect(err).ToNot(HaveOccurred())
   104  			}
   105  
   106  			metadata := &agents.Metadata{
   107  				Name:    "ginkgo",
   108  				Author:  "stub@example.com",
   109  				License: "Apache-2.0",
   110  				Timeout: 10,
   111  				URL:     "https://choria.io",
   112  				Version: "1.0.0",
   113  			}
   114  
   115  			ginkgoAgent := New(metadata.Name, metadata, am.Choria(), fw.Logger("test"))
   116  			action := func(ctx context.Context, req *Request, reply *Reply, agent *Agent, conn inter.ConnectorInfo) {}
   117  			ginkgoAgent.MustRegisterAction("boop", action)
   118  			ginkgoAgent.SetServerInfo(srvInfo)
   119  			Expect(err).ToNot(HaveOccurred())
   120  
   121  			authz = &regoPolicy{
   122  				cfg: cfg,
   123  				log: fw.Logger("x"),
   124  				req: &Request{
   125  					Agent:    ginkgoAgent.meta.Name,
   126  					Action:   "boop",
   127  					CallerID: "choria=ginkgo.mcollective",
   128  					Data:     json.RawMessage(`{"foo": "bar"}`),
   129  					TTL:      60,
   130  					Time:     time.Now(),
   131  					Filter:   protocol.NewFilter(),
   132  				},
   133  				agent: ginkgoAgent,
   134  			}
   135  		})
   136  
   137  		AfterEach(func() {
   138  			mockctl.Finish()
   139  			// cn.Stop()
   140  			ctx.Done()
   141  		})
   142  
   143  		Describe("Basic tests", func() {
   144  			Context("When the user agent or caller is right", func() {
   145  				It("Should succeed", func() {
   146  					auth, err := authz.authorize()
   147  					Expect(err).ToNot(HaveOccurred())
   148  					Expect(auth).To(BeTrue())
   149  				})
   150  
   151  				It("Default policy should fail", func() {
   152  					authz.agent.meta.Name = "boop"
   153  					auth, err := authz.authorize()
   154  
   155  					Expect(err).ToNot(HaveOccurred())
   156  					Expect(auth).To(BeFalse())
   157  				})
   158  
   159  			})
   160  
   161  			Context("When facts are correct", func() {
   162  				It("Should succeed", func() {
   163  					authz.agent.meta.Name = "facts"
   164  					auth, err := authz.authorize()
   165  					Expect(err).ToNot(HaveOccurred())
   166  					Expect(auth).To(BeTrue())
   167  
   168  				})
   169  			})
   170  
   171  			Context("When classes are present and available", func() {
   172  				It("Should succeed", func() {
   173  					authz.agent.meta.Name = "classes"
   174  					auth, err := authz.authorize()
   175  
   176  					Expect(err).ToNot(HaveOccurred())
   177  					Expect(auth).To(BeTrue())
   178  				})
   179  			})
   180  		})
   181  
   182  		Describe("Failing tests", func() {
   183  			Context("When the user agent or caller is wrong", func() {
   184  				It("Should fail if agent isn't ginkgo", func() {
   185  					authz.req.CallerID = "not=it"
   186  					auth, err := authz.authorize()
   187  
   188  					Expect(err).ToNot(HaveOccurred())
   189  					Expect(auth).To(BeFalse())
   190  				})
   191  
   192  				It("Should fail with a default policy", func() {
   193  					authz.req.CallerID = "not=it"
   194  					authz.agent.meta.Name = "boop"
   195  					Expect(authz.agent.Name()).To(Equal("boop"))
   196  
   197  					authz.cfg.SetOption("plugin.regopolicy.enable_default", "y")
   198  					auth, err := authz.authorize()
   199  
   200  					Expect(err).ToNot(HaveOccurred())
   201  					Expect(auth).To(BeFalse())
   202  				})
   203  			})
   204  		})
   205  
   206  		Describe("Agents", func() {
   207  			Context("If agent exists on the server", func() {
   208  				It("Should succeed", func() {
   209  					authz.agent.meta.Name = "agent"
   210  					auth, err := authz.authorize()
   211  
   212  					Expect(err).ToNot(HaveOccurred())
   213  					Expect(auth).To(BeTrue())
   214  				})
   215  			})
   216  		})
   217  
   218  		Describe("Request data", func() {
   219  			Context("It should succeed if the request parameters are set right", func() {
   220  				It("Should succeed", func() {
   221  					authz.agent.meta.Name = "data"
   222  					auth, err := authz.authorize()
   223  
   224  					Expect(err).ToNot(HaveOccurred())
   225  					Expect(auth).To(BeTrue())
   226  				})
   227  			})
   228  		})
   229  	})
   230  
   231  	Describe("Auth deny tests", func() {
   232  		BeforeEach(func() {
   233  			cfg.ClassesFile = "testdata/policies/rego/classes_fail.txt"
   234  			cfg.FactSourceFile = "testdata/policies/rego/facts_fail.json"
   235  			cfg.ConfigFile = "testdata/server.conf"
   236  			cfg.DisableSecurityProviderVerify = true
   237  
   238  			conn = imock.NewMockConnector(mockctl)
   239  			conn.EXPECT().QueueSubscribe(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
   240  			conn.EXPECT().AgentBroadcastTarget(gomock.AssignableToTypeOf("collective"), gomock.AssignableToTypeOf("agent")).DoAndReturn(func(c, a string) string {
   241  				return fmt.Sprintf("broadcast.%s.%s", c, a)
   242  			}).AnyTimes()
   243  
   244  			connInfo = imock.NewMockConnectorInfo(mockctl)
   245  
   246  			facts, err = os.ReadFile(cfg.FactSourceFile)
   247  			Expect(err).ToNot(HaveOccurred())
   248  			Expect(facts).ToNot(BeEmpty())
   249  
   250  			klasses, err := classes.ReadClasses(cfg.ClassesFile)
   251  			Expect(err).ToNot(HaveOccurred())
   252  
   253  			srvInfo = NewMockServerInfoSource(mockctl)
   254  			srvInfo.EXPECT().Classes().Return(klasses).AnyTimes()
   255  			srvInfo.EXPECT().KnownAgents().Return([]string{"ginkgo"}).AnyTimes()
   256  			srvInfo.EXPECT().Facts().DoAndReturn(func() json.RawMessage { return facts }).AnyTimes()
   257  
   258  			ctx = context.Background()
   259  
   260  			am = agents.New(requests, fw, conn, srvInfo, fw.Logger("test"))
   261  
   262  			metadata := &agents.Metadata{
   263  				Name:    "ginkgo",
   264  				Author:  "stub@example.com",
   265  				License: "Apache-2.0",
   266  				Timeout: 10,
   267  				URL:     "https://choria.io",
   268  				Version: "1.0.0",
   269  			}
   270  
   271  			ginkgoAgent := New(metadata.Name, metadata, am.Choria(), fw.Logger("test"))
   272  			action := func(ctx context.Context, req *Request, reply *Reply, agent *Agent, conn inter.ConnectorInfo) {}
   273  			ginkgoAgent.MustRegisterAction("boop", action)
   274  			Expect(err).ToNot(HaveOccurred())
   275  
   276  			err = am.RegisterAgent(ctx, ginkgoAgent.Name(), ginkgoAgent, conn)
   277  			Expect(err).ToNot(HaveOccurred())
   278  			Expect(ginkgoAgent.ServerInfoSource.Facts()).ToNot(BeNil())
   279  
   280  			authz = &regoPolicy{
   281  				cfg: cfg,
   282  				log: fw.Logger(""),
   283  				req: &Request{
   284  					Agent:    ginkgoAgent.meta.Name,
   285  					Action:   "boop",
   286  					CallerID: "choria=rip.mcollective",
   287  					SenderID: "choria=rip.mcollective",
   288  					Data:     json.RawMessage(`{"bar": "foo"}`), // reversed from above
   289  					TTL:      60,
   290  					Time:     time.Now(),
   291  					Filter:   protocol.NewFilter(),
   292  				},
   293  				agent: ginkgoAgent,
   294  			}
   295  
   296  		})
   297  
   298  		AfterEach(func() {
   299  			ctx.Done()
   300  		})
   301  
   302  		Describe("Basic tests", func() {
   303  			Context("When the user agent or caller is wrong", func() {
   304  				It("Should deny", func() {
   305  					auth, err := authz.authorize()
   306  					Expect(err).ToNot(HaveOccurred())
   307  					Expect(auth).To(BeFalse())
   308  				})
   309  
   310  				It("Default policy should fail", func() {
   311  					authz.agent.meta.Name = "boop"
   312  					auth, err := authz.authorize()
   313  
   314  					Expect(err).ToNot(HaveOccurred())
   315  					Expect(auth).To(BeFalse())
   316  				})
   317  
   318  			})
   319  
   320  			Context("When facts are incorrect", func() {
   321  				It("Should deny", func() {
   322  
   323  					authz.agent.meta.Name = "facts"
   324  					auth, err := authz.authorize()
   325  					Expect(err).ToNot(HaveOccurred())
   326  					Expect(auth).To(BeFalse())
   327  
   328  				})
   329  			})
   330  
   331  			Context("When classes are different but available", func() {
   332  				It("Should fail", func() {
   333  					authz.agent.meta.Name = "classes"
   334  					auth, err := authz.authorize()
   335  
   336  					Expect(err).ToNot(HaveOccurred())
   337  					Expect(auth).To(BeFalse())
   338  				})
   339  			})
   340  		})
   341  
   342  		Describe("Agents", func() {
   343  			Context("If agent does not exist on the server", func() {
   344  				It("Should fail", func() {
   345  					authz.agent.meta.Name = "agent"
   346  					auth, err := authz.authorize()
   347  
   348  					Expect(err).ToNot(HaveOccurred())
   349  					Expect(auth).To(BeFalse())
   350  				})
   351  			})
   352  		})
   353  
   354  		Describe("Request data", func() {
   355  			Context("The request parameters aren't set right", func() {
   356  				It("Should fail", func() {
   357  					authz.agent.meta.Name = "data"
   358  					auth, err := authz.authorize()
   359  
   360  					Expect(err).ToNot(HaveOccurred())
   361  					Expect(auth).To(BeFalse())
   362  				})
   363  			})
   364  		})
   365  	})
   366  
   367  	Describe("Multiple allow statement tests", func() {
   368  		BeforeEach(func() {
   369  			cfg.ClassesFile = "testdata/policies/rego/classes_fail.txt"
   370  			cfg.FactSourceFile = "testdata/policies/rego/facts_fail.json"
   371  			cfg.ConfigFile = "testdata/server.conf"
   372  			cfg.DisableSecurityProviderVerify = true
   373  
   374  			conn = imock.NewMockConnector(mockctl)
   375  			conn.EXPECT().QueueSubscribe(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
   376  			conn.EXPECT().AgentBroadcastTarget(gomock.AssignableToTypeOf("collective"), gomock.AssignableToTypeOf("agent")).DoAndReturn(func(c, a string) string {
   377  				return fmt.Sprintf("broadcast.%s.%s", c, a)
   378  			}).AnyTimes()
   379  
   380  			connInfo = imock.NewMockConnectorInfo(mockctl)
   381  
   382  			facts, err = os.ReadFile(cfg.FactSourceFile)
   383  			Expect(err).ToNot(HaveOccurred())
   384  			Expect(facts).ToNot(BeEmpty())
   385  
   386  			klasses, err := classes.ReadClasses(cfg.ClassesFile)
   387  			Expect(err).ToNot(HaveOccurred())
   388  
   389  			srvInfo = NewMockServerInfoSource(mockctl)
   390  			srvInfo.EXPECT().Classes().Return(klasses).AnyTimes()
   391  			srvInfo.EXPECT().KnownAgents().Return([]string{"ginkgo"}).AnyTimes()
   392  			srvInfo.EXPECT().Facts().DoAndReturn(func() json.RawMessage { return facts }).AnyTimes()
   393  
   394  			ctx = context.Background()
   395  
   396  			am = agents.New(requests, fw, conn, srvInfo, fw.Logger("test"))
   397  
   398  		})
   399  
   400  		AfterEach(func() {
   401  			ctx.Done()
   402  		})
   403  
   404  		Describe("With the agent set to gingko", func() {
   405  			BeforeEach(func() {
   406  				metadata := &agents.Metadata{
   407  					Name:    "ginkgo",
   408  					Author:  "stub@example.com",
   409  					License: "Apache-2.0",
   410  					Timeout: 10,
   411  					URL:     "https://choria.io",
   412  					Version: "1.0.0",
   413  				}
   414  
   415  				ginkgoAgent := New(metadata.Name, metadata, am.Choria(), fw.Logger("test"))
   416  				action := func(ctx context.Context, req *Request, reply *Reply, agent *Agent, conn inter.ConnectorInfo) {}
   417  				ginkgoAgent.MustRegisterAction("boop", action)
   418  				Expect(err).ToNot(HaveOccurred())
   419  
   420  				err = am.RegisterAgent(ctx, ginkgoAgent.Name(), ginkgoAgent, conn)
   421  				Expect(err).ToNot(HaveOccurred())
   422  				Expect(ginkgoAgent.ServerInfoSource.Facts()).ToNot(BeNil())
   423  
   424  				authz = &regoPolicy{
   425  					cfg: cfg,
   426  					log: fw.Logger(""),
   427  					req: &Request{
   428  						Agent:    ginkgoAgent.meta.Name,
   429  						Action:   "boop",
   430  						CallerID: "choria=rip.mcollective",
   431  						SenderID: "choria=rip.mcollective",
   432  						Data:     json.RawMessage(`{"bar": "foo"}`), // reversed from above
   433  						TTL:      60,
   434  						Time:     time.Now(),
   435  						Filter:   protocol.NewFilter(),
   436  					},
   437  					agent: ginkgoAgent,
   438  				}
   439  			})
   440  
   441  			Context("with multiple allow statements", func() {
   442  				It("Should allow", func() {
   443  					authz.agent.meta.Name = "multiple"
   444  					auth, err := authz.authorize()
   445  
   446  					Expect(err).ToNot(HaveOccurred())
   447  					Expect(auth).To(BeTrue())
   448  				})
   449  			})
   450  		})
   451  
   452  		Describe("With the agent set to other", func() {
   453  			BeforeEach(func() {
   454  				metadata := &agents.Metadata{
   455  					Name:    "other",
   456  					Author:  "stub@example.com",
   457  					License: "Apache-2.0",
   458  					Timeout: 10,
   459  					URL:     "https://choria.io",
   460  					Version: "1.0.0",
   461  				}
   462  
   463  				ginkgoAgent := New(metadata.Name, metadata, am.Choria(), fw.Logger("test"))
   464  				action := func(ctx context.Context, req *Request, reply *Reply, agent *Agent, conn inter.ConnectorInfo) {}
   465  				ginkgoAgent.MustRegisterAction("boop", action)
   466  				Expect(err).ToNot(HaveOccurred())
   467  
   468  				err = am.RegisterAgent(ctx, ginkgoAgent.Name(), ginkgoAgent, conn)
   469  				Expect(err).ToNot(HaveOccurred())
   470  				Expect(ginkgoAgent.ServerInfoSource.Facts()).ToNot(BeNil())
   471  
   472  				authz = &regoPolicy{
   473  					cfg: cfg,
   474  					log: fw.Logger(""),
   475  					req: &Request{
   476  						Agent:    ginkgoAgent.meta.Name,
   477  						Action:   "poob",
   478  						CallerID: "choria=rip.mcollective",
   479  						SenderID: "choria=rip.mcollective",
   480  						Data:     json.RawMessage(`{"bar": "foo"}`), // reversed from above
   481  						TTL:      60,
   482  						Time:     time.Now(),
   483  						Filter:   protocol.NewFilter(),
   484  					},
   485  					agent: ginkgoAgent,
   486  				}
   487  			})
   488  
   489  			Context("with multiple allow statements", func() {
   490  				It("Should allow", func() {
   491  					authz.agent.meta.Name = "multiple"
   492  					auth, err := authz.authorize()
   493  
   494  					Expect(err).ToNot(HaveOccurred())
   495  					Expect(auth).To(BeTrue())
   496  				})
   497  			})
   498  		})
   499  
   500  		Describe("With the agent set to somethingelse", func() {
   501  			BeforeEach(func() {
   502  				metadata := &agents.Metadata{
   503  					Name:    "somethingelse",
   504  					Author:  "stub@example.com",
   505  					License: "Apache-2.0",
   506  					Timeout: 10,
   507  					URL:     "https://choria.io",
   508  					Version: "1.0.0",
   509  				}
   510  
   511  				ginkgoAgent := New(metadata.Name, metadata, am.Choria(), fw.Logger("test"))
   512  				action := func(ctx context.Context, req *Request, reply *Reply, agent *Agent, conn inter.ConnectorInfo) {}
   513  				ginkgoAgent.MustRegisterAction("boop", action)
   514  				Expect(err).ToNot(HaveOccurred())
   515  
   516  				err = am.RegisterAgent(ctx, ginkgoAgent.Name(), ginkgoAgent, conn)
   517  				Expect(err).ToNot(HaveOccurred())
   518  				Expect(ginkgoAgent.ServerInfoSource.Facts()).ToNot(BeNil())
   519  
   520  				authz = &regoPolicy{
   521  					cfg: cfg,
   522  					log: fw.Logger(""),
   523  					req: &Request{
   524  						Agent:    ginkgoAgent.meta.Name,
   525  						Action:   "poob",
   526  						CallerID: "choria=rip.mcollective",
   527  						SenderID: "choria=rip.mcollective",
   528  						Data:     json.RawMessage(`{"bar": "foo"}`), // reversed from above
   529  						TTL:      60,
   530  						Time:     time.Now(),
   531  						Filter:   protocol.NewFilter(),
   532  					},
   533  					agent: ginkgoAgent,
   534  				}
   535  			})
   536  
   537  			Context("with multiple allow statements", func() {
   538  				It("Should deny", func() {
   539  					authz.agent.meta.Name = "multiple"
   540  					auth, err := authz.authorize()
   541  
   542  					Expect(err).ToNot(HaveOccurred())
   543  					Expect(auth).To(BeFalse())
   544  				})
   545  			})
   546  		})
   547  	})
   548  })