Skip to content

Commit

Permalink
Static assets endpoint (#5063)
Browse files Browse the repository at this point in the history
* static assets endpoint

* static assets endpoint - cache

* static assets endpoint - thread safety

* static assets endpoint - admin repo

* static assets endpoint - admin repo

* static assets endpoint - admin repo

* static assets endpoint - admin repo

* static assets endpoint - admin repo

* static assets endpoint - admin repo

* static assets endpoint - admin repo

* static assets endpoint - admin repo

* static assets endpoint - admin repo

* static assets endpoint - admin repo

* static assets endpoint - removing cache

* static assets endpoint - removing cache

* static assets endpoint - removing cache

* static assets endpoint - removing cache

* static assets endpoint - migration

* static assets endpoint - migration

* static assets: obscure root dir

* static assets: obscure root dir

* static assets: obscure root dir

* static assets: obscure root dir

---------

Co-authored-by: Egor Ryashin <[email protected]>
  • Loading branch information
egor-ryashin and Egor Ryashin committed Jun 28, 2024
1 parent c993f23 commit 349403e
Show file tree
Hide file tree
Showing 10 changed files with 129 additions and 9 deletions.
8 changes: 8 additions & 0 deletions runtime/compilers/rillv1/parse_rillyaml.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type RillYAML struct {
Variables []*VariableDef
Defaults map[ResourceKind]yaml.Node
FeatureFlags map[string]bool
PublicPaths []string
}

// ConnectorDef is a subtype of RillYAML, defining connectors required by the project
Expand Down Expand Up @@ -68,6 +69,8 @@ type rillYAML struct {
Migrations yaml.Node `yaml:"migrations"`
// Feature flags (preferably a map[string]bool, but can also be a []string for backwards compatibility)
Features yaml.Node `yaml:"features"`
// Paths to expose over HTTP (defaults to ./public)
PublicPaths []string `yaml:"public_paths"`
}

// parseRillYAML parses rill.yaml
Expand Down Expand Up @@ -161,6 +164,10 @@ func (p *Parser) parseRillYAML(ctx context.Context, path string) error {
}
}

if len(tmp.PublicPaths) == 0 {
tmp.PublicPaths = []string{"public"}
}

res := &RillYAML{
Title: tmp.Title,
Description: tmp.Description,
Expand All @@ -174,6 +181,7 @@ func (p *Parser) parseRillYAML(ctx context.Context, path string) error {
ResourceKindMigration: tmp.Migrations,
},
FeatureFlags: featureFlags,
PublicPaths: tmp.PublicPaths,
}

for i, c := range tmp.Connectors {
Expand Down
1 change: 1 addition & 0 deletions runtime/drivers/admin/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ var _ drivers.Handle = &Handle{}
// a smaller subset of relevant parts of rill.yaml
type rillYAML struct {
IgnorePaths []string `yaml:"ignore_paths"`
PublicPaths []string `yaml:"public_paths"`
}

// Driver implements drivers.Handle.
Expand Down
8 changes: 6 additions & 2 deletions runtime/drivers/admin/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,14 @@ func (h *Handle) Get(ctx context.Context, filePath string) (string, error) {
}
defer h.repoMu.RUnlock()

filePath = filepath.Join(h.projPath, filePath)
fp := filepath.Join(h.projPath, filePath)

b, err := os.ReadFile(filePath)
b, err := os.ReadFile(fp)
if err != nil {
// obscure the root directory location
if t, ok := err.(*fs.PathError); ok { // nolint:errorlint // we specifically check for a non-wrapped error
return "", fmt.Errorf("%s %s %s", t.Op, filePath, t.Err.Error())
}
return "", err
}

Expand Down
8 changes: 6 additions & 2 deletions runtime/drivers/file/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,14 @@ func (c *connection) ListRecursive(ctx context.Context, glob string, skipDirs bo

// Get implements drivers.RepoStore.
func (c *connection) Get(ctx context.Context, filePath string) (string, error) {
filePath = filepath.Join(c.root, filePath)
fp := filepath.Join(c.root, filePath)

b, err := os.ReadFile(filePath)
b, err := os.ReadFile(fp)
if err != nil {
// obscure the root directory location
if t, ok := err.(*fs.PathError); ok { // nolint:errorlint // we specifically check for a non-wrapped error
return "", fmt.Errorf("%s %s %s", t.Op, filePath, t.Err.Error())
}
return "", err
}

Expand Down
2 changes: 2 additions & 0 deletions runtime/drivers/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ type Instance struct {
EmbedCatalog bool `db:"embed_catalog"`
// WatchRepo indicates whether to watch the repo for file changes and reconcile them automatically.
WatchRepo bool `db:"watch_repo"`
// Paths to expose over HTTP (defaults to ./public)
PublicPaths []string `db:"public_paths"`
// IgnoreInitialInvalidProjectError indicates whether to ignore an invalid project error when the instance is initially created.
IgnoreInitialInvalidProjectError bool `db:"-"`
}
Expand Down
1 change: 1 addition & 0 deletions runtime/drivers/sqlite/migrations/0023.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE instances ADD COLUMN public_paths TEXT NOT NULL DEFAULT '[]';
44 changes: 39 additions & 5 deletions runtime/drivers/sqlite/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ func (c *connection) findInstances(_ context.Context, whereClause string, args .
feature_flags,
annotations,
embed_catalog,
watch_repo
watch_repo,
public_paths
FROM instances %s ORDER BY id
`, whereClause)

Expand All @@ -64,7 +65,7 @@ func (c *connection) findInstances(_ context.Context, whereClause string, args .
var res []*drivers.Instance
for rows.Next() {
// sqlite doesn't support maps need to read as bytes and convert to map
var variables, projectVariables, featureFlags, annotations, connectors, projectConnectors []byte
var variables, projectVariables, featureFlags, annotations, connectors, projectConnectors, publicPaths []byte
i := &drivers.Instance{}
err := rows.Scan(
&i.ID,
Expand All @@ -85,6 +86,7 @@ func (c *connection) findInstances(_ context.Context, whereClause string, args .
&annotations,
&i.EmbedCatalog,
&i.WatchRepo,
&publicPaths,
)
if err != nil {
return nil, err
Expand Down Expand Up @@ -120,6 +122,11 @@ func (c *connection) findInstances(_ context.Context, whereClause string, args .
return nil, err
}

i.PublicPaths, err = arrayFromJSON[string](publicPaths)
if err != nil {
return nil, err
}

res = append(res, i)
}

Expand Down Expand Up @@ -166,6 +173,11 @@ func (c *connection) CreateInstance(_ context.Context, inst *drivers.Instance) e
return err
}

publicPaths, err := arrayToJSON(inst.PublicPaths)
if err != nil {
return err
}

now := time.Now()
_, err = c.db.ExecContext(
ctx,
Expand All @@ -188,9 +200,10 @@ func (c *connection) CreateInstance(_ context.Context, inst *drivers.Instance) e
feature_flags,
annotations,
embed_catalog,
watch_repo
watch_repo,
public_paths
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)
`,
inst.ID,
inst.Environment,
Expand All @@ -210,6 +223,7 @@ func (c *connection) CreateInstance(_ context.Context, inst *drivers.Instance) e
annotations,
inst.EmbedCatalog,
inst.WatchRepo,
publicPaths,
)
if err != nil {
return err
Expand Down Expand Up @@ -257,6 +271,11 @@ func (c *connection) EditInstance(_ context.Context, inst *drivers.Instance) err
return err
}

publicPaths, err := arrayToJSON(inst.PublicPaths)
if err != nil {
return err
}

now := time.Now()
_, err = c.db.ExecContext(
ctx,
Expand All @@ -277,7 +296,8 @@ func (c *connection) EditInstance(_ context.Context, inst *drivers.Instance) err
feature_flags = $14,
annotations = $15,
embed_catalog = $16,
watch_repo = $17
watch_repo = $17,
public_paths = $18
WHERE id = $1
`,
inst.ID,
Expand All @@ -297,6 +317,7 @@ func (c *connection) EditInstance(_ context.Context, inst *drivers.Instance) err
annotations,
inst.EmbedCatalog,
inst.WatchRepo,
publicPaths,
)
if err != nil {
return err
Expand Down Expand Up @@ -329,6 +350,19 @@ func mapFromJSON[T any](data []byte) (map[string]T, error) {
return m, err
}

func arrayToJSON[T any](data []T) ([]byte, error) {
return json.Marshal(data)
}

func arrayFromJSON[T any](data []byte) ([]T, error) {
if len(data) == 0 {
return []T{}, nil
}
var a []T
err := json.Unmarshal(data, &a)
return a, err
}

func unmarshalConnectors(s []byte) ([]*runtimev1.Connector, error) {
if len(s) == 0 {
return make([]*runtimev1.Connector, 0), nil
Expand Down
1 change: 1 addition & 0 deletions runtime/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ func (r *Runtime) UpdateInstanceWithRillYAML(ctx context.Context, instanceID str
}
inst.ProjectVariables = vars
inst.FeatureFlags = rillYAML.FeatureFlags
inst.PublicPaths = rillYAML.PublicPaths
return r.EditInstance(ctx, inst, restartController)
}

Expand Down
63 changes: 63 additions & 0 deletions runtime/server/assets.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package server

import (
"fmt"
"net/http"
"os"
"path/filepath"

"github.com/rilldata/rill/runtime/pkg/httputil"
"github.com/rilldata/rill/runtime/pkg/observability"
"github.com/rilldata/rill/runtime/server/auth"
"go.opentelemetry.io/otel/attribute"
)

func (s *Server) assetsHandler(w http.ResponseWriter, req *http.Request) error {
ctx := req.Context()
instanceID := req.PathValue("instance_id")
path := req.PathValue("path")

observability.AddRequestAttributes(ctx,
attribute.String("args.instance_id", instanceID),
attribute.String("args.path", path),
)

if !auth.GetClaims(req.Context()).CanInstance(instanceID, auth.ReadObjects) {
return httputil.Errorf(http.StatusForbidden, "does not have access to assets")
}

inst, err := s.runtime.Instance(ctx, instanceID)
if err != nil {
return err
}

allowed := false
for _, p := range inst.PublicPaths {
// 'p' can be `/public`, `/public/`, `public/`, `public` (with os-based separators)
// match pattern `public/*` or `/public/*`
ok, err := filepath.Match(fmt.Sprintf("%s%c*", filepath.Clean(p), os.PathSeparator), path)
if err != nil {
return httputil.Error(http.StatusBadRequest, err)
}
if ok {
allowed = true
break
}
}
if !allowed {
return httputil.Error(http.StatusForbidden, fmt.Errorf("path is not allowed"))
}

repo, release, err := s.runtime.Repo(ctx, instanceID)
if err != nil {
return err
}
defer release()

str, err := repo.Get(ctx, path)
if err != nil {
return err
}
_, err = w.Write([]byte(str))
return err
}
2 changes: 2 additions & 0 deletions runtime/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,8 @@ func (s *Server) HTTPHandler(ctx context.Context, registerAdditionalHandlers fun

// Add handler for resolving component data
observability.MuxHandle(httpMux, "/v1/instances/{instance_id}/components/{name}/data", observability.Middleware("runtime", s.logger, auth.HTTPMiddleware(s.aud, httputil.Handler(s.componentDataHandler))))
// Serving static assets
observability.MuxHandle(httpMux, "/v1/instances/{instance_id}/assets/{path...}", observability.Middleware("runtime", s.logger, auth.HTTPMiddleware(s.aud, httputil.Handler(s.assetsHandler))))

// Add Prometheus
if s.opts.ServePrometheus {
Expand Down

0 comments on commit 349403e

Please sign in to comment.