[full-ci] enhancement: allow ocis to provide custom web applications (#8523)

* enhancement: allow ocis to provide custom web applications

* enhancement: add an option to disable web apps

* test: add default logger tests

* test: add app loading tests

* test: add asset server tests

* enhancement: make use of dedicated app conf file and app asset paths

* enhancement: adjust asset locations and deprecate WEB_ASSET_PATH

* enhancement: get rid of default logger and use the service level logger instead

* Apply suggestions from code review

Co-authored-by: Benedikt Kulmann <benedikt@kulmann.biz>
Co-authored-by: kobergj <juliankoberg@googlemail.com>

* enhancement: use basename as app id

* Apply suggestions from code review

Co-authored-by: Martin <github@diemattels.at>

* enhancement: use afero as fs abstraction

* enhancement: simplify logo upload

* enhancement: make use of introductionVersion field annotations

---------

Co-authored-by: Benedikt Kulmann <benedikt@kulmann.biz>
Co-authored-by: kobergj <juliankoberg@googlemail.com>
Co-authored-by: Martin <github@diemattels.at>
This commit is contained in:
Florian Schade
2024-03-05 14:11:18 +01:00
committed by GitHub
co-authored by Benedikt Kulmann kobergj Martin
parent 6ba9e4adf7
commit 6814c61506
63 changed files with 5835 additions and 130 deletions
-63
View File
@@ -1,63 +0,0 @@
package assetsfs
import (
"fmt"
"io/fs"
"net/http"
"os"
"path/filepath"
"github.com/owncloud/ocis/v2/ocis-pkg/log"
)
// FileSystem customized to load assets
type FileSystem struct {
fs http.FileSystem
assetPath string
log log.Logger
}
// Open checks if assetPath is set and tries to load from there. Falls back to fs if that is not possible
func (f *FileSystem) Open(original string) (http.File, error) {
if f.assetPath != "" {
file, err := os.Open(filepath.Join(f.assetPath, original))
if err == nil {
return file, nil
}
}
return f.fs.Open(original)
}
func (f *FileSystem) OpenEmbedded(name string) (http.File, error) {
return f.fs.Open(name)
}
// Create creates a new file in the assetPath
func (f *FileSystem) Create(name string) (*os.File, error) {
fullPath := f.jailPath(name)
if err := os.MkdirAll(filepath.Dir(fullPath), 0770); err != nil {
return nil, err
}
return os.Create(fullPath)
}
// jailPath returns the fullPath `<assetPath>/<name>`. It makes sure that the path is
// always under `<assetPath>` to prevent directory traversal.
func (f *FileSystem) jailPath(name string) string {
return filepath.Join(f.assetPath, filepath.Join("/", name))
}
// New initializes a new FileSystem. Quits on error
func New(embedFS fs.FS, assetPath string, logger log.Logger) *FileSystem {
f, err := fs.Sub(embedFS, "assets")
if err != nil {
fmt.Println("Cannot load subtree fs:", err.Error())
os.Exit(1)
}
return &FileSystem{
fs: http.FS(f),
assetPath: assetPath,
log: logger,
}
}
+2 -2
View File
@@ -5,6 +5,7 @@ import (
gofig "github.com/gookit/config/v2"
gooyaml "github.com/gookit/config/v2/yaml"
"github.com/owncloud/ocis/v2/ocis-pkg/config/defaults"
)
@@ -25,8 +26,7 @@ func BindSourcesToStructs(service string, dst interface{}) (*gofig.Config, error
})
cnf.AddDriver(gooyaml.Driver)
cfgFile := path.Join(defaults.BaseConfigPath(), service+".yaml")
_ = cnf.LoadFiles([]string{cfgFile}...)
_ = cnf.LoadFiles(path.Join(defaults.BaseConfigPath(), service+".yaml"))
err := cnf.BindStruct("", &dst)
if err != nil {
+9 -1
View File
@@ -10,10 +10,11 @@ import (
chimiddleware "github.com/go-chi/chi/v5/middleware"
mzlog "github.com/go-micro/plugins/v4/logger/zerolog"
"github.com/owncloud/ocis/v2/ocis-pkg/shared"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"go-micro.dev/v4/logger"
"github.com/owncloud/ocis/v2/ocis-pkg/shared"
)
var (
@@ -148,3 +149,10 @@ func (l Logger) SubloggerWithRequestID(c context.Context) Logger {
l.With().Str(RequestIDString, chimiddleware.GetReqID(c)).Logger(),
}
}
// Deprecation logs a deprecation message,
// it is used to inform the user that a certain feature is deprecated and will be removed in the future.
// Do not use a logger here because the message MUST be visible independent of the log level.
func Deprecation(a ...any) {
fmt.Printf("\033[1;31mDEPRECATION: %s\033[0m\n", a...)
}
+24
View File
@@ -0,0 +1,24 @@
package log_test
import (
"testing"
"github.com/onsi/gomega"
"github.com/owncloud/ocis/v2/internal/testenv"
"github.com/owncloud/ocis/v2/ocis-pkg/log"
)
func TestDeprecation(t *testing.T) {
cmdTest := testenv.NewCMDTest(t.Name())
if cmdTest.ShouldRun() {
log.Deprecation("this is a deprecation")
return
}
out, err := cmdTest.Run()
g := gomega.NewWithT(t)
g.Expect(err).ToNot(gomega.HaveOccurred())
g.Expect(string(out)).To(gomega.HavePrefix("\033[1;31mDEPRECATION: this is a deprecation"))
}
+37
View File
@@ -0,0 +1,37 @@
package fsx
import (
"github.com/spf13/afero"
)
var (
// assert interfaces implemented
_ afero.Fs = (*FallbackFS)(nil)
_ FS = (*FallbackFS)(nil)
)
// FallbackFS is a filesystem that layers a primary filesystem on top of a secondary filesystem.
type FallbackFS struct {
FS
primary *BaseFS
secondary *BaseFS
}
// Primary returns the primary filesystem.
func (d *FallbackFS) Primary() *BaseFS {
return d.primary
}
// Secondary returns the secondary filesystem.
func (d *FallbackFS) Secondary() *BaseFS {
return d.secondary
}
// NewFallbackFS returns a new FallbackFS instance.
func NewFallbackFS(primary, secondary FS) *FallbackFS {
return &FallbackFS{
FS: FromAfero(afero.NewCopyOnWriteFs(secondary, primary)),
primary: &BaseFS{Fs: primary},
secondary: &BaseFS{Fs: secondary},
}
}
+83
View File
@@ -0,0 +1,83 @@
package fsx_test
import (
"io"
"testing"
"github.com/onsi/gomega"
"github.com/spf13/afero"
"github.com/owncloud/ocis/v2/ocis-pkg/x/io/fsx"
)
func TestLayeredFS(t *testing.T) {
g := gomega.NewWithT(t)
read := func(fs fsx.FS, name string) (string, error) {
f, err := fs.Open(name)
if err != nil {
return "", err
}
b, err := io.ReadAll(f)
if err != nil {
return "", err
}
return string(b), nil
}
mustRead := func(fs fsx.FS, name string) string {
s, err := read(fs, name)
g.Expect(err).ToNot(gomega.HaveOccurred())
return s
}
create := func(fs fsx.FS, name, content string) {
err := afero.WriteFile(fs, name, []byte(content), 0644)
g.Expect(err).ToNot(gomega.HaveOccurred())
}
primary := fsx.NewMemMapFs()
create(primary, "both.txt", "primary")
g.Expect(mustRead(primary, "both.txt")).To(gomega.Equal("primary"))
create(primary, "primary.txt", "primary")
g.Expect(mustRead(primary, "primary.txt")).To(gomega.Equal("primary"))
secondary := fsx.NewMemMapFs()
create(secondary, "both.txt", "secondary")
g.Expect(mustRead(secondary, "both.txt")).To(gomega.Equal("secondary"))
create(secondary, "secondary.txt", "secondary")
g.Expect(mustRead(secondary, "secondary.txt")).To(gomega.Equal("secondary"))
fs := fsx.NewFallbackFS(primary, fsx.NewReadOnlyFs(secondary))
g.Expect(mustRead(fs, "both.txt")).To(gomega.Equal("primary"))
g.Expect(mustRead(fs, "primary.txt")).To(gomega.Equal("primary"))
g.Expect(mustRead(fs, "secondary.txt")).To(gomega.Equal("secondary"))
create(fs, "fallback-fs.txt", "fallback-fs")
g.Expect(mustRead(fs, "fallback-fs.txt")).To(gomega.Equal("fallback-fs"))
g.Expect(mustRead(primary, "fallback-fs.txt")).To(gomega.Equal("fallback-fs"))
g.Expect(mustRead(fs.Primary(), "fallback-fs.txt")).To(gomega.Equal("fallback-fs"))
_, err := read(secondary, "fallback-fs.txt")
g.Expect(err).To(gomega.HaveOccurred())
_, err = read(fs.Secondary(), "fallback-fs.txt")
g.Expect(err).To(gomega.HaveOccurred())
}
func TestLayeredFS_Primary(t *testing.T) {
g := gomega.NewWithT(t)
primary := fsx.NewMemMapFs()
fs := fsx.NewFallbackFS(primary, fsx.NewMemMapFs())
g.Expect(primary).To(gomega.BeIdenticalTo(fs.Primary().Fs))
}
func TestLayeredFS_Secondary(t *testing.T) {
g := gomega.NewWithT(t)
secondary := fsx.NewMemMapFs()
fs := fsx.NewFallbackFS(fsx.NewMemMapFs(), secondary)
g.Expect(secondary).To(gomega.BeIdenticalTo(fs.Secondary().Fs))
}
+59
View File
@@ -0,0 +1,59 @@
package fsx
import (
"io/fs"
"github.com/spf13/afero"
)
var (
// assert interfaces implemented
_ afero.Fs = (*BaseFS)(nil)
_ FS = (*BaseFS)(nil)
)
// FS is our default interface for filesystems.
type FS interface {
afero.Fs
IOFS() fs.FS
}
// BaseFS is our default implementation of the FS interface.
type BaseFS struct {
afero.Fs
}
// IOFS returns the filesystem as an io/fs.FS.
func (b *BaseFS) IOFS() fs.FS {
return afero.NewIOFS(b)
}
// FromAfero returns a new BaseFS instance from an afero.Fs.
func FromAfero(fSys afero.Fs) *BaseFS {
return &BaseFS{Fs: fSys}
}
// FromIOFS returns a new BaseFS instance from an io/fs.FS.
func FromIOFS(fSys fs.FS) *BaseFS {
return FromAfero(&afero.FromIOFS{FS: fSys})
}
// NewBasePathFs returns a new BaseFS which wraps the given filesystem with a base path.
func NewBasePathFs(fSys FS, basePath string) *BaseFS {
return FromAfero(afero.NewBasePathFs(fSys, basePath))
}
// NewOsFs returns a new BaseFS which wraps the OS filesystem.
func NewOsFs() *BaseFS {
return FromAfero(afero.NewOsFs())
}
// NewReadOnlyFs returns a new BaseFS which wraps the given filesystem with a read-only filesystem.
func NewReadOnlyFs(FfSys FS) *BaseFS {
return FromAfero(afero.NewReadOnlyFs(FfSys))
}
// NewMemMapFs returns a new BaseFS which wraps the memory filesystem.
func NewMemMapFs() *BaseFS {
return FromAfero(afero.NewMemMapFs())
}
+76
View File
@@ -0,0 +1,76 @@
package fsx_test
import (
"os"
"reflect"
"testing"
"github.com/onsi/gomega"
"github.com/owncloud/ocis/v2/ocis-pkg/x/io/fsx"
)
func TestBase(t *testing.T) {
g := gomega.NewWithT(t)
wrapped := fsx.NewMemMapFs()
fs := fsx.BaseFS{Fs: wrapped.Fs}
g.Expect(wrapped.Fs).Should(gomega.BeIdenticalTo(fs.Fs))
}
func TestBase_IOFS(t *testing.T) {
g := gomega.NewWithT(t)
fs := fsx.BaseFS{Fs: fsx.NewMemMapFs()}
g.Expect(reflect.TypeOf(fs.IOFS()).Name()).Should(gomega.Equal("IOFS"))
}
func TestFromIOFS(t *testing.T) {
g := gomega.NewWithT(t)
fs := fsx.FromIOFS(os.DirFS("."))
g.Expect(reflect.TypeOf(fs.Fs).String()).Should(gomega.Equal("*afero.FromIOFS"))
}
func TestNewOsFs(t *testing.T) {
g := gomega.NewWithT(t)
fs := fsx.NewOsFs()
g.Expect(reflect.TypeOf(fs.Fs).String()).Should(gomega.Equal("*afero.OsFs"))
}
func TestNewBasePathFs(t *testing.T) {
g := gomega.NewWithT(t)
base := fsx.NewMemMapFs()
err := base.MkdirAll("first/foo/bar", 0755)
g.Expect(err).ToNot(gomega.HaveOccurred())
err = base.MkdirAll("second/foo/baz", 0755)
g.Expect(err).ToNot(gomega.HaveOccurred())
fs := fsx.NewBasePathFs(base, "first")
info, err := fs.Stat("/foo/bar")
g.Expect(err).ToNot(gomega.HaveOccurred())
g.Expect(info.IsDir()).To(gomega.BeTrue())
info, err = fs.Stat("/foo/baz")
g.Expect(err).To(gomega.HaveOccurred())
g.Expect(info).To(gomega.BeNil())
info, err = fs.Stat("../second/foo/baz")
g.Expect(err).To(gomega.HaveOccurred())
g.Expect(info).To(gomega.BeNil())
}
func TestNewReadOnlyFs(t *testing.T) {
g := gomega.NewWithT(t)
base := fsx.NewMemMapFs()
err := base.MkdirAll("first/foo/bar", 0755)
g.Expect(err).ToNot(gomega.HaveOccurred())
fs := fsx.NewReadOnlyFs(base)
err = fs.MkdirAll("first/foo/bay", 0755)
g.Expect(err).To(gomega.HaveOccurred())
}
+12
View File
@@ -0,0 +1,12 @@
package filepathx
import (
"path/filepath"
)
// JailJoin joins any number of path elements into a single path,
// it protects against directory traversal by removing any "../" elements
// and ensuring that the path is always under the jail.
func JailJoin(jail string, elem ...string) string {
return filepath.Join(jail, filepath.Join(append([]string{"/"}, elem...)...))
}
+65
View File
@@ -0,0 +1,65 @@
package filepathx_test
import (
"testing"
"github.com/owncloud/ocis/v2/ocis-pkg/x/path/filepathx"
)
func TestJailJoin(t *testing.T) {
type args struct {
jail string
elem []string
}
tests := []struct {
name string
args args
want string
}{
{
name: "regular use case",
args: args{
jail: "/",
elem: []string{"a", "b", "c"},
},
want: "/a/b/c",
},
{
name: "access parent directory",
args: args{
jail: "/",
elem: []string{"a", "b", "c", ".."},
},
want: "/a/b",
},
{
name: "restrict breaking out of jail",
args: args{
jail: "/",
elem: []string{"a", "b", "c", "..", "..", "..", "..", "..", "..", ".."},
},
want: "/",
},
{
name: "restrict to child of jail",
args: args{
jail: "/a/b",
elem: []string{"a", "b", "c", "..", "..", "..", "..", "..", "..", ".."},
},
want: "/a/b",
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if got := filepathx.JailJoin(tt.args.jail, tt.args.elem...); got != tt.want {
t.Errorf("JailJoin() = %v, want %v", got, tt.want)
}
})
}
}