Skip to content

feat: Test opa #88

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,19 @@ jobs:
run: |
sudo ls /etc/cni/net.d
sudo rm /etc/cni/net.d/87-podman-bridge.conflist
- name: Verify Rego file presence
run: ls -l ${{ github.workspace }}/docs/sample-rego-policies/default.rego
- name: Set Rego file path
run: echo "REGO_FILE_PATH=${{ github.workspace }}/docs/sample-rego-policies/default.rego" >> $GITHUB_ENV
- name: Start finch-daemon with opa Authz
run: sudo bin/finch-daemon --debug --enable-middleware --rego-file ${{ github.workspace }}/docs/sample-rego-policies/default.rego --skip-rego-perm-check --socket-owner $UID --socket-addr /run/finch.sock --pidfile /run/finch.pid &
- name: Run opa e2e tests
run: sudo -E make test-e2e-opa
- name: Clean up Daemon socket
run: sudo rm /run/finch.sock && sudo rm /run/finch.pid
- name: Start finch-daemon
run: sudo bin/finch-daemon --debug --socket-owner $UID &
- name: Run e2e test
run: sudo make test-e2e
- name: Clean up Daemon socket
run: sudo rm /var/run/finch.sock && sudo rm /run/finch.pid
9 changes: 9 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,15 @@ test-e2e: linux
TEST_E2E=1 \
$(GINKGO) $(GFLAGS) ./e2e/...

.PHONY: test-e2e-opa
test-e2e-opa: linux
DOCKER_HOST="unix:///run/finch.sock" \
DOCKER_API_VERSION="v1.41" \
MIDDLEWARE_E2E=1 \
TEST_E2E=0 \
DAEMON_ROOT="$(BIN)/finch-daemon" \
$(GINKGO) $(GFLAGS) ./e2e/...

.PHONY: licenses
licenses:
PATH=$(BIN):$(PATH) go-licenses report --template="scripts/third-party-license.tpl" --ignore github.com/runfinch ./... > THIRD_PARTY_LICENSES
Expand Down
74 changes: 72 additions & 2 deletions api/router/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package router

import (
"context"
"errors"
"fmt"
"net/http"
"os"
Expand All @@ -15,6 +16,7 @@ import (
"github.com/moby/moby/api/server/httputils"
"github.com/moby/moby/api/types/versions"

"github.com/open-policy-agent/opa/v1/rego"
"github.com/runfinch/finch-daemon/api/handlers/builder"
"github.com/runfinch/finch-daemon/api/handlers/container"
"github.com/runfinch/finch-daemon/api/handlers/distribution"
Expand All @@ -30,6 +32,14 @@ import (
"github.com/runfinch/finch-daemon/version"
)

var errRego = errors.New("error in rego policy file")
var errInput = errors.New("error in HTTP request")

type inputRegoRequest struct {
Method string
Path string
}

// Options defines the router options to be passed into the handlers.
type Options struct {
Config *config.Config
Expand All @@ -41,16 +51,24 @@ type Options struct {
VolumeService volume.Service
ExecService exec.Service
DistributionService distribution.Service
RegoFilePath string

// NerdctlWrapper wraps the interactions with nerdctl to build
NerdctlWrapper *backend.NerdctlWrapper
}

// New creates a new router and registers the handlers to it. Returns a handler object
// The struct definitions of the HTTP responses come from https://github.com/moby/moby/tree/master/api/types.
func New(opts *Options) http.Handler {
func New(opts *Options) (http.Handler, error) {
r := mux.NewRouter()
r.Use(VersionMiddleware)
if opts.RegoFilePath != "" {
regoMiddleware, err := CreateRegoMiddleware(opts.RegoFilePath)
if err != nil {
return nil, err
}
r.Use(regoMiddleware)
}
vr := types.VersionedRouter{Router: r}

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

// VersionMiddleware checks for the requested version of the api and makes sure it falls within the bounds
Expand Down Expand Up @@ -90,3 +108,55 @@ func VersionMiddleware(next http.Handler) http.Handler {
next.ServeHTTP(w, newReq)
})
}

// CreateRegoMiddleware dynamically parses the rego file at the path specified in options
// and allows or denies the request based on the policy.
// Will return a nil function and an error if the given file path is blank or invalid.
func CreateRegoMiddleware(regoFilePath string) (func(next http.Handler) http.Handler, error) {
if regoFilePath == "" {
return nil, errRego
}

query := "data.finch.authz.allow"
nr := rego.New(
rego.Load([]string{regoFilePath}, nil),
rego.Query(query),
)

preppedQuery, err := nr.PrepareForEval(context.Background())
if err != nil {
return nil, err
}

return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
input := inputRegoRequest{
Method: r.Method,
Path: r.URL.Path,
}

fmt.Printf("[OPA Debug] Input being evaluated: Method=%s, Path=%s\n", input.Method, input.Path)
fmt.Printf("[OPA Debug] Query being executed: %s\n", query)

rs, err := preppedQuery.Eval(r.Context(), rego.EvalInput(input))
if err != nil {
fmt.Printf("[OPA Error] Policy evaluation failed: %v\n", err)
response.SendErrorResponse(w, http.StatusInternalServerError, errInput)
return
}

fmt.Printf("[OPA Debug] Evaluation results: %+v\n", rs)
fmt.Printf("[OPA Debug] Number of results: %d\n", len(rs))

if !rs.Allowed() {
fmt.Printf("[OPA Denied] Request denied: Method=%s, Path=%s\n", r.Method, r.URL.Path)
response.SendErrorResponse(w, http.StatusForbidden,
fmt.Errorf("method %s not allowed for path %s", r.Method, r.URL.Path))
return
}
fmt.Printf("[OPA Allowed] Request allowed: Method=%s, Path=%s\n", r.Method, r.URL.Path)
newReq := r.WithContext(r.Context())
next.ServeHTTP(w, newReq)
})
}, nil
}
71 changes: 70 additions & 1 deletion api/router/router_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"fmt"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"

"github.com/containerd/nerdctl/v2/pkg/config"
Expand Down Expand Up @@ -51,8 +53,9 @@ var _ = Describe("version middleware test", func() {
BuilderService: nil,
VolumeService: nil,
NerdctlWrapper: nil,
RegoFilePath: "",
}
h = New(opts)
h, _ = New(opts)
rr = httptest.NewRecorder()
expected = types.VersionInfo{
Platform: struct {
Expand Down Expand Up @@ -126,3 +129,69 @@ var _ = Describe("version middleware test", func() {
Expect(v).Should(Equal(expected))
})
})

// Unit tests for the rego handler.
var _ = Describe("rego middleware test", func() {
var (
opts *Options
rr *httptest.ResponseRecorder
expected types.VersionInfo
sysSvc *mocks_system.MockService
regoFilePath string
)

BeforeEach(func() {
mockCtrl := gomock.NewController(GinkgoT())
defer mockCtrl.Finish()

tempDirPath := GinkgoT().TempDir()
regoFilePath = filepath.Join(tempDirPath, "authz.rego")
os.Create(regoFilePath)

c := config.Config{}
sysSvc = mocks_system.NewMockService(mockCtrl)
opts = &Options{
Config: &c,
SystemService: sysSvc,
}
rr = httptest.NewRecorder()
expected = types.VersionInfo{}
sysSvc.EXPECT().GetVersion(gomock.Any()).Return(&expected, nil).AnyTimes()
})
It("should return a 200 error for calls by default", func() {
h, err := New(opts)
Expect(err).Should(BeNil())

req, _ := http.NewRequest(http.MethodGet, "/version", nil)
h.ServeHTTP(rr, req)

Expect(rr).Should(HaveHTTPStatus(http.StatusOK))
})

It("should return a 400 error for disallowed calls", func() {
regoPolicy := `package finch.authz
import rego.v1

default allow = false`

os.WriteFile(regoFilePath, []byte(regoPolicy), 0644)
opts.RegoFilePath = regoFilePath
h, err := New(opts)
Expect(err).Should(BeNil())

req, _ := http.NewRequest(http.MethodGet, "/version", nil)
h.ServeHTTP(rr, req)

Expect(rr).Should(HaveHTTPStatus(http.StatusForbidden))
})

It("should return an error for poorly formed rego files", func() {
regoPolicy := `poorly formed rego file`

os.WriteFile(regoFilePath, []byte(regoPolicy), 0644)
opts.RegoFilePath = regoFilePath
_, err := New(opts)

Expect(err).Should(Not(BeNil()))
})
})
42 changes: 34 additions & 8 deletions cmd/finch-daemon/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,16 @@ const (
)

type DaemonOptions struct {
debug bool
socketAddr string
socketOwner int
debugAddress string
configPath string
pidFile string
debug bool
socketAddr string
socketOwner int
debugAddress string
configPath string
pidFile string
regoFilePath string
enableMiddleware bool
skipRegoPermCheck bool
regoFileLock *flock.Flock
}

var options = new(DaemonOptions)
Expand All @@ -67,6 +71,10 @@ func main() {
rootCmd.Flags().StringVar(&options.debugAddress, "debug-addr", "", "")
rootCmd.Flags().StringVar(&options.configPath, "config-file", defaultConfigPath, "Daemon Config Path")
rootCmd.Flags().StringVar(&options.pidFile, "pidfile", defaultPidFile, "pid file location")
rootCmd.Flags().StringVar(&options.regoFilePath, "rego-file", "", "Rego Policy Path")
rootCmd.Flags().BoolVar(&options.enableMiddleware, "enable-middleware", false, "turn on middleware for allowlisting")
rootCmd.Flags().BoolVar(&options.skipRegoPermCheck, "skip-rego-perm-check", false, "skip the rego file permission check (allows permissions more permissive than 0600)")

if err := rootCmd.Execute(); err != nil {
log.Printf("got error: %v", err)
log.Fatal(err)
Expand Down Expand Up @@ -144,6 +152,10 @@ func run(options *DaemonOptions) error {
logger := flog.NewLogrus()
r, err := newRouter(options, logger)
if err != nil {
// call regoFile cleanup function here to unlock previously locked file
if options.regoFilePath != "" {
cleanupRegoFile(options, logger)
}
return fmt.Errorf("failed to create a router: %w", err)
}

Expand Down Expand Up @@ -193,6 +205,8 @@ func run(options *DaemonOptions) error {
}
}()

defer cleanupRegoFile(options, logger)

sdNotify(daemon.SdNotifyReady, logger)
serverWg.Wait()
logger.Debugln("Server stopped. Exiting...")
Expand All @@ -215,8 +229,20 @@ func newRouter(options *DaemonOptions, logger *flog.Logrus) (http.Handler, error
return nil, err
}

opts := createRouterOptions(conf, clientWrapper, ncWrapper, logger)
return router.New(opts), nil
var regoFilePath string
if options.enableMiddleware {
regoFilePath, err = sanitizeRegoFile(options, logger)
if err != nil {
return nil, err
}
}

opts := createRouterOptions(conf, clientWrapper, ncWrapper, logger, regoFilePath)
newRouter, err := router.New(opts)
if err != nil {
return nil, err
}
return newRouter, nil
}

func handleSignal(socket string, server *http.Server, logger *flog.Logrus) {
Expand Down
Loading
Loading