diff --git a/app/options.go b/app/options.go index 471324659..64a8e7f18 100644 --- a/app/options.go +++ b/app/options.go @@ -22,6 +22,7 @@ type ( HTTPServer options.HTTPServerOpt Websocket options.WebsocketOpt Eventbus options.EventbusOpt + Federation options.FederationOpt } ) @@ -44,5 +45,6 @@ func NewOptions() *Options { HTTPServer: *options.HTTPServer(), Websocket: *options.Websocket(), Eventbus: *options.Eventbus(), + Federation: *options.Federation(), } } diff --git a/app/servers.go b/app/servers.go index 2768bd413..5deb55906 100644 --- a/app/servers.go +++ b/app/servers.go @@ -3,6 +3,7 @@ package app import ( "context" composeRest "github.com/cortezaproject/corteza-server/compose/rest" + federationRest "github.com/cortezaproject/corteza-server/federation/rest" messagingRest "github.com/cortezaproject/corteza-server/messaging/rest" "github.com/cortezaproject/corteza-server/pkg/actionlog" "github.com/cortezaproject/corteza-server/pkg/api/server" @@ -70,4 +71,8 @@ func (app *CortezaApp) mountHttpRoutes(r chi.Router) { ) r.Route("/"+webappBaseUrl, webapp.MakeWebappServer(app.Opt.HTTPServer)) } + + if app.Opt.Federation.Enabled { + r.Route("/federation", federationRest.MountRoutes) + } } diff --git a/federation/rest.yaml b/federation/rest.yaml new file mode 100644 index 000000000..388dfd67b --- /dev/null +++ b/federation/rest.yaml @@ -0,0 +1,17 @@ +--- +# This is direct conversion from old api/*/spec.json files +# using https://www.json2yaml.com/ +# +# Next step: swagger. + +endpoints: + - title: Foobar + entrypoint: foobar + path: "/foobar" + authentication: [] + imports: + apis: + - name: foobar + method: GET + title: Foo bar + path: "/" diff --git a/federation/rest/foobar.go b/federation/rest/foobar.go new file mode 100644 index 000000000..4e1cbabb1 --- /dev/null +++ b/federation/rest/foobar.go @@ -0,0 +1,37 @@ +package rest + +import ( + "context" + + "github.com/cortezaproject/corteza-server/compose/types" + "github.com/cortezaproject/corteza-server/federation/rest/request" +) + +type ( + namespacePayload struct { + *types.Namespace + + CanGrant bool `json:"canGrant"` + CanUpdateNamespace bool `json:"canUpdateNamespace"` + CanDeleteNamespace bool `json:"canDeleteNamespace"` + CanManageNamespace bool `json:"canManageNamespace"` + CanCreateModule bool `json:"canCreateModule"` + CanCreateChart bool `json:"canCreateChart"` + CanCreatePage bool `json:"canCreatePage"` + } + + payload struct{} + Foobar struct{} + + accessController interface { + CanGrant(context.Context) bool + } +) + +func (Foobar) New() *Foobar { + return &Foobar{} +} + +func (ctrl Foobar) Foobar(ctx context.Context, r *request.FoobarFoobar) (interface{}, error) { + return &struct{}{}, nil +} diff --git a/federation/rest/handlers/foobar.go b/federation/rest/handlers/foobar.go new file mode 100644 index 000000000..af75fc41b --- /dev/null +++ b/federation/rest/handlers/foobar.go @@ -0,0 +1,63 @@ +package handlers + +// This file is auto-generated. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// Definitions file that controls how this file is generated: +// + +import ( + "context" + "github.com/go-chi/chi" + "github.com/titpetric/factory/resputil" + "net/http" + + "github.com/cortezaproject/corteza-server/federation/rest/request" + "github.com/cortezaproject/corteza-server/pkg/logger" +) + +type ( + // Internal API interface + FoobarAPI interface { + Foobar(context.Context, *request.FoobarFoobar) (interface{}, error) + } + + // HTTP API interface + Foobar struct { + Foobar func(http.ResponseWriter, *http.Request) + } +) + +func NewFoobar(h FoobarAPI) *Foobar { + return &Foobar{ + Foobar: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewFoobarFoobar() + if err := params.Fill(r); err != nil { + logger.LogParamError("Foobar.Foobar", r, err) + resputil.JSON(w, err) + return + } + + value, err := h.Foobar(r.Context(), params) + if err != nil { + logger.LogControllerError("Foobar.Foobar", r, err, params.Auditable()) + resputil.JSON(w, err) + return + } + logger.LogControllerCall("Foobar.Foobar", r, params.Auditable()) + if !serveHTTP(value, w, r) { + resputil.JSON(w, value) + } + }, + } +} + +func (h Foobar) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) { + r.Group(func(r chi.Router) { + r.Use(middlewares...) + r.Get("/foobar/", h.Foobar) + }) +} diff --git a/federation/rest/handlers/util.go b/federation/rest/handlers/util.go new file mode 100644 index 000000000..287c61d5d --- /dev/null +++ b/federation/rest/handlers/util.go @@ -0,0 +1,17 @@ +package handlers + +import ( + "net/http" +) + +// Edit this file in `codegen/common/rest/handlers/util.go`; +// any changes under [service] will be overwritten by codegen. + +func serveHTTP(value interface{}, w http.ResponseWriter, r *http.Request) bool { + switch fn := value.(type) { + case func(http.ResponseWriter, *http.Request): + fn(w, r) + return true + } + return false +} diff --git a/federation/rest/middleware.go b/federation/rest/middleware.go new file mode 100644 index 000000000..d98c814ba --- /dev/null +++ b/federation/rest/middleware.go @@ -0,0 +1,18 @@ +package rest + +import ( + "net/http" + + "github.com/cortezaproject/corteza-server/compose/service" +) + +func middlewareAllowedAccess(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if !service.DefaultAccessControl.CanAccess(r.Context()) { + http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden) + return + } + + next.ServeHTTP(w, r) + }) +} diff --git a/federation/rest/request/foobar.go b/federation/rest/request/foobar.go new file mode 100644 index 000000000..33b23eac6 --- /dev/null +++ b/federation/rest/request/foobar.go @@ -0,0 +1,60 @@ +package request + +// This file is auto-generated. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// Definitions file that controls how this file is generated: +// + +import ( + "encoding/json" + "fmt" + "github.com/cortezaproject/corteza-server/pkg/payload" + "github.com/go-chi/chi" + "io" + "mime/multipart" + "net/http" + "strings" +) + +// dummy vars to prevent +// unused imports complain +var ( + _ = chi.URLParam + _ = multipart.ErrMessageTooLarge + _ = payload.ParseUint64s +) + +type ( + // Internal API interface + FoobarFoobar struct { + } +) + +// NewFoobarFoobar request +func NewFoobarFoobar() *FoobarFoobar { + return &FoobarFoobar{} +} + +// Auditable returns all auditable/loggable parameters +func (r FoobarFoobar) Auditable() map[string]interface{} { + return map[string]interface{}{} +} + +// Fill processes request and fills internal variables +func (r *FoobarFoobar) Fill(req *http.Request) (err error) { + if strings.ToLower(req.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(req.Body).Decode(r) + + switch { + case err == io.EOF: + err = nil + case err != nil: + return fmt.Errorf("error parsing http request body: %w", err) + } + } + + return err +} diff --git a/federation/rest/router.go b/federation/rest/router.go new file mode 100644 index 000000000..d25625011 --- /dev/null +++ b/federation/rest/router.go @@ -0,0 +1,22 @@ +package rest + +import ( + "github.com/go-chi/chi" + + "github.com/cortezaproject/corteza-server/federation/rest/handlers" + "github.com/cortezaproject/corteza-server/pkg/auth" +) + +func MountRoutes(r chi.Router) { + var ( + foobar = Foobar{}.New() + ) + + // Protect all _private_ routes + r.Group(func(r chi.Router) { + r.Use(auth.MiddlewareValidOnly) + r.Use(middlewareAllowedAccess) + + handlers.NewFoobar(foobar).MountRoutes(r) + }) +} diff --git a/federation/types/node.go b/federation/types/node.go new file mode 100644 index 000000000..b25f1e23e --- /dev/null +++ b/federation/types/node.go @@ -0,0 +1,8 @@ +package types + +type ( + Node struct { + ID uint64 `json:"recordID,string"` + Name string `json:"name,string"` + } +) diff --git a/federation/types/type_set.gen.go b/federation/types/type_set.gen.go new file mode 100644 index 000000000..596601a24 --- /dev/null +++ b/federation/types/type_set.gen.go @@ -0,0 +1,73 @@ +package types + +// This file is auto-generated. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// Definitions file that controls how this file is generated: +// federation/types/types.yaml + +type ( + + // NodeSet slice of Node + // + // This type is auto-generated. + NodeSet []*Node +) + +// Walk iterates through every slice item and calls w(Node) err +// +// This function is auto-generated. +func (set NodeSet) Walk(w func(*Node) error) (err error) { + for i := range set { + if err = w(set[i]); err != nil { + return + } + } + + return +} + +// Filter iterates through every slice item, calls f(Node) (bool, err) and return filtered slice +// +// This function is auto-generated. +func (set NodeSet) Filter(f func(*Node) (bool, error)) (out NodeSet, err error) { + var ok bool + out = NodeSet{} + for i := range set { + if ok, err = f(set[i]); err != nil { + return + } else if ok { + out = append(out, set[i]) + } + } + + return +} + +// FindByID finds items from slice by its ID property +// +// This function is auto-generated. +func (set NodeSet) FindByID(ID uint64) *Node { + for i := range set { + if set[i].ID == ID { + return set[i] + } + } + + return nil +} + +// IDs returns a slice of uint64s from all items in the set +// +// This function is auto-generated. +func (set NodeSet) IDs() (IDs []uint64) { + IDs = make([]uint64, len(set)) + + for i := range set { + IDs[i] = set[i].ID + } + + return +} diff --git a/federation/types/type_set.gen_test.go b/federation/types/type_set.gen_test.go new file mode 100644 index 000000000..f6d0ea32e --- /dev/null +++ b/federation/types/type_set.gen_test.go @@ -0,0 +1,105 @@ +package types + +// This file is auto-generated. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// Definitions file that controls how this file is generated: +// federation/types/types.yaml + +import ( + "fmt" + "github.com/stretchr/testify/require" + "testing" +) + +func TestNodeSetWalk(t *testing.T) { + var ( + value = make(NodeSet, 3) + req = require.New(t) + ) + + // check walk with no errors + { + err := value.Walk(func(*Node) error { + return nil + }) + req.NoError(err) + } + + // check walk with error + req.Error(value.Walk(func(*Node) error { return fmt.Errorf("walk error") })) +} + +func TestNodeSetFilter(t *testing.T) { + var ( + value = make(NodeSet, 3) + req = require.New(t) + ) + + // filter nothing + { + set, err := value.Filter(func(*Node) (bool, error) { + return true, nil + }) + req.NoError(err) + req.Equal(len(set), len(value)) + } + + // filter one item + { + found := false + set, err := value.Filter(func(*Node) (bool, error) { + if !found { + found = true + return found, nil + } + return false, nil + }) + req.NoError(err) + req.Len(set, 1) + } + + // filter error + { + _, err := value.Filter(func(*Node) (bool, error) { + return false, fmt.Errorf("filter error") + }) + req.Error(err) + } +} + +func TestNodeSetIDs(t *testing.T) { + var ( + value = make(NodeSet, 3) + req = require.New(t) + ) + + // construct objects + value[0] = new(Node) + value[1] = new(Node) + value[2] = new(Node) + // set ids + value[0].ID = 1 + value[1].ID = 2 + value[2].ID = 3 + + // Find existing + { + val := value.FindByID(2) + req.Equal(uint64(2), val.ID) + } + + // Find non-existing + { + val := value.FindByID(4) + req.Nil(val) + } + + // List IDs from set + { + val := value.IDs() + req.Equal(len(val), len(value)) + } +} diff --git a/federation/types/types.yaml b/federation/types/types.yaml new file mode 100644 index 000000000..dfda807d0 --- /dev/null +++ b/federation/types/types.yaml @@ -0,0 +1,2 @@ +types: + Node: diff --git a/pkg/options/federation.go b/pkg/options/federation.go new file mode 100644 index 000000000..d4f759d3d --- /dev/null +++ b/pkg/options/federation.go @@ -0,0 +1,18 @@ +package options + +type ( + FederationOpt struct { + Enabled bool `env:"FEDERATION_ENABLED"` + } +) + +func Federation() (o *FederationOpt) { + + o = &FederationOpt{ + Enabled: false, + } + + fill(o) + + return +}