Skip to content
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

feat: add rego + YAML parsing capabilities #32

Closed
wants to merge 3 commits into from
Closed
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
68 changes: 66 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 @@ -14,6 +15,7 @@ import (
"github.com/gorilla/mux"
"github.com/moby/moby/api/server/httputils"
"github.com/moby/moby/api/types/versions"
"github.com/open-policy-agent/opa/rego"
Shubhranshu153 marked this conversation as resolved.
Show resolved Hide resolved

"github.com/runfinch/finch-daemon/api/handlers/builder"
"github.com/runfinch/finch-daemon/api/handlers/container"
Expand All @@ -29,6 +31,9 @@ import (
"github.com/runfinch/finch-daemon/version"
)

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

// Options defines the router options to be passed into the handlers.
type Options struct {
Config *config.Config
Expand All @@ -40,15 +45,31 @@ type Options struct {
VolumeService volume.Service
ExecService exec.Service

// PolicyRegoFilePath points to a rego file used to parse allowlist/denylist policies
// Ignored if empty
RegoFilePath string

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

type inputRegoRequest struct {
Method string
Path string
}

// 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 @@ -59,7 +80,7 @@ func New(opts *Options) http.Handler {
builder.RegisterHandlers(vr, opts.BuilderService, opts.Config, logger, opts.NerdctlWrapper)
volume.RegisterHandlers(vr, opts.VolumeService, opts.Config, logger)
exec.RegisterHandlers(vr, opts.ExecService, 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 @@ -87,3 +108,46 @@ 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.docker.authz.allow"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe s/docker/finch/ ?

Copy link
Author

@sondavidb sondavidb Sep 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could make this configurable in a followup PR if that's ok? I'd like to push this out for now if that's ok

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it a big change that deserves a separate PR?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually why can't we just hard code it to finch?

newRego := rego.New(
rego.Load([]string{regoFilePath}, nil),
rego.Query(query),
)

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

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,
}

rs, err := preppedQuery.Eval(r.Context(), rego.EvalInput(input))
if err != nil {
response.SendErrorResponse(w, http.StatusInternalServerError, errInput)
return
}

if !rs.Allowed() {
response.SendErrorResponse(w, http.StatusBadRequest,
fmt.Errorf("method %s not allowed for path %s", r.Method, r.URL.Path))
return
}
newReq := r.WithContext(r.Context())
next.ServeHTTP(w, newReq)
})
}, nil
}
69 changes: 68 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/pkg/config"
Expand Down Expand Up @@ -52,7 +54,7 @@ var _ = Describe("version middleware test", func() {
VolumeService: nil,
NerdctlWrapper: nil,
}
h = New(opts)
h, _ = New(opts)
rr = httptest.NewRecorder()
expected = types.VersionInfo{
Platform: struct {
Expand Down Expand Up @@ -126,3 +128,68 @@ 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 docker.authz

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.StatusBadRequest))
})

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(Equal(errRego))
})
})
34 changes: 26 additions & 8 deletions cmd/finch-daemon/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import (
"github.com/runfinch/finch-daemon/internal/service/system"
"github.com/runfinch/finch-daemon/internal/service/volume"
"github.com/runfinch/finch-daemon/pkg/archive"
daemonConfig "github.com/runfinch/finch-daemon/pkg/config"
"github.com/runfinch/finch-daemon/pkg/ecc"
"github.com/runfinch/finch-daemon/pkg/flog"
)
Expand All @@ -45,9 +46,11 @@ const (
)

type DaemonOptions struct {
debug bool
socketAddr string
socketOwner int
debug bool
socketAddr string
socketOwner int
regoFilePath string
configPath string
}

var options = new(DaemonOptions)
Expand All @@ -66,6 +69,8 @@ func main() {
" For macOS, the socket has to be owned by the lima user to make port forwarding work"+
" (more info: https://github.com/lima-vm/lima/blob/5a9bca3d09481ed7109b14f8d3f0074816731f43/examples/default.yaml#L340)."+
" -1 means no-op.")
rootCmd.Flags().StringVar(&options.regoFilePath, "rego-path", "", "Optional path to a rego policy. Currently only allowlist/denylist options are available")
rootCmd.Flags().StringVar(&options.configPath, "config", "", "Optional path to a settings YAML file (finch-daemon.yaml)")
if err := rootCmd.Execute(); err != nil {
log.Printf("got error: %v", err)
log.Fatal(err)
Expand All @@ -77,13 +82,26 @@ func runAdapter(cmd *cobra.Command, _ []string) error {
}

func run(options *DaemonOptions) error {
fs := afero.NewOsFs()

if options.configPath != "" {
pendo324 marked this conversation as resolved.
Show resolved Hide resolved
cfg, err := daemonConfig.Load(options.configPath, fs)
if err != nil {
return fmt.Errorf("could not read from %v: %v", options.configPath, err)
}

if options.regoFilePath == "" {
options.regoFilePath = cfg.RegoFilePath
}
}

// This sets the log level of the dependencies that use logrus (e.g., containerd library).
if options.debug {
logrus.SetLevel(logrus.DebugLevel)
}

logger := flog.NewLogrus()
r, err := newRouter(options.debug, logger)
r, err := newRouter(options, logger, fs)
if err != nil {
return fmt.Errorf("failed to create a router: %w", err)
}
Expand Down Expand Up @@ -122,9 +140,9 @@ func run(options *DaemonOptions) error {
return nil
}

func newRouter(debug bool, logger *flog.Logrus) (http.Handler, error) {
func newRouter(options *DaemonOptions, logger *flog.Logrus, fs afero.Fs) (http.Handler, error) {
conf := config.New()
conf.Debug = debug
conf.Debug = options.debug
conf.Namespace = defaultNamespace
client, err := containerd.New(conf.Address, containerd.WithDefaultNamespace(conf.Namespace))
if err != nil {
Expand All @@ -138,7 +156,6 @@ func newRouter(debug bool, logger *flog.Logrus) (http.Handler, error) {
if _, err = ncWrapper.GetNerdctlExe(); err != nil {
return nil, fmt.Errorf("failed to find nerdctl binary: %w", err)
}
fs := afero.NewOsFs()
execCmdCreator := ecc.NewExecCmdCreator()
tarCreator := archive.NewTarCreator(execCmdCreator, logger)
tarExtractor := archive.NewTarExtractor(execCmdCreator, logger)
Expand All @@ -151,9 +168,10 @@ func newRouter(debug bool, logger *flog.Logrus) (http.Handler, error) {
BuilderService: builder.NewService(clientWrapper, ncWrapper, logger, tarExtractor),
VolumeService: volume.NewService(ncWrapper, logger),
ExecService: exec.NewService(clientWrapper, logger),
RegoFilePath: options.regoFilePath,
NerdctlWrapper: ncWrapper,
}
return router.New(opts), nil
return router.New(opts)
}

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