Skip to content

Commit 50f6d5d

Browse files
committed
feat: add opa allowlisting support
Signed-off-by: Arjun Raja Yogidas <[email protected]>
1 parent 253b7eb commit 50f6d5d

File tree

7 files changed

+366
-33
lines changed

7 files changed

+366
-33
lines changed

api/router/router.go

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package router
55

66
import (
77
"context"
8+
"errors"
89
"fmt"
910
"net/http"
1011
"os"
@@ -15,6 +16,7 @@ import (
1516
"github.com/moby/moby/api/server/httputils"
1617
"github.com/moby/moby/api/types/versions"
1718

19+
"github.com/open-policy-agent/opa/v1/rego"
1820
"github.com/runfinch/finch-daemon/api/handlers/builder"
1921
"github.com/runfinch/finch-daemon/api/handlers/container"
2022
"github.com/runfinch/finch-daemon/api/handlers/distribution"
@@ -30,6 +32,14 @@ import (
3032
"github.com/runfinch/finch-daemon/version"
3133
)
3234

35+
var errRego = errors.New("error in rego policy file")
36+
var errInput = errors.New("error in HTTP request")
37+
38+
type inputRegoRequest struct {
39+
Method string
40+
Path string
41+
}
42+
3343
// Options defines the router options to be passed into the handlers.
3444
type Options struct {
3545
Config *config.Config
@@ -41,16 +51,24 @@ type Options struct {
4151
VolumeService volume.Service
4252
ExecService exec.Service
4353
DistributionService distribution.Service
54+
RegoFilePath string
4455

4556
// NerdctlWrapper wraps the interactions with nerdctl to build
4657
NerdctlWrapper *backend.NerdctlWrapper
4758
}
4859

4960
// New creates a new router and registers the handlers to it. Returns a handler object
5061
// The struct definitions of the HTTP responses come from https://github.com/moby/moby/tree/master/api/types.
51-
func New(opts *Options) http.Handler {
62+
func New(opts *Options) (http.Handler, error) {
5263
r := mux.NewRouter()
5364
r.Use(VersionMiddleware)
65+
if opts.RegoFilePath != "" {
66+
regoMiddleware, err := CreateRegoMiddleware(opts.RegoFilePath)
67+
if err != nil {
68+
return nil, err
69+
}
70+
r.Use(regoMiddleware)
71+
}
5472
vr := types.VersionedRouter{Router: r}
5573

5674
logger := flog.NewLogrus()
@@ -62,7 +80,7 @@ func New(opts *Options) http.Handler {
6280
volume.RegisterHandlers(vr, opts.VolumeService, opts.Config, logger)
6381
exec.RegisterHandlers(vr, opts.ExecService, opts.Config, logger)
6482
distribution.RegisterHandlers(vr, opts.DistributionService, opts.Config, logger)
65-
return ghandlers.LoggingHandler(os.Stderr, r)
83+
return ghandlers.LoggingHandler(os.Stderr, r), nil
6684
}
6785

6886
// VersionMiddleware checks for the requested version of the api and makes sure it falls within the bounds
@@ -90,3 +108,46 @@ func VersionMiddleware(next http.Handler) http.Handler {
90108
next.ServeHTTP(w, newReq)
91109
})
92110
}
111+
112+
// CreateRegoMiddleware dynamically parses the rego file at the path specified in options
113+
// and allows or denies the request based on the policy.
114+
// Will return a nil function and an error if the given file path is blank or invalid.
115+
func CreateRegoMiddleware(regoFilePath string) (func(next http.Handler) http.Handler, error) {
116+
if regoFilePath == "" {
117+
return nil, errRego
118+
}
119+
120+
query := "data.finch.authz.allow"
121+
nr := rego.New(
122+
rego.Load([]string{regoFilePath}, nil),
123+
rego.Query(query),
124+
)
125+
126+
preppedQuery, err := nr.PrepareForEval(context.Background())
127+
if err != nil {
128+
return nil, err
129+
}
130+
131+
return func(next http.Handler) http.Handler {
132+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
133+
input := inputRegoRequest{
134+
Method: r.Method,
135+
Path: r.URL.Path,
136+
}
137+
138+
rs, err := preppedQuery.Eval(r.Context(), rego.EvalInput(input))
139+
if err != nil {
140+
response.SendErrorResponse(w, http.StatusInternalServerError, errInput)
141+
return
142+
}
143+
144+
if !rs.Allowed() {
145+
response.SendErrorResponse(w, http.StatusForbidden,
146+
fmt.Errorf("method %s not allowed for path %s", r.Method, r.URL.Path))
147+
return
148+
}
149+
newReq := r.WithContext(r.Context())
150+
next.ServeHTTP(w, newReq)
151+
})
152+
}, nil
153+
}

api/router/router_test.go

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
"fmt"
99
"net/http"
1010
"net/http/httptest"
11+
"os"
12+
"path/filepath"
1113
"testing"
1214

1315
"github.com/containerd/nerdctl/v2/pkg/config"
@@ -51,8 +53,9 @@ var _ = Describe("version middleware test", func() {
5153
BuilderService: nil,
5254
VolumeService: nil,
5355
NerdctlWrapper: nil,
56+
RegoFilePath: "",
5457
}
55-
h = New(opts)
58+
h, _ = New(opts)
5659
rr = httptest.NewRecorder()
5760
expected = types.VersionInfo{
5861
Platform: struct {
@@ -126,3 +129,69 @@ var _ = Describe("version middleware test", func() {
126129
Expect(v).Should(Equal(expected))
127130
})
128131
})
132+
133+
// Unit tests for the rego handler.
134+
var _ = Describe("rego middleware test", func() {
135+
var (
136+
opts *Options
137+
rr *httptest.ResponseRecorder
138+
expected types.VersionInfo
139+
sysSvc *mocks_system.MockService
140+
regoFilePath string
141+
)
142+
143+
BeforeEach(func() {
144+
mockCtrl := gomock.NewController(GinkgoT())
145+
defer mockCtrl.Finish()
146+
147+
tempDirPath := GinkgoT().TempDir()
148+
regoFilePath = filepath.Join(tempDirPath, "authz.rego")
149+
os.Create(regoFilePath)
150+
151+
c := config.Config{}
152+
sysSvc = mocks_system.NewMockService(mockCtrl)
153+
opts = &Options{
154+
Config: &c,
155+
SystemService: sysSvc,
156+
}
157+
rr = httptest.NewRecorder()
158+
expected = types.VersionInfo{}
159+
sysSvc.EXPECT().GetVersion(gomock.Any()).Return(&expected, nil).AnyTimes()
160+
})
161+
It("should return a 200 error for calls by default", func() {
162+
h, err := New(opts)
163+
Expect(err).Should(BeNil())
164+
165+
req, _ := http.NewRequest(http.MethodGet, "/version", nil)
166+
h.ServeHTTP(rr, req)
167+
168+
Expect(rr).Should(HaveHTTPStatus(http.StatusOK))
169+
})
170+
171+
It("should return a 400 error for disallowed calls", func() {
172+
regoPolicy := `package finch.authz
173+
import rego.v1
174+
175+
default allow = false`
176+
177+
os.WriteFile(regoFilePath, []byte(regoPolicy), 0644)
178+
opts.RegoFilePath = regoFilePath
179+
h, err := New(opts)
180+
Expect(err).Should(BeNil())
181+
182+
req, _ := http.NewRequest(http.MethodGet, "/version", nil)
183+
h.ServeHTTP(rr, req)
184+
185+
Expect(rr).Should(HaveHTTPStatus(http.StatusForbidden))
186+
})
187+
188+
It("should return an error for poorly formed rego files", func() {
189+
regoPolicy := `poorly formed rego file`
190+
191+
os.WriteFile(regoFilePath, []byte(regoPolicy), 0644)
192+
opts.RegoFilePath = regoFilePath
193+
_, err := New(opts)
194+
195+
Expect(err).Should(Not(BeNil()))
196+
})
197+
})

cmd/finch-daemon/main.go

Lines changed: 97 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ type DaemonOptions struct {
4949
debugAddress string
5050
configPath string
5151
pidFile string
52+
regoFilePath string
53+
enableOpa bool
54+
regoFileLock *flock.Flock
5255
}
5356

5457
var options = new(DaemonOptions)
@@ -67,6 +70,8 @@ func main() {
6770
rootCmd.Flags().StringVar(&options.debugAddress, "debug-addr", "", "")
6871
rootCmd.Flags().StringVar(&options.configPath, "config-file", defaultConfigPath, "Daemon Config Path")
6972
rootCmd.Flags().StringVar(&options.pidFile, "pidfile", defaultPidFile, "pid file location")
73+
rootCmd.Flags().StringVar(&options.regoFilePath, "rego-file", "", "Rego Policy Path")
74+
rootCmd.Flags().BoolVar(&options.enableOpa, "enable-opa", false, "turn on opa allowlisting")
7075
if err := rootCmd.Execute(); err != nil {
7176
log.Printf("got error: %v", err)
7277
log.Fatal(err)
@@ -193,6 +198,16 @@ func run(options *DaemonOptions) error {
193198
}
194199
}()
195200

201+
defer func() {
202+
if options.regoFileLock != nil {
203+
if err := options.regoFileLock.Unlock(); err != nil {
204+
logrus.Errorf("failed to unlock Rego file: %v", err)
205+
}
206+
logger.Infof("rego file unlocked")
207+
// todo : chmod to read-write permissions
208+
}
209+
}()
210+
196211
sdNotify(daemon.SdNotifyReady, logger)
197212
serverWg.Wait()
198213
logger.Debugln("Server stopped. Exiting...")
@@ -215,8 +230,20 @@ func newRouter(options *DaemonOptions, logger *flog.Logrus) (http.Handler, error
215230
return nil, err
216231
}
217232

218-
opts := createRouterOptions(conf, clientWrapper, ncWrapper, logger)
219-
return router.New(opts), nil
233+
var regoFilePath string
234+
if options.enableOpa {
235+
regoFilePath, err = sanitizeRegoFile(options)
236+
if err != nil {
237+
return nil, err
238+
}
239+
}
240+
241+
opts := createRouterOptions(conf, clientWrapper, ncWrapper, logger, regoFilePath)
242+
newRouter, err := router.New(opts)
243+
if err != nil {
244+
return nil, err
245+
}
246+
return newRouter, nil
220247
}
221248

222249
func handleSignal(socket string, server *http.Server, logger *flog.Logrus) {
@@ -265,3 +292,71 @@ func defineDockerConfig(uid int) error {
265292
return true
266293
})
267294
}
295+
296+
func checkRegoFileValidity(filePath string) error {
297+
fmt.Println("checking file validity.....")
298+
if _, err := os.Stat(filePath); os.IsNotExist(err) {
299+
return fmt.Errorf("provided Rego file path does not exist: %s", filePath)
300+
}
301+
302+
// Check if the file has a valid extension (.rego, .yaml, or .json)
303+
// validExtensions := []string{".rego", ".yaml", ".yml", ".json"}
304+
fileExt := strings.ToLower(filepath.Ext(options.regoFilePath))
305+
306+
if fileExt != ".rego" {
307+
return fmt.Errorf("invalid file extension for Rego file. Only .rego files are supported")
308+
}
309+
310+
// isValidExtension := false
311+
// for _, ext := range validExtensions {
312+
// if fileExt == ext {
313+
// isValidExtension = true
314+
// break
315+
// }
316+
// }
317+
318+
// if !isValidExtension {
319+
// return fmt.Errorf("Invalid file extension for Rego file. Allowed extensions are: %v", validExtensions)
320+
// }
321+
322+
fmt.Println(" file valid!")
323+
return nil
324+
}
325+
326+
// todo : rename this function to be more descriptve
327+
func sanitizeRegoFile(options *DaemonOptions) (string, error) {
328+
fmt.Println("sanitizeRegoFile called.....")
329+
if options.regoFilePath != "" {
330+
if !options.enableOpa {
331+
return "", fmt.Errorf("rego file path was provided without the --enable-opa flag, please provide the --enable-opa flag") // todo, can we default to setting this flag ourselves is this better UX?
332+
}
333+
334+
if err := checkRegoFileValidity(options.regoFilePath); err != nil {
335+
return "", err
336+
}
337+
}
338+
339+
if options.enableOpa && options.regoFilePath == "" {
340+
return "", fmt.Errorf("rego file path not provided, please provide the policy file path using the --rego-file flag")
341+
}
342+
343+
fileLock := flock.New(options.regoFilePath)
344+
345+
locked, err := fileLock.TryLock()
346+
if err != nil {
347+
return "", fmt.Errorf("error acquiring lock on rego file: %v", err)
348+
}
349+
if !locked {
350+
return "", fmt.Errorf("unable to acquire lock on rego file, it may be in use by another process")
351+
}
352+
353+
// Change file permissions to read-only
354+
err = os.Chmod(options.regoFilePath, 0444) // read-only for all users
355+
if err != nil {
356+
fileLock.Unlock()
357+
return "", fmt.Errorf("error changing rego file permissions: %v", err)
358+
}
359+
options.regoFileLock = fileLock
360+
361+
return options.regoFilePath, nil
362+
}

cmd/finch-daemon/router_utils.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ func createRouterOptions(
9696
clientWrapper *backend.ContainerdClientWrapper,
9797
ncWrapper *backend.NerdctlWrapper,
9898
logger *flog.Logrus,
99+
regoFilePath string,
99100
) *router.Options {
100101
fs := afero.NewOsFs()
101102
tarCreator := archive.NewTarCreator(ecc.NewExecCmdCreator(), logger)
@@ -112,5 +113,6 @@ func createRouterOptions(
112113
ExecService: exec.NewService(clientWrapper, logger),
113114
DistributionService: distribution.NewService(clientWrapper, ncWrapper, logger),
114115
NerdctlWrapper: ncWrapper,
116+
RegoFilePath: regoFilePath,
115117
}
116118
}

0 commit comments

Comments
 (0)