snapshot of https://github.com/cortezaproject/corteza.git (2024.9.x, 2026-09-18; blobs > 131072B omitted)
Some checks failed
SonarQube / SonarQube (push) Failing after 5m28s
E2e test on main branch push / server and client test (map[name:admin port:8080]) (push) Failing after 3m33s
Server and lib tests / Library test (js) (push) Failing after 1m36s
Server and lib tests / Library test (vue) (push) Successful in 2m24s
Server and lib tests / Web client test (admin) (push) Successful in 2m52s
Server and lib tests / Web client test (compose) (push) Successful in 3m23s
Server and lib tests / Web client test (discovery) (push) Successful in 2m49s
Server and lib tests / Web client test (one) (push) Successful in 2m52s
Server and lib tests / Web client test (privacy) (push) Successful in 2m44s
Server and lib tests / Web client test (reporter) (push) Successful in 2m56s
Server and lib tests / Web client test (workflow) (push) Successful in 3m0s
Server and lib tests / Server tests (push) Failing after 4m55s
E2e test on main branch push / notify-failure (push) Failing after 9s
Server and lib tests / Send matrix testing notification (push) Successful in 2s
Mark stale issues and pull requests / stale (push) Successful in 40s

This commit is contained in:
upstream-mirror
2026-09-18 01:22:23 +05:00
commit 7e385fc126
8391 changed files with 1766236 additions and 0 deletions

View File

@@ -0,0 +1,622 @@
package service
// This file is auto-generated.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
//
import (
"context"
"fmt"
"github.com/cortezaproject/corteza/server/federation/types"
"github.com/cortezaproject/corteza/server/pkg/actionlog"
internalAuth "github.com/cortezaproject/corteza/server/pkg/auth"
"github.com/cortezaproject/corteza/server/pkg/rbac"
"github.com/cortezaproject/corteza/server/store"
systemTypes "github.com/cortezaproject/corteza/server/system/types"
"github.com/spf13/cast"
"strings"
)
type (
rbacService interface {
Can(rbac.Session, string, rbac.Resource) bool
Trace(rbac.Session, string, rbac.Resource) *rbac.Trace
Grant(context.Context, ...*rbac.Rule) error
FindRulesByRoleID(roleID uint64) (rr rbac.RuleSet)
}
accessControl struct {
actionlog actionlog.Recorder
store store.Storer
rbac rbacService
}
)
func AccessControl(s store.Storer) *accessControl {
return &accessControl{
store: s,
rbac: rbac.Global(),
actionlog: DefaultActionlog,
}
}
func (svc accessControl) can(ctx context.Context, op string, res rbac.Resource) bool {
return svc.rbac.Can(rbac.ContextToSession(ctx), op, res)
}
// Effective returns a list of effective permissions for all given resource
//
// This function is auto-generated
func (svc accessControl) Effective(ctx context.Context, rr ...rbac.Resource) (ee rbac.EffectiveSet) {
for _, res := range rr {
r := res.RbacResource()
for op := range rbacResourceOperations(r) {
ee.Push(r, op, svc.can(ctx, op, res))
}
}
return
}
// Evaluate returns a list of permissions evaluated for the given user/roles combo
//
// This function is auto-generated
func (svc accessControl) Trace(ctx context.Context, userID uint64, roles []uint64, rr ...string) (ee []*rbac.Trace, err error) {
// Reusing the grant permission since this is who the feature is for
if !svc.CanGrant(ctx) {
// @todo should be altered to check grant permissions PER resource
return nil, AccessControlErrNotAllowedToSetPermissions()
}
var (
resource rbac.Resource
resources []rbac.Resource
members systemTypes.RoleMemberSet
)
if len(rr) > 0 {
resources = make([]rbac.Resource, 0, len(rr))
for _, r := range rr {
if err = rbacResourceValidator(r); err != nil {
return nil, fmt.Errorf("can not use resource %q: %w", r, err)
}
resource, err = svc.resourceLoader(ctx, r)
if err != nil {
return
}
resources = append(resources, resource)
}
} else {
resources = svc.Resources()
}
// User ID specified, load its roles
if userID != 0 {
if len(roles) > 0 {
// should be prevented on the client
return nil, fmt.Errorf("userID and roles are mutually exclusive")
}
members, _, err = store.SearchRoleMembers(ctx, svc.store, systemTypes.RoleMemberFilter{Resource: fmt.Sprintf("corteza::system:user/%d", userID)})
if err != nil {
return nil, err
}
for _, m := range members {
roles = append(roles, m.RoleID)
}
for _, r := range internalAuth.AuthenticatedRoles() {
roles = append(roles, r.ID)
}
}
if len(roles) == 0 {
// should be prevented on the client
return nil, fmt.Errorf("no roles specified")
}
session := rbac.ParamsToSession(ctx, userID, roles...)
for _, res := range resources {
r := res.RbacResource()
for op := range rbacResourceOperations(r) {
ee = append(ee, svc.rbac.Trace(session, op, res))
}
}
return
}
// Resources returns list of resources
//
// This function is auto-generated
func (svc accessControl) Resources() []rbac.Resource {
return []rbac.Resource{
rbac.NewResource(types.NodeRbacResource(0)),
rbac.NewResource(types.ExposedModuleRbacResource(0, 0)),
rbac.NewResource(types.SharedModuleRbacResource(0, 0)),
rbac.NewResource(types.ComponentRbacResource()),
}
}
// List returns list of operations on all resources
//
// This function is auto-generated
func (svc accessControl) List() (out []map[string]string) {
def := []map[string]string{
{
"type": types.NodeResourceType,
"any": types.NodeRbacResource(0),
"op": "manage",
},
{
"type": types.NodeResourceType,
"any": types.NodeRbacResource(0),
"op": "module.create",
},
{
"type": types.ExposedModuleResourceType,
"any": types.ExposedModuleRbacResource(0, 0),
"op": "manage",
},
{
"type": types.SharedModuleResourceType,
"any": types.SharedModuleRbacResource(0, 0),
"op": "map",
},
{
"type": types.ComponentResourceType,
"any": types.ComponentRbacResource(),
"op": "grant",
},
{
"type": types.ComponentResourceType,
"any": types.ComponentRbacResource(),
"op": "pair",
},
{
"type": types.ComponentResourceType,
"any": types.ComponentRbacResource(),
"op": "settings.read",
},
{
"type": types.ComponentResourceType,
"any": types.ComponentRbacResource(),
"op": "settings.manage",
},
{
"type": types.ComponentResourceType,
"any": types.ComponentRbacResource(),
"op": "node.create",
},
{
"type": types.ComponentResourceType,
"any": types.ComponentRbacResource(),
"op": "nodes.search",
},
}
func(svc interface{}) {
if svc, is := svc.(interface{}).(interface{ list() []map[string]string }); is {
def = append(def, svc.list()...)
}
}(svc)
return def
}
// Grant applies one or more RBAC rules
//
// This function is auto-generated
func (svc accessControl) Grant(ctx context.Context, rr ...*rbac.Rule) error {
if !svc.CanGrant(ctx) {
// @todo should be altered to check grant permissions PER resource
return AccessControlErrNotAllowedToSetPermissions()
}
for _, r := range rr {
err := rbacResourceValidator(r.Resource, r.Operation)
if err != nil {
return err
}
}
if err := svc.rbac.Grant(ctx, rr...); err != nil {
return AccessControlErrGeneric().Wrap(err)
}
svc.logGrants(ctx, rr)
return nil
}
// This function is auto-generated
func (svc accessControl) logGrants(ctx context.Context, rr []*rbac.Rule) {
if svc.actionlog == nil {
return
}
for _, r := range rr {
g := AccessControlActionGrant(&accessControlActionProps{r})
g.log = r.String()
g.resource = r.Resource
svc.actionlog.Record(ctx, g.ToAction())
}
}
// FindRules find all rules based on filters
//
// This function is auto-generated
func (svc accessControl) FindRules(ctx context.Context, roleID uint64, rr ...string) (out rbac.RuleSet, err error) {
if !svc.CanGrant(ctx) {
return nil, AccessControlErrNotAllowedToSetPermissions()
}
out, err = svc.FindRulesByRoleID(ctx, roleID)
if err != nil {
return
}
var resources []rbac.Resource
if len(rr) > 0 {
resources = make([]rbac.Resource, 0, len(rr))
for _, r := range rr {
if err = rbacResourceValidator(r); err != nil {
return nil, fmt.Errorf("can not use resource %q: %w", r, err)
}
resources = append(resources, rbac.NewResource(r))
}
} else {
resources = svc.Resources()
}
return out.FilterResource(resources...), nil
}
// FindRulesByRoleID find all rules for a specific role
//
// This function is auto-generated
func (svc accessControl) FindRulesByRoleID(ctx context.Context, roleID uint64) (rbac.RuleSet, error) {
if !svc.CanGrant(ctx) {
return nil, AccessControlErrNotAllowedToSetPermissions()
}
return svc.rbac.FindRulesByRoleID(roleID), nil
}
// CanManageNode checks if current user can manage federation node
//
// This function is auto-generated
func (svc accessControl) CanManageNode(ctx context.Context, r *types.Node) bool {
return svc.can(ctx, "manage", r)
}
// CanCreateModuleOnNode checks if current user can create shared module
//
// This function is auto-generated
func (svc accessControl) CanCreateModuleOnNode(ctx context.Context, r *types.Node) bool {
return svc.can(ctx, "module.create", r)
}
// CanManageExposedModule checks if current user can manage exposed module module
//
// This function is auto-generated
func (svc accessControl) CanManageExposedModule(ctx context.Context, r *types.ExposedModule) bool {
return svc.can(ctx, "manage", r)
}
// CanMapSharedModule checks if current user can map shared module
//
// This function is auto-generated
func (svc accessControl) CanMapSharedModule(ctx context.Context, r *types.SharedModule) bool {
return svc.can(ctx, "map", r)
}
// CanGrant checks if current user can manage federation permissions
//
// This function is auto-generated
func (svc accessControl) CanGrant(ctx context.Context) bool {
r := &types.Component{}
return svc.can(ctx, "grant", r)
}
// CanPair checks if current user can pair federation nodes
//
// This function is auto-generated
func (svc accessControl) CanPair(ctx context.Context) bool {
r := &types.Component{}
return svc.can(ctx, "pair", r)
}
// CanReadSettings checks if current user can read settings
//
// This function is auto-generated
func (svc accessControl) CanReadSettings(ctx context.Context) bool {
r := &types.Component{}
return svc.can(ctx, "settings.read", r)
}
// CanManageSettings checks if current user can manage settings
//
// This function is auto-generated
func (svc accessControl) CanManageSettings(ctx context.Context) bool {
r := &types.Component{}
return svc.can(ctx, "settings.manage", r)
}
// CanCreateNode checks if current user can create new federation node
//
// This function is auto-generated
func (svc accessControl) CanCreateNode(ctx context.Context) bool {
r := &types.Component{}
return svc.can(ctx, "node.create", r)
}
// CanSearchNodes checks if current user can list, search or filter federation nodes
//
// This function is auto-generated
func (svc accessControl) CanSearchNodes(ctx context.Context) bool {
r := &types.Component{}
return svc.can(ctx, "nodes.search", r)
}
// rbacResourceValidator validates known component's resource by routing it to the appropriate validator
//
// This function is auto-generated
func rbacResourceValidator(r string, oo ...string) error {
switch rbac.ResourceType(r) {
case types.NodeResourceType:
return rbacNodeResourceValidator(r, oo...)
case types.ExposedModuleResourceType:
return rbacExposedModuleResourceValidator(r, oo...)
case types.SharedModuleResourceType:
return rbacSharedModuleResourceValidator(r, oo...)
case types.ComponentResourceType:
return rbacComponentResourceValidator(r, oo...)
}
return fmt.Errorf("unknown resource type %q", r)
}
// resourceLoader loads resource from store
//
// # Notes
// Function assumes existence of loader functions for all resource types
//
// This function is auto-generated
func (svc accessControl) resourceLoader(ctx context.Context, resource string) (rbac.Resource, error) {
var (
hasWildcard = false
resourceType, ids = rbac.ParseResourceID(resource)
)
for _, id := range ids {
if id == 0 {
hasWildcard = true
break
}
}
switch rbac.ResourceType(resourceType) {
case types.NodeResourceType:
if hasWildcard {
return rbac.NewResource(types.NodeRbacResource(ids[0])), nil
}
return loadNode(ctx, svc.store, ids[0])
case types.ExposedModuleResourceType:
if hasWildcard {
return rbac.NewResource(types.ExposedModuleRbacResource(ids[0], ids[1])), nil
}
return loadExposedModule(ctx, svc.store, ids[0], ids[1])
case types.SharedModuleResourceType:
if hasWildcard {
return rbac.NewResource(types.SharedModuleRbacResource(ids[0], ids[1])), nil
}
return loadSharedModule(ctx, svc.store, ids[0], ids[1])
case types.ComponentResourceType:
return &types.Component{}, nil
}
_ = ids
return nil, fmt.Errorf("unknown resource type %q", resourceType)
}
// rbacResourceOperations returns defined operations for a requested resource
//
// This function is auto-generated
func rbacResourceOperations(r string) map[string]bool {
switch rbac.ResourceType(r) {
case types.NodeResourceType:
return map[string]bool{
"manage": true,
"module.create": true,
}
case types.ExposedModuleResourceType:
return map[string]bool{
"manage": true,
}
case types.SharedModuleResourceType:
return map[string]bool{
"map": true,
}
case types.ComponentResourceType:
return map[string]bool{
"grant": true,
"pair": true,
"settings.read": true,
"settings.manage": true,
"node.create": true,
"nodes.search": true,
}
}
return nil
}
// rbacNodeResourceValidator checks validity of RBAC resource and operations
//
// # Notes
// Can be called without operations to check for validity of resource string only
//
// This function is auto-generated
func rbacNodeResourceValidator(r string, oo ...string) error {
if !strings.HasPrefix(r, types.NodeResourceType) {
// expecting resource to always include path
return fmt.Errorf("invalid resource type")
}
defOps := rbacResourceOperations(r)
for _, o := range oo {
if !defOps[o] {
return fmt.Errorf("invalid operation '%s' for node resource", o)
}
}
const sep = "/"
var (
pp = strings.Split(strings.Trim(r[len(types.NodeResourceType):], sep), sep)
prc = []string{
"ID",
}
)
if len(pp) != len(prc) {
return fmt.Errorf("invalid resource path structure")
}
for i := 0; i < len(pp); i++ {
if pp[i] != "*" {
if i > 0 && pp[i-1] == "*" {
return fmt.Errorf("invalid path wildcard level (%d) for node resource", i)
}
if _, err := cast.ToUint64E(pp[i]); err != nil {
return fmt.Errorf("invalid reference for %s: '%s'", prc[i], pp[i])
}
}
}
return nil
}
// rbacExposedModuleResourceValidator checks validity of RBAC resource and operations
//
// # Notes
// Can be called without operations to check for validity of resource string only
//
// This function is auto-generated
func rbacExposedModuleResourceValidator(r string, oo ...string) error {
if !strings.HasPrefix(r, types.ExposedModuleResourceType) {
// expecting resource to always include path
return fmt.Errorf("invalid resource type")
}
defOps := rbacResourceOperations(r)
for _, o := range oo {
if !defOps[o] {
return fmt.Errorf("invalid operation '%s' for exposedModule resource", o)
}
}
const sep = "/"
var (
pp = strings.Split(strings.Trim(r[len(types.ExposedModuleResourceType):], sep), sep)
prc = []string{
"NodeID",
"ID",
}
)
if len(pp) != len(prc) {
return fmt.Errorf("invalid resource path structure")
}
for i := 0; i < len(pp); i++ {
if pp[i] != "*" {
if i > 0 && pp[i-1] == "*" {
return fmt.Errorf("invalid path wildcard level (%d) for exposedModule resource", i)
}
if _, err := cast.ToUint64E(pp[i]); err != nil {
return fmt.Errorf("invalid reference for %s: '%s'", prc[i], pp[i])
}
}
}
return nil
}
// rbacSharedModuleResourceValidator checks validity of RBAC resource and operations
//
// # Notes
// Can be called without operations to check for validity of resource string only
//
// This function is auto-generated
func rbacSharedModuleResourceValidator(r string, oo ...string) error {
if !strings.HasPrefix(r, types.SharedModuleResourceType) {
// expecting resource to always include path
return fmt.Errorf("invalid resource type")
}
defOps := rbacResourceOperations(r)
for _, o := range oo {
if !defOps[o] {
return fmt.Errorf("invalid operation '%s' for sharedModule resource", o)
}
}
const sep = "/"
var (
pp = strings.Split(strings.Trim(r[len(types.SharedModuleResourceType):], sep), sep)
prc = []string{
"NodeID",
"ID",
}
)
if len(pp) != len(prc) {
return fmt.Errorf("invalid resource path structure")
}
for i := 0; i < len(pp); i++ {
if pp[i] != "*" {
if i > 0 && pp[i-1] == "*" {
return fmt.Errorf("invalid path wildcard level (%d) for sharedModule resource", i)
}
if _, err := cast.ToUint64E(pp[i]); err != nil {
return fmt.Errorf("invalid reference for %s: '%s'", prc[i], pp[i])
}
}
}
return nil
}
// rbacComponentResourceValidator checks validity of RBAC resource and operations
//
// # Notes
// Can be called without operations to check for validity of resource string only
//
// This function is auto-generated
func rbacComponentResourceValidator(r string, oo ...string) error {
if r != types.ComponentResourceType+"/" {
// expecting resource to always include path
return fmt.Errorf("invalid component resource, expecting " + types.ComponentResourceType + "/")
}
defOps := rbacResourceOperations(r)
for _, o := range oo {
if !defOps[o] {
return fmt.Errorf("invalid operation '%s' for federation component resource", o)
}
}
return nil
}

View File

@@ -0,0 +1,284 @@
package service
// 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/service/access_control_actions.yaml
import (
"context"
"fmt"
"github.com/cortezaproject/corteza/server/pkg/actionlog"
"github.com/cortezaproject/corteza/server/pkg/errors"
"github.com/cortezaproject/corteza/server/pkg/locale"
"github.com/cortezaproject/corteza/server/pkg/rbac"
"strings"
"time"
)
type (
accessControlActionProps struct {
rule *rbac.Rule
}
accessControlAction struct {
timestamp time.Time
resource string
action string
log string
severity actionlog.Severity
// prefix for error when action fails
errorMessage string
props *accessControlActionProps
}
accessControlLogMetaKey struct{}
accessControlPropsMetaKey struct{}
)
var (
// just a placeholder to cover template cases w/o fmt package use
_ = fmt.Println
)
// *********************************************************************************************************************
// *********************************************************************************************************************
// Props methods
// setRule updates accessControlActionProps's rule
//
// This function is auto-generated.
func (p *accessControlActionProps) setRule(rule *rbac.Rule) *accessControlActionProps {
p.rule = rule
return p
}
// Serialize converts accessControlActionProps to actionlog.Meta
//
// This function is auto-generated.
func (p accessControlActionProps) Serialize() actionlog.Meta {
var (
m = make(actionlog.Meta)
)
if p.rule != nil {
m.Set("rule.operation", p.rule.Operation, true)
m.Set("rule.roleID", p.rule.RoleID, true)
m.Set("rule.access", p.rule.Access, true)
m.Set("rule.resource", p.rule.Resource, true)
}
return m
}
// tr translates string and replaces meta value placeholder with values
//
// This function is auto-generated.
func (p accessControlActionProps) Format(in string, err error) string {
var (
pairs = []string{"{{err}}"}
// first non-empty string
fns = func(ii ...interface{}) string {
for _, i := range ii {
if s := fmt.Sprintf("%v", i); len(s) > 0 {
return s
}
}
return ""
}
)
if err != nil {
pairs = append(pairs, err.Error())
} else {
pairs = append(pairs, "nil")
}
if p.rule != nil {
// replacement for "{{rule}}" (in order how fields are defined)
pairs = append(
pairs,
"{{rule}}",
fns(
p.rule.Operation,
p.rule.RoleID,
p.rule.Access,
p.rule.Resource,
),
)
pairs = append(pairs, "{{rule.operation}}", fns(p.rule.Operation))
pairs = append(pairs, "{{rule.roleID}}", fns(p.rule.RoleID))
pairs = append(pairs, "{{rule.access}}", fns(p.rule.Access))
pairs = append(pairs, "{{rule.resource}}", fns(p.rule.Resource))
}
return strings.NewReplacer(pairs...).Replace(in)
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// Action methods
// String returns loggable description as string
//
// This function is auto-generated.
func (a *accessControlAction) String() string {
var props = &accessControlActionProps{}
if a.props != nil {
props = a.props
}
return props.Format(a.log, nil)
}
func (e *accessControlAction) ToAction() *actionlog.Action {
return &actionlog.Action{
Resource: e.resource,
Action: e.action,
Severity: e.severity,
Description: e.String(),
Meta: e.props.Serialize(),
}
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// Action constructors
// AccessControlActionGrant returns "federation:access_control.grant" action
//
// This function is auto-generated.
func AccessControlActionGrant(props ...*accessControlActionProps) *accessControlAction {
a := &accessControlAction{
timestamp: time.Now(),
resource: "federation:access_control",
action: "grant",
log: "grant",
severity: actionlog.Notice,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// Error constructors
// AccessControlErrGeneric returns "federation:access_control.generic" as *errors.Error
//
// This function is auto-generated.
func AccessControlErrGeneric(mm ...*accessControlActionProps) *errors.Error {
var p = &accessControlActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("failed to complete request due to internal error", nil),
errors.Meta("type", "generic"),
errors.Meta("resource", "federation:access_control"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(accessControlLogMetaKey{}, "{err}"),
errors.Meta(accessControlPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
errors.Meta(locale.ErrorMetaKey{}, "access-control.errors.generic"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// AccessControlErrNotAllowedToSetPermissions returns "federation:access_control.notAllowedToSetPermissions" as *errors.Error
//
// This function is auto-generated.
func AccessControlErrNotAllowedToSetPermissions(mm ...*accessControlActionProps) *errors.Error {
var p = &accessControlActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("not allowed to set permissions", nil),
errors.Meta("type", "notAllowedToSetPermissions"),
errors.Meta("resource", "federation:access_control"),
errors.Meta(accessControlPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
errors.Meta(locale.ErrorMetaKey{}, "access-control.errors.notAllowedToSetPermissions"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// recordAction is a service helper function wraps function that can return error
//
// It will wrap unrecognized/internal errors with generic errors.
//
// This function is auto-generated.
func (svc accessControl) recordAction(ctx context.Context, props *accessControlActionProps, actionFn func(...*accessControlActionProps) *accessControlAction, err error) error {
if svc.actionlog == nil || actionFn == nil {
// action log disabled or no action fn passed, return error as-is
return err
} else if err == nil {
// action completed w/o error, record it
svc.actionlog.Record(ctx, actionFn(props).ToAction())
return nil
}
a := actionFn(props).ToAction()
// Extracting error information and recording it as action
a.Error = err.Error()
switch c := err.(type) {
case *errors.Error:
m := c.Meta()
a.Error = err.Error()
a.Severity = actionlog.Severity(m.AsInt("severity"))
a.Description = props.Format(m.AsString(accessControlLogMetaKey{}), err)
if p, has := m[accessControlPropsMetaKey{}]; has {
a.Meta = p.(*accessControlActionProps).Serialize()
}
svc.actionlog.Record(ctx, a)
default:
svc.actionlog.Record(ctx, a)
}
// Original error is passed on
return err
}

View File

@@ -0,0 +1,25 @@
# List of security/audit events and errors that we need to log
resource: federation:access_control
service: accessControl
# Default sensitivity for actions
defaultActionSeverity: notice
# default severity for errors
defaultErrorSeverity: alert
import:
- github.com/cortezaproject/corteza/server/pkg/rbac
props:
- name: rule
type: "*rbac.Rule"
fields: [ operation, roleID, access, resource ]
actions:
- action: grant
errors:
- error: notAllowedToSetPermissions
message: "not allowed to set permissions"

View File

@@ -0,0 +1,11 @@
package decoder
import (
"github.com/cortezaproject/corteza/server/compose/types"
)
type (
ComposeModule struct {
types.Module
}
)

View File

@@ -0,0 +1,16 @@
package decoder
import (
"github.com/cortezaproject/corteza/server/compose/types"
)
type (
ComposeRecord struct {
types.Record
}
ComposeRecordSet []*ComposeRecord
ComposeRecordFilter struct {
types.RecordFilter
}
)

View File

@@ -0,0 +1,105 @@
package decoder
import (
"encoding/json"
"time"
"github.com/cortezaproject/corteza/server/compose/types"
ftypes "github.com/cortezaproject/corteza/server/federation/types"
)
type (
ExposedRecord struct {
ID uint64 `json:"recordID,string"`
Values types.RecordValueSet `json:"values"`
CreatedAt time.Time `json:"createdAt,omitempty"`
UpdatedAt *time.Time `json:"updatedAt,omitempty"`
DeletedAt *time.Time `json:"deletedAt,omitempty"`
}
// Bits for decoding structure sync
ModuleDocument struct {
Response ComposeModule
}
ExposedModuleDocument struct {
Response struct {
Set ftypes.ExposedModuleSet
}
}
ExposedRecordDocument struct {
Response struct {
Set []*ExposedRecord
}
}
ExposedModule struct {
ftypes.ExposedModule
}
// Bits for decoding data sync
ComposeRecordResponse struct {
Filter ComposeRecordFilter `json:",omitempty"`
Set ComposeRecordSet `json:",omitempty"`
}
ComposeRecordDocument struct {
Response ComposeRecordResponse
}
)
// DecodeModuleSync decodes the response from the structure sync request.
//
// It returns a set of modules.
func DecodeModuleSync(in []byte) (types.ModuleSet, error) {
out := &ModuleDocument{}
err := json.Unmarshal(in, out)
if err != nil {
return nil, err
}
return types.ModuleSet{&out.Response.Module}, nil
}
func DecodeFederationModuleSync(in []byte) (ftypes.ExposedModuleSet, error) {
out := &ExposedModuleDocument{}
err := json.Unmarshal(in, out)
if err != nil {
return nil, err
}
return out.Response.Set, nil
}
func DecodeFederationRecordSync(in []byte) ([]*ExposedRecord, error) {
out := &ExposedRecordDocument{}
err := json.Unmarshal(in, out)
if err != nil {
return nil, err
}
return out.Response.Set, nil
}
// DecodeRecordSync decodes the response from the data sync request.
//
// It returns a set of records along with the used filter.
func DecodeRecordSync(in []byte) (types.RecordSet, *types.RecordFilter, error) {
out := &ComposeRecordDocument{}
err := json.Unmarshal(in, out)
if err != nil {
return nil, nil, err
}
ss := make(types.RecordSet, 0, len(out.Response.Set))
for _, r := range out.Response.Set {
ss = append(ss, &r.Record)
}
return ss, &out.Response.Filter.RecordFilter, nil
}

View File

@@ -0,0 +1,341 @@
package service
import (
"context"
"strconv"
"github.com/cortezaproject/corteza/server/pkg/errors"
cs "github.com/cortezaproject/corteza/server/compose/service"
ct "github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/federation/types"
"github.com/cortezaproject/corteza/server/pkg/actionlog"
"github.com/cortezaproject/corteza/server/pkg/auth"
"github.com/cortezaproject/corteza/server/pkg/rbac"
"github.com/cortezaproject/corteza/server/store"
ss "github.com/cortezaproject/corteza/server/system/service"
st "github.com/cortezaproject/corteza/server/system/types"
)
type (
exposedModule struct {
node node
ac exposedModuleAccessController
module cs.ModuleService
namespace cs.NamespaceService
role ss.RoleService
store store.Storer
actionlog actionlog.Recorder
}
exposedModuleAccessController interface {
CanCreateModuleOnNode(ctx context.Context, r *types.Node) bool
CanManageExposedModule(ctx context.Context, r *types.ExposedModule) bool
}
ExposedModuleService interface {
Create(ctx context.Context, new *types.ExposedModule) (*types.ExposedModule, error)
Update(ctx context.Context, updated *types.ExposedModule) (*types.ExposedModule, error)
Find(ctx context.Context, filter types.ExposedModuleFilter) (types.ExposedModuleSet, types.ExposedModuleFilter, error)
FindByID(ctx context.Context, nodeID uint64, moduleID uint64) (*types.ExposedModule, error)
DeleteByID(ctx context.Context, nodeID, moduleID uint64) (*types.ExposedModule, error)
}
moduleUpdateHandler func(ctx context.Context, ns *types.Node, c *types.ExposedModule) (bool, bool, error)
)
func ExposedModule() *exposedModule {
return &exposedModule{
ac: DefaultAccessControl,
node: *DefaultNode,
module: cs.DefaultModule,
role: ss.DefaultRole,
namespace: cs.DefaultNamespace,
store: DefaultStore,
actionlog: DefaultActionlog,
}
}
// FindByAny tries to find module in a particular namespace by id, handle or name
func (svc exposedModule) FindByAny(ctx context.Context, nodeID uint64, identifier interface{}) (m *types.ExposedModule, err error) {
if ID, ok := identifier.(uint64); ok {
m, err = svc.FindByID(ctx, nodeID, ID)
} else if strIdentifier, ok := identifier.(string); ok {
if ID, _ := strconv.ParseUint(strIdentifier, 10, 64); ID > 0 {
m, err = svc.FindByID(ctx, nodeID, ID)
}
} else {
// force invalid ID error
// we do that to wrap error with lookup action context
_, err = svc.FindByID(ctx, nodeID, 0)
}
if err != nil {
return nil, err
}
return m, nil
}
func (svc exposedModule) FindByID(ctx context.Context, nodeID uint64, moduleID uint64) (module *types.ExposedModule, err error) {
err = func() error {
if module, err = loadExposedModule(ctx, svc.store, nodeID, moduleID); err != nil {
return err
}
if !svc.ac.CanManageExposedModule(ctx, module) {
return ExposedModuleErrNotAllowedToManage()
}
return nil
}()
return module, err
}
func (svc exposedModule) Update(ctx context.Context, updated *types.ExposedModule) (*types.ExposedModule, error) {
var (
aProps = &exposedModuleActionProps{update: updated}
)
err := store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) {
var (
m *ct.Module
node *types.Node
old *types.ExposedModule
)
if node, err = svc.node.FindByID(ctx, updated.NodeID); err != nil {
return ExposedModuleErrNodeNotFound()
}
if !svc.ac.CanManageExposedModule(ctx, updated) {
return ExposedModuleErrNotAllowedToManage()
}
if _, err := svc.namespace.FindByID(ctx, updated.ComposeNamespaceID); err != nil {
return ExposedModuleErrComposeNamespaceNotFound()
}
if m, err = svc.module.FindByID(ctx, updated.ComposeNamespaceID, updated.ComposeModuleID); err != nil {
return ExposedModuleErrComposeModuleNotFound()
}
if old, err = svc.FindByID(ctx, updated.NodeID, updated.ID); err != nil {
return ExposedModuleErrNotFound()
}
updated.UpdatedAt = now()
updated.CreatedAt = old.CreatedAt
updated.UpdatedBy = auth.GetIdentityFromContext(ctx).Identity()
// set labels
AddFederationLabel(m, "federation", node.BaseURL)
if _, err := svc.module.Update(ctx, m); err != nil {
return err
}
aProps.setModule(updated)
if err = store.UpdateFederationExposedModule(ctx, s, updated); err != nil {
return err
}
return nil
})
return updated, svc.recordAction(ctx, aProps, ExposedModuleActionUpdate, err)
}
func (svc exposedModule) updater(ctx context.Context, nodeID, moduleID uint64, action func(...*exposedModuleActionProps) *exposedModuleAction, fn moduleUpdateHandler) (*types.ExposedModule, error) {
var (
moduleChanged, fieldsChanged bool
n *types.Node
m *types.ExposedModule
aProps = &exposedModuleActionProps{module: &types.ExposedModule{ID: moduleID, NodeID: nodeID}}
err error
)
err = store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) {
if m, err = loadExposedModule(ctx, svc.store, nodeID, moduleID); err != nil {
return err
}
// TODO - handle node id also
if moduleChanged, fieldsChanged, err = fn(ctx, n, m); err != nil {
return err
}
_ = moduleChanged
_ = fieldsChanged
return err
})
return m, svc.recordAction(ctx, aProps, action, err)
}
func (svc exposedModule) DeleteByID(ctx context.Context, nodeID, moduleID uint64) (m *types.ExposedModule, err error) {
var (
aProps = &exposedModuleActionProps{}
)
err = store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) {
var (
m *types.ExposedModule
)
if _, err = svc.node.FindByID(ctx, nodeID); err != nil {
return ExposedModuleErrNodeNotFound()
}
if m, err = svc.FindByID(ctx, nodeID, moduleID); err != nil {
return err
}
if !svc.ac.CanManageExposedModule(ctx, m) {
return ExposedModuleErrNotAllowedToManage()
}
m.DeletedAt = now()
m.DeletedBy = auth.GetIdentityFromContext(ctx).Identity()
if err = store.UpdateFederationExposedModule(ctx, s, m); err != nil {
return err
}
aProps.delete = m
return nil
})
return m, svc.recordAction(ctx, aProps, ExposedModuleActionDelete, err)
}
func (svc exposedModule) Find(ctx context.Context, filter types.ExposedModuleFilter) (set types.ExposedModuleSet, f types.ExposedModuleFilter, err error) {
filter.Check = func(res *types.ExposedModule) (bool, error) {
if !svc.ac.CanManageExposedModule(ctx, res) {
return false, ExposedModuleErrNotAllowedToManage()
}
return true, nil
}
err = func() error {
if set, f, err = store.SearchFederationExposedModules(ctx, svc.store, filter); err != nil {
return err
}
return nil
}()
return set, f, err
}
func (svc exposedModule) Create(ctx context.Context, new *types.ExposedModule) (*types.ExposedModule, error) {
var (
aProps = &exposedModuleActionProps{create: new}
)
err := store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) {
var (
m *ct.Module
node *types.Node
fedRole *st.Role
)
if node, err = svc.node.FindByID(ctx, new.NodeID); err != nil {
return ExposedModuleErrNodeNotFound()
}
if !svc.ac.CanCreateModuleOnNode(ctx, node) {
return ExposedModuleErrNotAllowedToCreate()
}
if _, err := svc.namespace.FindByID(ctx, new.ComposeNamespaceID); err != nil {
return ExposedModuleErrComposeNamespaceNotFound()
}
if m, err = svc.module.FindByID(ctx, new.ComposeNamespaceID, new.ComposeModuleID); err != nil {
return ExposedModuleErrComposeModuleNotFound()
}
if err = svc.uniqueCheck(ctx, new); err != nil {
return err
}
ctxIdentity := auth.GetIdentityFromContext(ctx)
new.ID = nextID()
new.CreatedAt = *now()
new.CreatedBy = ctxIdentity.Identity()
// find the federation role
if fedRole, err = svc.role.FindByHandle(ctx, "federation"); err != nil {
return nil
}
if fedRole != nil {
// get first ID from role and add it as allow rule
err = cs.DefaultAccessControl.Grant(ctx, rbac.AllowRule(fedRole.ID, ct.RecordRbacResource(m.NamespaceID, m.ID, 0), "read"))
}
// set labels
AddFederationLabel(m, "federation", node.BaseURL)
if _, err := svc.module.Update(ctx, m); err != nil {
return err
}
aProps.setModule(new)
if err = store.CreateFederationExposedModule(ctx, s, new); err != nil {
return err
}
return nil
})
return new, svc.recordAction(ctx, aProps, ExposedModuleActionCreate, err)
}
func (svc exposedModule) uniqueCheck(ctx context.Context, m *types.ExposedModule) (err error) {
f := types.ExposedModuleFilter{
NodeID: m.NodeID,
ComposeModuleID: m.ComposeModuleID,
ComposeNamespaceID: m.ComposeNamespaceID,
}
set, _, err := store.SearchFederationExposedModules(ctx, svc.store, f)
if len(set) > 0 && err == nil {
return ExposedModuleErrNotUnique()
} else if err != nil {
return err
}
return nil
}
func loadExposedModule(ctx context.Context, s store.FederationExposedModules, nodeID, ID uint64) (res *types.ExposedModule, err error) {
if ID == 0 || nodeID == 0 {
return nil, SharedModuleErrInvalidID()
}
if res, err = store.LookupFederationExposedModuleByID(ctx, s, ID); errors.IsNotFound(err) {
err = SharedModuleErrNotFound()
}
if err != nil {
return
}
if nodeID != res.NodeID {
// Make sure chart belongs to the right namespace
return nil, SharedModuleErrNotFound()
}
return
}

View File

@@ -0,0 +1,829 @@
package service
// 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/service/exposed_module_actions.yaml
import (
"context"
"fmt"
"github.com/cortezaproject/corteza/server/federation/types"
"github.com/cortezaproject/corteza/server/pkg/actionlog"
"github.com/cortezaproject/corteza/server/pkg/errors"
"github.com/cortezaproject/corteza/server/pkg/locale"
"strings"
"time"
)
type (
exposedModuleActionProps struct {
module *types.ExposedModule
update *types.ExposedModule
create *types.ExposedModule
delete *types.ExposedModule
filter *types.ExposedModuleFilter
node *types.Node
}
exposedModuleAction struct {
timestamp time.Time
resource string
action string
log string
severity actionlog.Severity
// prefix for error when action fails
errorMessage string
props *exposedModuleActionProps
}
exposedModuleLogMetaKey struct{}
exposedModulePropsMetaKey struct{}
)
var (
// just a placeholder to cover template cases w/o fmt package use
_ = fmt.Println
)
// *********************************************************************************************************************
// *********************************************************************************************************************
// Props methods
// setModule updates exposedModuleActionProps's module
//
// This function is auto-generated.
func (p *exposedModuleActionProps) setModule(module *types.ExposedModule) *exposedModuleActionProps {
p.module = module
return p
}
// setUpdate updates exposedModuleActionProps's update
//
// This function is auto-generated.
func (p *exposedModuleActionProps) setUpdate(update *types.ExposedModule) *exposedModuleActionProps {
p.update = update
return p
}
// setCreate updates exposedModuleActionProps's create
//
// This function is auto-generated.
func (p *exposedModuleActionProps) setCreate(create *types.ExposedModule) *exposedModuleActionProps {
p.create = create
return p
}
// setDelete updates exposedModuleActionProps's delete
//
// This function is auto-generated.
func (p *exposedModuleActionProps) setDelete(delete *types.ExposedModule) *exposedModuleActionProps {
p.delete = delete
return p
}
// setFilter updates exposedModuleActionProps's filter
//
// This function is auto-generated.
func (p *exposedModuleActionProps) setFilter(filter *types.ExposedModuleFilter) *exposedModuleActionProps {
p.filter = filter
return p
}
// setNode updates exposedModuleActionProps's node
//
// This function is auto-generated.
func (p *exposedModuleActionProps) setNode(node *types.Node) *exposedModuleActionProps {
p.node = node
return p
}
// Serialize converts exposedModuleActionProps to actionlog.Meta
//
// This function is auto-generated.
func (p exposedModuleActionProps) Serialize() actionlog.Meta {
var (
m = make(actionlog.Meta)
)
if p.module != nil {
m.Set("module.ID", p.module.ID, true)
m.Set("module.ComposeNamespaceID", p.module.ComposeNamespaceID, true)
m.Set("module.ComposeModuleID", p.module.ComposeModuleID, true)
m.Set("module.NodeID", p.module.NodeID, true)
}
if p.update != nil {
m.Set("update.ID", p.update.ID, true)
m.Set("update.ComposeNamespaceID", p.update.ComposeNamespaceID, true)
m.Set("update.ComposeModuleID", p.update.ComposeModuleID, true)
m.Set("update.NodeID", p.update.NodeID, true)
}
if p.create != nil {
m.Set("create.ID", p.create.ID, true)
m.Set("create.ComposeNamespaceID", p.create.ComposeNamespaceID, true)
m.Set("create.ComposeModuleID", p.create.ComposeModuleID, true)
m.Set("create.NodeID", p.create.NodeID, true)
}
if p.delete != nil {
m.Set("delete.ID", p.delete.ID, true)
m.Set("delete.ComposeNamespaceID", p.delete.ComposeNamespaceID, true)
m.Set("delete.ComposeModuleID", p.delete.ComposeModuleID, true)
m.Set("delete.NodeID", p.delete.NodeID, true)
}
if p.filter != nil {
m.Set("filter.query", p.filter.Query, true)
m.Set("filter.sort", p.filter.Sort, true)
m.Set("filter.limit", p.filter.Limit, true)
}
if p.node != nil {
m.Set("node.ID", p.node.ID, true)
m.Set("node.Name", p.node.Name, true)
}
return m
}
// tr translates string and replaces meta value placeholder with values
//
// This function is auto-generated.
func (p exposedModuleActionProps) Format(in string, err error) string {
var (
pairs = []string{"{{err}}"}
// first non-empty string
fns = func(ii ...interface{}) string {
for _, i := range ii {
if s := fmt.Sprintf("%v", i); len(s) > 0 {
return s
}
}
return ""
}
)
if err != nil {
pairs = append(pairs, err.Error())
} else {
pairs = append(pairs, "nil")
}
if p.module != nil {
// replacement for "{{module}}" (in order how fields are defined)
pairs = append(
pairs,
"{{module}}",
fns(
p.module.ID,
p.module.ComposeNamespaceID,
p.module.ComposeModuleID,
p.module.NodeID,
),
)
pairs = append(pairs, "{{module.ID}}", fns(p.module.ID))
pairs = append(pairs, "{{module.ComposeNamespaceID}}", fns(p.module.ComposeNamespaceID))
pairs = append(pairs, "{{module.ComposeModuleID}}", fns(p.module.ComposeModuleID))
pairs = append(pairs, "{{module.NodeID}}", fns(p.module.NodeID))
}
if p.update != nil {
// replacement for "{{update}}" (in order how fields are defined)
pairs = append(
pairs,
"{{update}}",
fns(
p.update.ID,
p.update.ComposeNamespaceID,
p.update.ComposeModuleID,
p.update.NodeID,
),
)
pairs = append(pairs, "{{update.ID}}", fns(p.update.ID))
pairs = append(pairs, "{{update.ComposeNamespaceID}}", fns(p.update.ComposeNamespaceID))
pairs = append(pairs, "{{update.ComposeModuleID}}", fns(p.update.ComposeModuleID))
pairs = append(pairs, "{{update.NodeID}}", fns(p.update.NodeID))
}
if p.create != nil {
// replacement for "{{create}}" (in order how fields are defined)
pairs = append(
pairs,
"{{create}}",
fns(
p.create.ID,
p.create.ComposeNamespaceID,
p.create.ComposeModuleID,
p.create.NodeID,
),
)
pairs = append(pairs, "{{create.ID}}", fns(p.create.ID))
pairs = append(pairs, "{{create.ComposeNamespaceID}}", fns(p.create.ComposeNamespaceID))
pairs = append(pairs, "{{create.ComposeModuleID}}", fns(p.create.ComposeModuleID))
pairs = append(pairs, "{{create.NodeID}}", fns(p.create.NodeID))
}
if p.delete != nil {
// replacement for "{{delete}}" (in order how fields are defined)
pairs = append(
pairs,
"{{delete}}",
fns(
p.delete.ID,
p.delete.ComposeNamespaceID,
p.delete.ComposeModuleID,
p.delete.NodeID,
),
)
pairs = append(pairs, "{{delete.ID}}", fns(p.delete.ID))
pairs = append(pairs, "{{delete.ComposeNamespaceID}}", fns(p.delete.ComposeNamespaceID))
pairs = append(pairs, "{{delete.ComposeModuleID}}", fns(p.delete.ComposeModuleID))
pairs = append(pairs, "{{delete.NodeID}}", fns(p.delete.NodeID))
}
if p.filter != nil {
// replacement for "{{filter}}" (in order how fields are defined)
pairs = append(
pairs,
"{{filter}}",
fns(
p.filter.Query,
p.filter.Sort,
p.filter.Limit,
),
)
pairs = append(pairs, "{{filter.query}}", fns(p.filter.Query))
pairs = append(pairs, "{{filter.sort}}", fns(p.filter.Sort))
pairs = append(pairs, "{{filter.limit}}", fns(p.filter.Limit))
}
if p.node != nil {
// replacement for "{{node}}" (in order how fields are defined)
pairs = append(
pairs,
"{{node}}",
fns(
p.node.ID,
p.node.Name,
),
)
pairs = append(pairs, "{{node.ID}}", fns(p.node.ID))
pairs = append(pairs, "{{node.Name}}", fns(p.node.Name))
}
return strings.NewReplacer(pairs...).Replace(in)
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// Action methods
// String returns loggable description as string
//
// This function is auto-generated.
func (a *exposedModuleAction) String() string {
var props = &exposedModuleActionProps{}
if a.props != nil {
props = a.props
}
return props.Format(a.log, nil)
}
func (e *exposedModuleAction) ToAction() *actionlog.Action {
return &actionlog.Action{
Resource: e.resource,
Action: e.action,
Severity: e.severity,
Description: e.String(),
Meta: e.props.Serialize(),
}
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// Action constructors
// ExposedModuleActionSearch returns "federation:exposed_module.search" action
//
// This function is auto-generated.
func ExposedModuleActionSearch(props ...*exposedModuleActionProps) *exposedModuleAction {
a := &exposedModuleAction{
timestamp: time.Now(),
resource: "federation:exposed_module",
action: "search",
log: "searched for modules",
severity: actionlog.Info,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// ExposedModuleActionLookup returns "federation:exposed_module.lookup" action
//
// This function is auto-generated.
func ExposedModuleActionLookup(props ...*exposedModuleActionProps) *exposedModuleAction {
a := &exposedModuleAction{
timestamp: time.Now(),
resource: "federation:exposed_module",
action: "lookup",
log: "looked-up for a {{module}}",
severity: actionlog.Info,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// ExposedModuleActionCreate returns "federation:exposed_module.create" action
//
// This function is auto-generated.
func ExposedModuleActionCreate(props ...*exposedModuleActionProps) *exposedModuleAction {
a := &exposedModuleAction{
timestamp: time.Now(),
resource: "federation:exposed_module",
action: "create",
log: "created {{module}}",
severity: actionlog.Notice,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// ExposedModuleActionUpdate returns "federation:exposed_module.update" action
//
// This function is auto-generated.
func ExposedModuleActionUpdate(props ...*exposedModuleActionProps) *exposedModuleAction {
a := &exposedModuleAction{
timestamp: time.Now(),
resource: "federation:exposed_module",
action: "update",
log: "updated {{module}}",
severity: actionlog.Notice,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// ExposedModuleActionDelete returns "federation:exposed_module.delete" action
//
// This function is auto-generated.
func ExposedModuleActionDelete(props ...*exposedModuleActionProps) *exposedModuleAction {
a := &exposedModuleAction{
timestamp: time.Now(),
resource: "federation:exposed_module",
action: "delete",
log: "deleted {{module}}",
severity: actionlog.Notice,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// ExposedModuleActionUndelete returns "federation:exposed_module.undelete" action
//
// This function is auto-generated.
func ExposedModuleActionUndelete(props ...*exposedModuleActionProps) *exposedModuleAction {
a := &exposedModuleAction{
timestamp: time.Now(),
resource: "federation:exposed_module",
action: "undelete",
log: "undeleted {{module}}",
severity: actionlog.Notice,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// Error constructors
// ExposedModuleErrGeneric returns "federation:exposed_module.generic" as *errors.Error
//
// This function is auto-generated.
func ExposedModuleErrGeneric(mm ...*exposedModuleActionProps) *errors.Error {
var p = &exposedModuleActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("failed to complete request due to internal error", nil),
errors.Meta("type", "generic"),
errors.Meta("resource", "federation:exposed_module"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(exposedModuleLogMetaKey{}, "{err}"),
errors.Meta(exposedModulePropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
errors.Meta(locale.ErrorMetaKey{}, "exposed-module.errors.generic"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// ExposedModuleErrNotFound returns "federation:exposed_module.notFound" as *errors.Error
//
// This function is auto-generated.
func ExposedModuleErrNotFound(mm ...*exposedModuleActionProps) *errors.Error {
var p = &exposedModuleActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("module does not exist", nil),
errors.Meta("type", "notFound"),
errors.Meta("resource", "federation:exposed_module"),
errors.Meta(exposedModulePropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
errors.Meta(locale.ErrorMetaKey{}, "exposed-module.errors.notFound"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// ExposedModuleErrInvalidID returns "federation:exposed_module.invalidID" as *errors.Error
//
// This function is auto-generated.
func ExposedModuleErrInvalidID(mm ...*exposedModuleActionProps) *errors.Error {
var p = &exposedModuleActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("invalid ID", nil),
errors.Meta("type", "invalidID"),
errors.Meta("resource", "federation:exposed_module"),
errors.Meta(exposedModulePropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
errors.Meta(locale.ErrorMetaKey{}, "exposed-module.errors.invalidID"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// ExposedModuleErrStaleData returns "federation:exposed_module.staleData" as *errors.Error
//
// This function is auto-generated.
func ExposedModuleErrStaleData(mm ...*exposedModuleActionProps) *errors.Error {
var p = &exposedModuleActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("module was modified by someone else or by a workflow after you've opened it. Please refresh to see the latest updated version", nil),
errors.Meta("type", "staleData"),
errors.Meta("resource", "federation:exposed_module"),
errors.Meta(exposedModulePropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
errors.Meta(locale.ErrorMetaKey{}, "exposed-module.errors.staleData"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// ExposedModuleErrNotUnique returns "federation:exposed_module.notUnique" as *errors.Error
//
// This function is auto-generated.
func ExposedModuleErrNotUnique(mm ...*exposedModuleActionProps) *errors.Error {
var p = &exposedModuleActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("node not unique", nil),
errors.Meta("type", "notUnique"),
errors.Meta("resource", "federation:exposed_module"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(exposedModuleLogMetaKey{}, "used duplicate node TODO - {{module.NodeID}} for this compose module TODO - module.rel_compose_module"),
errors.Meta(exposedModulePropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
errors.Meta(locale.ErrorMetaKey{}, "exposed-module.errors.notUnique"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// ExposedModuleErrNodeNotFound returns "federation:exposed_module.nodeNotFound" as *errors.Error
//
// This function is auto-generated.
func ExposedModuleErrNodeNotFound(mm ...*exposedModuleActionProps) *errors.Error {
var p = &exposedModuleActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("node does not exist", nil),
errors.Meta("type", "nodeNotFound"),
errors.Meta("resource", "federation:exposed_module"),
errors.Meta(exposedModulePropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
errors.Meta(locale.ErrorMetaKey{}, "exposed-module.errors.nodeNotFound"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// ExposedModuleErrComposeModuleNotFound returns "federation:exposed_module.composeModuleNotFound" as *errors.Error
//
// This function is auto-generated.
func ExposedModuleErrComposeModuleNotFound(mm ...*exposedModuleActionProps) *errors.Error {
var p = &exposedModuleActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("compose module not found", nil),
errors.Meta("type", "composeModuleNotFound"),
errors.Meta("resource", "federation:exposed_module"),
errors.Meta(exposedModulePropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
errors.Meta(locale.ErrorMetaKey{}, "exposed-module.errors.composeModuleNotFound"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// ExposedModuleErrComposeNamespaceNotFound returns "federation:exposed_module.composeNamespaceNotFound" as *errors.Error
//
// This function is auto-generated.
func ExposedModuleErrComposeNamespaceNotFound(mm ...*exposedModuleActionProps) *errors.Error {
var p = &exposedModuleActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("compose namespace not found", nil),
errors.Meta("type", "composeNamespaceNotFound"),
errors.Meta("resource", "federation:exposed_module"),
errors.Meta(exposedModulePropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
errors.Meta(locale.ErrorMetaKey{}, "exposed-module.errors.composeNamespaceNotFound"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// ExposedModuleErrRequestParametersInvalid returns "federation:exposed_module.requestParametersInvalid" as *errors.Error
//
// This function is auto-generated.
func ExposedModuleErrRequestParametersInvalid(mm ...*exposedModuleActionProps) *errors.Error {
var p = &exposedModuleActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("request parameters invalid", nil),
errors.Meta("type", "requestParametersInvalid"),
errors.Meta("resource", "federation:exposed_module"),
errors.Meta(exposedModulePropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
errors.Meta(locale.ErrorMetaKey{}, "exposed-module.errors.requestParametersInvalid"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// ExposedModuleErrNotAllowedToCreate returns "federation:exposed_module.notAllowedToCreate" as *errors.Error
//
// This function is auto-generated.
func ExposedModuleErrNotAllowedToCreate(mm ...*exposedModuleActionProps) *errors.Error {
var p = &exposedModuleActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("not allowed to create modules", nil),
errors.Meta("type", "notAllowedToCreate"),
errors.Meta("resource", "federation:exposed_module"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(exposedModuleLogMetaKey{}, "could not create modules; insufficient permissions"),
errors.Meta(exposedModulePropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
errors.Meta(locale.ErrorMetaKey{}, "exposed-module.errors.notAllowedToCreate"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// ExposedModuleErrNotAllowedToManage returns "federation:exposed_module.notAllowedToManage" as *errors.Error
//
// This function is auto-generated.
func ExposedModuleErrNotAllowedToManage(mm ...*exposedModuleActionProps) *errors.Error {
var p = &exposedModuleActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("not allowed to manage this module", nil),
errors.Meta("type", "notAllowedToManage"),
errors.Meta("resource", "federation:exposed_module"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(exposedModuleLogMetaKey{}, "could not manage {{module}}; insufficient permissions"),
errors.Meta(exposedModulePropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
errors.Meta(locale.ErrorMetaKey{}, "exposed-module.errors.notAllowedToManage"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// recordAction is a service helper function wraps function that can return error
//
// It will wrap unrecognized/internal errors with generic errors.
//
// This function is auto-generated.
func (svc exposedModule) recordAction(ctx context.Context, props *exposedModuleActionProps, actionFn func(...*exposedModuleActionProps) *exposedModuleAction, err error) error {
if svc.actionlog == nil || actionFn == nil {
// action log disabled or no action fn passed, return error as-is
return err
} else if err == nil {
// action completed w/o error, record it
svc.actionlog.Record(ctx, actionFn(props).ToAction())
return nil
}
a := actionFn(props).ToAction()
// Extracting error information and recording it as action
a.Error = err.Error()
switch c := err.(type) {
case *errors.Error:
m := c.Meta()
a.Error = err.Error()
a.Severity = actionlog.Severity(m.AsInt("severity"))
a.Description = props.Format(m.AsString(exposedModuleLogMetaKey{}), err)
if p, has := m[exposedModulePropsMetaKey{}]; has {
a.Meta = p.(*exposedModuleActionProps).Serialize()
}
svc.actionlog.Record(ctx, a)
default:
svc.actionlog.Record(ctx, a)
}
// Original error is passed on
return err
}

View File

@@ -0,0 +1,97 @@
# List of loggable service actions
resource: federation:exposed_module
service: exposedModule
# Default sensitivity for actions
defaultActionSeverity: notice
# default severity for errors
defaultErrorSeverity: error
import:
- github.com/cortezaproject/corteza/server/federation/types
props:
- name: module
type: "*types.ExposedModule"
fields: [ ID, ComposeNamespaceID, ComposeModuleID, NodeID ]
- name: update
type: "*types.ExposedModule"
fields: [ ID, ComposeNamespaceID, ComposeModuleID, NodeID ]
- name: create
type: "*types.ExposedModule"
fields: [ ID, ComposeNamespaceID, ComposeModuleID, NodeID ]
- name: delete
type: "*types.ExposedModule"
fields: [ ID, ComposeNamespaceID, ComposeModuleID, NodeID ]
- name: filter
type: "*types.ExposedModuleFilter"
fields: [ query, sort, limit ]
- name: node
type: "*types.Node"
fields: [ ID, Name ]
actions:
- action: search
log: "searched for modules"
severity: info
- action: lookup
log: "looked-up for a {{module}}"
severity: info
- action: create
log: "created {{module}}"
- action: update
log: "updated {{module}}"
- action: delete
log: "deleted {{module}}"
- action: undelete
log: "undeleted {{module}}"
errors:
- error: notFound
message: "module does not exist"
severity: warning
- error: invalidID
message: "invalid ID"
severity: warning
- error: staleData
message: "module was modified by someone else or by a workflow after you've opened it. Please refresh to see the latest updated version"
severity: warning
- error: notUnique
message: "node not unique"
log: "used duplicate node TODO - {{module.NodeID}} for this compose module TODO - module.rel_compose_module"
severity: warning
- error: nodeNotFound
message: "node does not exist"
severity: warning
- error: composeModuleNotFound
message: "compose module not found"
severity: "warning"
- error: composeNamespaceNotFound
message: "compose namespace not found"
severity: "warning"
- error: requestParametersInvalid
message: "request parameters invalid"
severity: "warning"
- error: notAllowedToCreate
message: "not allowed to create modules"
log: "could not create modules; insufficient permissions"
- error: notAllowedToManage
message: "not allowed to manage this module"
log: "could not manage {{module}}; insufficient permissions"

View File

@@ -0,0 +1,54 @@
package service
import (
ct "github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/federation/types"
)
type (
Mapper struct{}
)
// Merge copies the values from originating structure
// do the destination
//
// mostly, there will be less mapped fields on the destination
// side, so start looping from here
func (m *Mapper) Merge(in *ct.RecordValueSet, out *ct.RecordValueSet, mappings *types.ModuleFieldMappingSet) {
var match *types.ModuleFieldMapping
for _, destVal := range *out {
// preset the value, since we're working with
// a pointer to an existing structure, could be some leftovers
destVal.Value = ""
// find origin field
if match, _ = mappings.FindByName(destVal.Name, types.ModuleFieldMappingSetFindTypeDestination); match == nil {
continue
}
// use the found origin, so the mapping is correct
for _, origVal := range *in {
if origVal.Name == match.Origin.Name {
destVal.Value = origVal.Value
break
}
}
}
return
}
// Prepare creates a set of Records to be used later
// when the fields will be mapped via Merge()
func (m *Mapper) Prepare(mappings types.ModuleFieldMappingSet) (out ct.RecordValueSet) {
for _, mm := range mappings {
rv := &ct.RecordValue{
Name: mm.Destination.Name,
Value: "",
}
out = append(out, rv)
}
return
}

View File

@@ -0,0 +1,97 @@
package service
import (
"encoding/json"
"testing"
ct "github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/federation/types"
"github.com/stretchr/testify/require"
)
func TestMapper_merge(t *testing.T) {
var (
tcc = []struct {
name string
m string
in ct.RecordValueSet
out ct.RecordValueSet
expect interface{}
}{
{
"merge_missing_destination_field",
`[{"origin":{"kind":"String","name":"Description","label":"Description","isMulti":false},"destination":{"kind":"String","name":"Name","label":"Description","isMulti":false}},{"origin":{"kind":"Url","name":"Facebook","label":"Facebook","isMulti":false},"destination":{"kind":"Url","name":"Fb","label":"Facebook","isMulti":false}}]`,
ct.RecordValueSet{&ct.RecordValue{Name: "Facebook", Value: "https://fb.com/user_1"}, &ct.RecordValue{Name: "Phone", Value: "000 111 222"}, &ct.RecordValue{Name: "Twitter", Value: "https://twitter.com/@russian_bot_0"}},
ct.RecordValueSet{&ct.RecordValue{Name: "Name", Value: ""}, &ct.RecordValue{Name: "Fb", Value: ""}},
ct.RecordValueSet{&ct.RecordValue{Name: "Name", Value: ""}, &ct.RecordValue{Name: "Fb", Value: "https://fb.com/user_1"}},
},
{
"merge_empty_origin",
`[{"origin":{"kind":"String","name":"Description","label":"Description","isMulti":false},"destination":{"kind":"String","name":"Name","label":"Description","isMulti":false}},{"origin":{"kind":"Url","name":"Facebook","label":"Facebook","isMulti":false},"destination":{"kind":"Url","name":"Fb","label":"Facebook","isMulti":false}}]`,
ct.RecordValueSet{},
ct.RecordValueSet{&ct.RecordValue{Name: "Name", Value: ""}, &ct.RecordValue{Name: "Fb", Value: ""}},
ct.RecordValueSet{&ct.RecordValue{Name: "Name", Value: ""}, &ct.RecordValue{Name: "Fb", Value: ""}},
},
{
"merge_empty_destination",
`[{"origin":{"kind":"String","name":"Description","label":"Description","isMulti":false},"destination":{"kind":"String","name":"Name","label":"Description","isMulti":false}},{"origin":{"kind":"Url","name":"Facebook","label":"Facebook","isMulti":false},"destination":{"kind":"Url","name":"Fb","label":"Facebook","isMulti":false}}]`,
ct.RecordValueSet{&ct.RecordValue{Name: "Facebook", Value: "https://fb.com/user_1"}, &ct.RecordValue{Name: "Phone", Value: "000 111 222"}, &ct.RecordValue{Name: "Twitter", Value: "https://twitter.com/@russian_bot_0"}},
ct.RecordValueSet{},
ct.RecordValueSet{},
},
}
)
for _, tc := range tcc {
t.Run(tc.name, func(t *testing.T) {
var (
req = require.New(t)
mapper = &Mapper{}
)
// dont catch any helper errors
mm := &types.ModuleFieldMappingSet{}
json.Unmarshal([]byte(tc.m), mm)
mapper.Merge(&tc.in, &tc.out, mm)
req.Equal(tc.out, tc.expect)
})
}
}
func TestMapper_prepare(t *testing.T) {
var (
tcc = []struct {
name string
m string
expect ct.RecordValueSet
}{
{
"prepare values",
`[
{"origin":{"kind":"String","name":"AccountName","label":"Account Name","isMulti":false},"destination":{"kind":"String","name":"AccountName","label":"Account Name","isMulti":false}},
{"origin":{"kind":"String","name":"Phone","label":"Phone","isMulti":false},"destination":{"kind":"String","name":"Phone","label":"Phone","isMulti":false}},
{"origin":{"kind":"Url","name":"Twitter","label":"Twitter","isMulti":false},"destination":{"kind":"Url","name":"Twitter","label":"Twitter","isMulti":false}}
]`,
ct.RecordValueSet{&ct.RecordValue{Name: "AccountName", Value: ""}, &ct.RecordValue{Name: "Phone", Value: ""}, &ct.RecordValue{Name: "Twitter", Value: ""}},
},
}
)
for _, tc := range tcc {
t.Run(tc.name, func(t *testing.T) {
var (
req = require.New(t)
mapper = &Mapper{}
)
// dont catch any helper errors
mm := &types.ModuleFieldMappingSet{}
json.Unmarshal([]byte(tc.m), mm)
out := mapper.Prepare(*mm)
req.Equal(tc.expect, out)
})
}
}

View File

@@ -0,0 +1,219 @@
package service
import (
"context"
cs "github.com/cortezaproject/corteza/server/compose/service"
ct "github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/federation/types"
"github.com/cortezaproject/corteza/server/pkg/actionlog"
"github.com/cortezaproject/corteza/server/store"
)
type (
moduleMapping struct {
store store.Storer
node node
ac moduleMappingAccessController
module cs.ModuleService
smodule SharedModuleService
namespace cs.NamespaceService
actionlog actionlog.Recorder
}
moduleMappingAccessController interface {
CanMapSharedModule(ctx context.Context, r *types.SharedModule) bool
}
ModuleMappingService interface {
Find(ctx context.Context, filter types.ModuleMappingFilter) (types.ModuleMappingSet, types.ModuleMappingFilter, error)
FindByID(ctx context.Context, federationModuleID uint64) (*types.ModuleMapping, error)
Create(ctx context.Context, new *types.ModuleMapping) (*types.ModuleMapping, error)
Update(ctx context.Context, updated *types.ModuleMapping) (*types.ModuleMapping, error)
}
moduleMappingUpdateHandler func(ctx context.Context, c *types.ModuleMapping) (bool, bool, error)
)
func ModuleMapping() *moduleMapping {
return &moduleMapping{
ac: DefaultAccessControl,
node: *DefaultNode,
store: DefaultStore,
actionlog: DefaultActionlog,
smodule: DefaultSharedModule,
module: cs.DefaultModule,
namespace: cs.DefaultNamespace,
}
}
func (svc moduleMapping) FindByID(ctx context.Context, federationModuleID uint64) (mm *types.ModuleMapping, err error) {
err = func() error {
var (
sm *types.SharedModule
)
if mm, err = store.LookupFederationModuleMappingByFederationModuleID(ctx, svc.store, federationModuleID); err != nil {
return err
}
// fetch shared module for access check
if sm, err = svc.smodule.FindByID(ctx, mm.NodeID, federationModuleID); err != nil {
return err
}
if !svc.ac.CanMapSharedModule(ctx, sm) {
return ModuleMappingErrNotAllowedToMap()
}
return nil
}()
return
}
func (svc moduleMapping) Find(ctx context.Context, filter types.ModuleMappingFilter) (set types.ModuleMappingSet, f types.ModuleMappingFilter, err error) {
// @todo - optimise this access check
filter.Check = func(res *types.ModuleMapping) (bool, error) {
// fetch shared module for this
sm, err := svc.smodule.FindByID(ctx, res.NodeID, res.FederationModuleID)
if err != nil {
return false, err
}
if !svc.ac.CanMapSharedModule(ctx, sm) {
return false, ModuleMappingErrNotAllowedToMap()
}
return true, nil
}
err = func() error {
if set, f, err = store.SearchFederationModuleMappings(ctx, svc.store, filter); err != nil {
return err
}
return nil
}()
set.Walk(func(mm *types.ModuleMapping) error {
mm.NodeID = f.NodeID
return nil
})
return
}
func (svc moduleMapping) Create(ctx context.Context, new *types.ModuleMapping) (*types.ModuleMapping, error) {
var (
aProps = &moduleMappingActionProps{created: new}
)
err := store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) {
var (
m *ct.Module
sm *types.SharedModule
)
if _, err := svc.namespace.FindByID(ctx, new.ComposeNamespaceID); err != nil {
return ModuleMappingErrComposeNamespaceNotFound()
}
if _, err = svc.node.FindByID(ctx, new.NodeID); err != nil {
return ModuleMappingErrNodeNotFound()
}
// Check for federation module - compose.Module combo
if err = svc.uniqueCheck(ctx, new); err != nil {
return err
}
if m, err = svc.module.FindByID(ctx, new.ComposeNamespaceID, new.ComposeModuleID); err != nil {
return ModuleMappingErrComposeModuleNotFound()
}
if sm, err = svc.smodule.FindByID(ctx, new.NodeID, new.FederationModuleID); err != nil {
return err
}
if !svc.ac.CanMapSharedModule(ctx, sm) {
return ModuleMappingErrNotAllowedToMap()
}
if err = store.CreateFederationModuleMapping(ctx, s, new); err != nil {
return err
}
// set labels
AddFederationLabel(m, "federation", "")
if _, err := svc.module.Update(ctx, m); err != nil {
return err
}
return nil
})
return new, svc.recordAction(ctx, aProps, ModuleMappingActionCreate, err)
}
func (svc moduleMapping) Update(ctx context.Context, updated *types.ModuleMapping) (*types.ModuleMapping, error) {
var (
aProps = &moduleMappingActionProps{changed: updated}
)
err := store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) {
var (
m *ct.Module
sm *types.SharedModule
)
if _, err := svc.namespace.FindByID(ctx, updated.ComposeNamespaceID); err != nil {
return ModuleMappingErrComposeNamespaceNotFound()
}
if m, err = svc.module.FindByID(ctx, updated.ComposeNamespaceID, updated.ComposeModuleID); err != nil {
return ModuleMappingErrComposeModuleNotFound()
}
if sm, err = svc.smodule.FindByID(ctx, updated.NodeID, updated.FederationModuleID); err != nil {
return err
}
if !svc.ac.CanMapSharedModule(ctx, sm) {
return ModuleMappingErrNotAllowedToMap()
}
if err = store.UpdateFederationModuleMapping(ctx, s, updated); err != nil {
return err
}
// set labels
AddFederationLabel(m, "federation", "")
if _, err := svc.module.Update(ctx, m); err != nil {
return err
}
return nil
})
return updated, svc.recordAction(ctx, aProps, ModuleMappingActionUpdate, err)
}
func (svc moduleMapping) uniqueCheck(ctx context.Context, m *types.ModuleMapping) (err error) {
f := types.ModuleMappingFilter{
FederationModuleID: m.FederationModuleID,
ComposeModuleID: m.ComposeModuleID,
ComposeNamespaceID: m.ComposeNamespaceID,
}
if set, _, err := store.SearchFederationModuleMappings(ctx, svc.store, f); len(set) > 0 && err == nil {
return ModuleMappingErrModuleMappingExists()
} else if err != nil {
return err
}
return err
}

View File

@@ -0,0 +1,632 @@
package service
// 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/service/module_mapping_actions.yaml
import (
"context"
"fmt"
"github.com/cortezaproject/corteza/server/federation/types"
"github.com/cortezaproject/corteza/server/pkg/actionlog"
"github.com/cortezaproject/corteza/server/pkg/errors"
"github.com/cortezaproject/corteza/server/pkg/locale"
"strings"
"time"
)
type (
moduleMappingActionProps struct {
created *types.ModuleMapping
mapping *types.ModuleMapping
changed *types.ModuleMapping
filter *types.ModuleMappingFilter
}
moduleMappingAction struct {
timestamp time.Time
resource string
action string
log string
severity actionlog.Severity
// prefix for error when action fails
errorMessage string
props *moduleMappingActionProps
}
moduleMappingLogMetaKey struct{}
moduleMappingPropsMetaKey struct{}
)
var (
// just a placeholder to cover template cases w/o fmt package use
_ = fmt.Println
)
// *********************************************************************************************************************
// *********************************************************************************************************************
// Props methods
// setCreated updates moduleMappingActionProps's created
//
// This function is auto-generated.
func (p *moduleMappingActionProps) setCreated(created *types.ModuleMapping) *moduleMappingActionProps {
p.created = created
return p
}
// setMapping updates moduleMappingActionProps's mapping
//
// This function is auto-generated.
func (p *moduleMappingActionProps) setMapping(mapping *types.ModuleMapping) *moduleMappingActionProps {
p.mapping = mapping
return p
}
// setChanged updates moduleMappingActionProps's changed
//
// This function is auto-generated.
func (p *moduleMappingActionProps) setChanged(changed *types.ModuleMapping) *moduleMappingActionProps {
p.changed = changed
return p
}
// setFilter updates moduleMappingActionProps's filter
//
// This function is auto-generated.
func (p *moduleMappingActionProps) setFilter(filter *types.ModuleMappingFilter) *moduleMappingActionProps {
p.filter = filter
return p
}
// Serialize converts moduleMappingActionProps to actionlog.Meta
//
// This function is auto-generated.
func (p moduleMappingActionProps) Serialize() actionlog.Meta {
var (
m = make(actionlog.Meta)
)
if p.created != nil {
m.Set("created.FederationModuleID", p.created.FederationModuleID, true)
m.Set("created.ComposeModuleID", p.created.ComposeModuleID, true)
}
if p.mapping != nil {
m.Set("mapping.FederationModuleID", p.mapping.FederationModuleID, true)
m.Set("mapping.ComposeModuleID", p.mapping.ComposeModuleID, true)
}
if p.changed != nil {
m.Set("changed.FederationModuleID", p.changed.FederationModuleID, true)
m.Set("changed.ComposeModuleID", p.changed.ComposeModuleID, true)
}
if p.filter != nil {
m.Set("filter.query", p.filter.Query, true)
m.Set("filter.sort", p.filter.Sort, true)
m.Set("filter.limit", p.filter.Limit, true)
}
return m
}
// tr translates string and replaces meta value placeholder with values
//
// This function is auto-generated.
func (p moduleMappingActionProps) Format(in string, err error) string {
var (
pairs = []string{"{{err}}"}
// first non-empty string
fns = func(ii ...interface{}) string {
for _, i := range ii {
if s := fmt.Sprintf("%v", i); len(s) > 0 {
return s
}
}
return ""
}
)
if err != nil {
pairs = append(pairs, err.Error())
} else {
pairs = append(pairs, "nil")
}
if p.created != nil {
// replacement for "{{created}}" (in order how fields are defined)
pairs = append(
pairs,
"{{created}}",
fns(
p.created.FederationModuleID,
p.created.ComposeModuleID,
),
)
pairs = append(pairs, "{{created.FederationModuleID}}", fns(p.created.FederationModuleID))
pairs = append(pairs, "{{created.ComposeModuleID}}", fns(p.created.ComposeModuleID))
}
if p.mapping != nil {
// replacement for "{{mapping}}" (in order how fields are defined)
pairs = append(
pairs,
"{{mapping}}",
fns(
p.mapping.FederationModuleID,
p.mapping.ComposeModuleID,
),
)
pairs = append(pairs, "{{mapping.FederationModuleID}}", fns(p.mapping.FederationModuleID))
pairs = append(pairs, "{{mapping.ComposeModuleID}}", fns(p.mapping.ComposeModuleID))
}
if p.changed != nil {
// replacement for "{{changed}}" (in order how fields are defined)
pairs = append(
pairs,
"{{changed}}",
fns(
p.changed.FederationModuleID,
p.changed.ComposeModuleID,
),
)
pairs = append(pairs, "{{changed.FederationModuleID}}", fns(p.changed.FederationModuleID))
pairs = append(pairs, "{{changed.ComposeModuleID}}", fns(p.changed.ComposeModuleID))
}
if p.filter != nil {
// replacement for "{{filter}}" (in order how fields are defined)
pairs = append(
pairs,
"{{filter}}",
fns(
p.filter.Query,
p.filter.Sort,
p.filter.Limit,
),
)
pairs = append(pairs, "{{filter.query}}", fns(p.filter.Query))
pairs = append(pairs, "{{filter.sort}}", fns(p.filter.Sort))
pairs = append(pairs, "{{filter.limit}}", fns(p.filter.Limit))
}
return strings.NewReplacer(pairs...).Replace(in)
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// Action methods
// String returns loggable description as string
//
// This function is auto-generated.
func (a *moduleMappingAction) String() string {
var props = &moduleMappingActionProps{}
if a.props != nil {
props = a.props
}
return props.Format(a.log, nil)
}
func (e *moduleMappingAction) ToAction() *actionlog.Action {
return &actionlog.Action{
Resource: e.resource,
Action: e.action,
Severity: e.severity,
Description: e.String(),
Meta: e.props.Serialize(),
}
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// Action constructors
// ModuleMappingActionSearch returns "federation:module_mapping.search" action
//
// This function is auto-generated.
func ModuleMappingActionSearch(props ...*moduleMappingActionProps) *moduleMappingAction {
a := &moduleMappingAction{
timestamp: time.Now(),
resource: "federation:module_mapping",
action: "search",
log: "searched for modules",
severity: actionlog.Info,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// ModuleMappingActionLookup returns "federation:module_mapping.lookup" action
//
// This function is auto-generated.
func ModuleMappingActionLookup(props ...*moduleMappingActionProps) *moduleMappingAction {
a := &moduleMappingAction{
timestamp: time.Now(),
resource: "federation:module_mapping",
action: "lookup",
log: "looked-up for a module",
severity: actionlog.Info,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// ModuleMappingActionCreate returns "federation:module_mapping.create" action
//
// This function is auto-generated.
func ModuleMappingActionCreate(props ...*moduleMappingActionProps) *moduleMappingAction {
a := &moduleMappingAction{
timestamp: time.Now(),
resource: "federation:module_mapping",
action: "create",
log: "created module",
severity: actionlog.Notice,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// ModuleMappingActionUpdate returns "federation:module_mapping.update" action
//
// This function is auto-generated.
func ModuleMappingActionUpdate(props ...*moduleMappingActionProps) *moduleMappingAction {
a := &moduleMappingAction{
timestamp: time.Now(),
resource: "federation:module_mapping",
action: "update",
log: "updated module",
severity: actionlog.Notice,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// ModuleMappingActionDelete returns "federation:module_mapping.delete" action
//
// This function is auto-generated.
func ModuleMappingActionDelete(props ...*moduleMappingActionProps) *moduleMappingAction {
a := &moduleMappingAction{
timestamp: time.Now(),
resource: "federation:module_mapping",
action: "delete",
log: "deleted module",
severity: actionlog.Notice,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// Error constructors
// ModuleMappingErrGeneric returns "federation:module_mapping.generic" as *errors.Error
//
// This function is auto-generated.
func ModuleMappingErrGeneric(mm ...*moduleMappingActionProps) *errors.Error {
var p = &moduleMappingActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("failed to complete request due to internal error", nil),
errors.Meta("type", "generic"),
errors.Meta("resource", "federation:module_mapping"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(moduleMappingLogMetaKey{}, "{err}"),
errors.Meta(moduleMappingPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
errors.Meta(locale.ErrorMetaKey{}, "module-mapping.errors.generic"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// ModuleMappingErrNotFound returns "federation:module_mapping.notFound" as *errors.Error
//
// This function is auto-generated.
func ModuleMappingErrNotFound(mm ...*moduleMappingActionProps) *errors.Error {
var p = &moduleMappingActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("module mapping does not exist", nil),
errors.Meta("type", "notFound"),
errors.Meta("resource", "federation:module_mapping"),
errors.Meta(moduleMappingPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
errors.Meta(locale.ErrorMetaKey{}, "module-mapping.errors.notFound"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// ModuleMappingErrComposeModuleNotFound returns "federation:module_mapping.composeModuleNotFound" as *errors.Error
//
// This function is auto-generated.
func ModuleMappingErrComposeModuleNotFound(mm ...*moduleMappingActionProps) *errors.Error {
var p = &moduleMappingActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("compose module not found", nil),
errors.Meta("type", "composeModuleNotFound"),
errors.Meta("resource", "federation:module_mapping"),
errors.Meta(moduleMappingPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
errors.Meta(locale.ErrorMetaKey{}, "module-mapping.errors.composeModuleNotFound"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// ModuleMappingErrComposeNamespaceNotFound returns "federation:module_mapping.composeNamespaceNotFound" as *errors.Error
//
// This function is auto-generated.
func ModuleMappingErrComposeNamespaceNotFound(mm ...*moduleMappingActionProps) *errors.Error {
var p = &moduleMappingActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("compose namespace not found", nil),
errors.Meta("type", "composeNamespaceNotFound"),
errors.Meta("resource", "federation:module_mapping"),
errors.Meta(moduleMappingPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
errors.Meta(locale.ErrorMetaKey{}, "module-mapping.errors.composeNamespaceNotFound"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// ModuleMappingErrFederationModuleNotFound returns "federation:module_mapping.federationModuleNotFound" as *errors.Error
//
// This function is auto-generated.
func ModuleMappingErrFederationModuleNotFound(mm ...*moduleMappingActionProps) *errors.Error {
var p = &moduleMappingActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("federation module not found", nil),
errors.Meta("type", "federationModuleNotFound"),
errors.Meta("resource", "federation:module_mapping"),
errors.Meta(moduleMappingPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
errors.Meta(locale.ErrorMetaKey{}, "module-mapping.errors.federationModuleNotFound"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// ModuleMappingErrNodeNotFound returns "federation:module_mapping.nodeNotFound" as *errors.Error
//
// This function is auto-generated.
func ModuleMappingErrNodeNotFound(mm ...*moduleMappingActionProps) *errors.Error {
var p = &moduleMappingActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("node does not exist", nil),
errors.Meta("type", "nodeNotFound"),
errors.Meta("resource", "federation:module_mapping"),
errors.Meta(moduleMappingPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
errors.Meta(locale.ErrorMetaKey{}, "module-mapping.errors.nodeNotFound"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// ModuleMappingErrModuleMappingExists returns "federation:module_mapping.moduleMappingExists" as *errors.Error
//
// This function is auto-generated.
func ModuleMappingErrModuleMappingExists(mm ...*moduleMappingActionProps) *errors.Error {
var p = &moduleMappingActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("module mapping already exists", nil),
errors.Meta("type", "moduleMappingExists"),
errors.Meta("resource", "federation:module_mapping"),
errors.Meta(moduleMappingPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
errors.Meta(locale.ErrorMetaKey{}, "module-mapping.errors.moduleMappingExists"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// ModuleMappingErrNotAllowedToMap returns "federation:module_mapping.notAllowedToMap" as *errors.Error
//
// This function is auto-generated.
func ModuleMappingErrNotAllowedToMap(mm ...*moduleMappingActionProps) *errors.Error {
var p = &moduleMappingActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("not allowed to map this module", nil),
errors.Meta("type", "notAllowedToMap"),
errors.Meta("resource", "federation:module_mapping"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(moduleMappingLogMetaKey{}, "could not manage mapping; insufficient permissions"),
errors.Meta(moduleMappingPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
errors.Meta(locale.ErrorMetaKey{}, "module-mapping.errors.notAllowedToMap"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// recordAction is a service helper function wraps function that can return error
//
// It will wrap unrecognized/internal errors with generic errors.
//
// This function is auto-generated.
func (svc moduleMapping) recordAction(ctx context.Context, props *moduleMappingActionProps, actionFn func(...*moduleMappingActionProps) *moduleMappingAction, err error) error {
if svc.actionlog == nil || actionFn == nil {
// action log disabled or no action fn passed, return error as-is
return err
} else if err == nil {
// action completed w/o error, record it
svc.actionlog.Record(ctx, actionFn(props).ToAction())
return nil
}
a := actionFn(props).ToAction()
// Extracting error information and recording it as action
a.Error = err.Error()
switch c := err.(type) {
case *errors.Error:
m := c.Meta()
a.Error = err.Error()
a.Severity = actionlog.Severity(m.AsInt("severity"))
a.Description = props.Format(m.AsString(moduleMappingLogMetaKey{}), err)
if p, has := m[moduleMappingPropsMetaKey{}]; has {
a.Meta = p.(*moduleMappingActionProps).Serialize()
}
svc.actionlog.Record(ctx, a)
default:
svc.actionlog.Record(ctx, a)
}
// Original error is passed on
return err
}

View File

@@ -0,0 +1,74 @@
# List of loggable service actions
resource: federation:module_mapping
service: moduleMapping
# Default sensitivity for actions
defaultActionSeverity: notice
# default severity for errors
defaultErrorSeverity: error
import:
- github.com/cortezaproject/corteza/server/federation/types
props:
- name: created
type: "*types.ModuleMapping"
fields: [ FederationModuleID, ComposeModuleID ]
- name: mapping
type: "*types.ModuleMapping"
fields: [ FederationModuleID, ComposeModuleID ]
- name: changed
type: "*types.ModuleMapping"
fields: [ FederationModuleID, ComposeModuleID ]
- name: filter
type: "*types.ModuleMappingFilter"
fields: [ query, sort, limit ]
actions:
- action: search
log: "searched for modules"
severity: info
- action: lookup
log: "looked-up for a module"
severity: info
- action: create
log: "created module"
- action: update
log: "updated module"
- action: delete
log: "deleted module"
errors:
- error: notFound
message: "module mapping does not exist"
severity: warning
- error: composeModuleNotFound
message: "compose module not found"
severity: "warning"
- error: composeNamespaceNotFound
message: "compose namespace not found"
severity: "warning"
- error: federationModuleNotFound
message: "federation module not found"
severity: "warning"
- error: nodeNotFound
message: "node does not exist"
severity: warning
- error: moduleMappingExists
message: "module mapping already exists"
# severity: "warning"
- error: notAllowedToMap
message: "not allowed to map this module"
log: "could not manage mapping; insufficient permissions"

View File

@@ -0,0 +1,602 @@
package service
import (
"context"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/cortezaproject/corteza/server/pkg/errors"
"github.com/cortezaproject/corteza/server/federation/types"
"github.com/cortezaproject/corteza/server/pkg/actionlog"
"github.com/cortezaproject/corteza/server/pkg/auth"
"github.com/cortezaproject/corteza/server/pkg/options"
"github.com/cortezaproject/corteza/server/pkg/rand"
"github.com/cortezaproject/corteza/server/store"
"github.com/cortezaproject/corteza/server/system/service"
sysTypes "github.com/cortezaproject/corteza/server/system/types"
)
const (
TokenLength = 32
)
type (
tokenIssuer func(context.Context, auth.Identifiable) (token []byte, err error)
node struct {
store store.Storer
sysUser service.UserService
actionlog actionlog.Recorder
tokenIssuer tokenIssuer
name string
host string
baseURL string
handshaker nodeHandshaker
ac nodeAccessController
}
nodeAccessController interface {
CanPair(ctx context.Context) bool
CanSearchNodes(ctx context.Context) bool
CanCreateNode(ctx context.Context) bool
CanManageNode(ctx context.Context, r *types.Node) bool
}
nodeUpdateHandler func(ctx context.Context, n *types.Node) error
nodeHandshaker interface {
Init(ctx context.Context, n *types.Node, authToken string) error
Complete(ctx context.Context, n *types.Node, authToken string) error
}
)
func Node(s store.Storer, u service.UserService, al actionlog.Recorder, th tokenIssuer, options options.FederationOpt, sopt options.HttpServerOpt, ac nodeAccessController) *node {
return &node{
store: s,
sysUser: u,
actionlog: al,
tokenIssuer: th,
ac: ac,
name: options.Label,
host: options.Host,
baseURL: fmt.Sprintf("%s/federation", strings.TrimRight(sopt.ApiBaseUrl, "/")),
handshaker: HttpHandshake(http.DefaultClient),
}
}
func (svc *node) SetHandshaker(h nodeHandshaker) {
svc.handshaker = h
}
func (svc node) Search(ctx context.Context, filter types.NodeFilter) (set types.NodeSet, f types.NodeFilter, err error) {
var (
aProps = &nodeActionProps{filter: &filter}
)
filter.Check = func(res *types.Node) (bool, error) {
if !svc.ac.CanManageNode(ctx, res) {
return false, NodeErrNotAllowedToManage()
}
return true, nil
}
err = func() error {
if !svc.ac.CanSearchNodes(ctx) {
return NodeErrNotAllowedToSearch()
}
if set, f, err = store.SearchFederationNodes(ctx, svc.store, filter); err != nil {
return err
}
return nil
}()
return set, f, svc.recordAction(ctx, aProps, NodeActionSearch, err)
}
// Create is used on server A to create federation with server B
func (svc node) Create(ctx context.Context, new *types.Node) (*types.Node, error) {
var (
aProps = &nodeActionProps{}
n *types.Node
err error
)
// @todo verify new.baseURL; if it's set it must be valid URL that returns 200
err = store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) {
n = &types.Node{
ID: nextID(),
Name: new.Name,
BaseURL: new.BaseURL,
Contact: new.Contact,
Status: types.NodeStatusPending,
CreatedAt: *now(),
}
if !svc.ac.CanCreateNode(ctx) {
return NodeErrNotAllowedToCreate()
}
return store.CreateFederationNode(ctx, s, n)
})
return n, svc.recordAction(ctx, aProps, NodeActionCreate, err)
}
// Read is used mainly in UI, when retrieving details about the node
func (svc node) Read(ctx context.Context, ID uint64) (*types.Node, error) {
var (
n, err = loadNode(ctx, svc.store, ID)
aProps = &nodeActionProps{node: n}
)
// permission takes precedence over any db error
if n != nil && !svc.ac.CanManageNode(ctx, n) {
err = NodeErrNotAllowedToManage()
}
return n, svc.recordAction(ctx, aProps, NodeActionCreate, err)
}
// CreateFromURI is used on server B to create federation with server A
func (svc node) CreateFromPairingURI(ctx context.Context, uri string) (n *types.Node, err error) {
var (
existing *types.Node
aProps = &nodeActionProps{pairingURI: uri}
)
err = func(ctx context.Context) error {
if !svc.ac.CanPair(ctx) {
return NodeErrNotAllowedToPair()
}
n, err = svc.decodePairingURI(uri)
return err
}(ctx)
if err != nil {
return nil, svc.recordAction(ctx, aProps, NodeActionCreateFromPairingURI, err)
}
existing, err = store.LookupFederationNodeByBaseURLSharedNodeID(ctx, svc.store, n.BaseURL, n.SharedNodeID)
aProps.setNode(existing)
if errors.Is(err, store.ErrNotFound) {
if !svc.ac.CanCreateNode(ctx) {
return n, svc.recordAction(ctx, aProps, NodeActionCreateFromPairingURI, NodeErrNotAllowedToCreate())
}
n.ID = nextID()
n.CreatedAt = *now()
n.UpdatedBy = auth.GetIdentityFromContext(ctx).Identity()
n.Status = types.NodeStatusPending
err = store.CreateFederationNode(ctx, svc.store, n)
return n, svc.recordAction(ctx, aProps, NodeActionCreateFromPairingURI, err)
} else if err != nil {
return nil, svc.recordAction(ctx, aProps, NodeActionCreateFromPairingURI, err)
} else {
if !svc.ac.CanManageNode(ctx, n) {
return n, svc.recordAction(ctx, aProps, NodeActionCreateFromPairingURI, NodeErrNotAllowedToManage())
}
// Node with the same sharing ID and domain already exists
// so we'll reset status and tokens
existing.Status = types.NodeStatusPending
// Remove existing auth token
existing.AuthToken = ""
// Reset pairing token
existing.PairToken = n.PairToken
existing.UpdatedBy = auth.GetIdentityFromContext(ctx).Identity()
existing.UpdatedAt = now()
err = store.UpdateFederationNode(ctx, svc.store, n)
return n, svc.recordAction(ctx, aProps, NodeActionRecreateFromPairingURI, err)
}
}
// RegenerateNodeURI loads and updates node with the new OTT and returns sharable link
func (svc node) RegenerateNodeURI(ctx context.Context, nodeID uint64) (string, error) {
var (
uri string
)
_, err := svc.updater(
ctx,
nodeID,
NodeActionOttRegenerated,
func(ctx context.Context, n *types.Node) error {
n.PairToken = string(rand.Bytes(TokenLength))
return nil
},
func(ctx context.Context, n *types.Node) error {
uri = svc.makePairingURI(n)
return nil
},
)
if err != nil {
return "", err
}
return uri, nil
}
func (svc node) Update(ctx context.Context, upd *types.Node) (*types.Node, error) {
return svc.updater(ctx, upd.ID, NodeActionUpdate, func(ctx context.Context, n *types.Node) error {
n.Name = upd.Name
n.BaseURL = upd.BaseURL
n.Contact = upd.Contact
n.UpdatedBy = auth.GetIdentityFromContext(ctx).Identity()
n.UpdatedAt = now()
return nil
}, nil)
}
func (svc node) DeleteByID(ctx context.Context, ID uint64) error {
_, err := svc.updater(ctx, ID, NodeActionDelete, func(ctx context.Context, n *types.Node) error {
n.DeletedAt = now()
n.DeletedBy = auth.GetIdentityFromContext(ctx).Identity()
return nil
}, nil)
return err
}
func (svc node) UndeleteByID(ctx context.Context, ID uint64) error {
_, err := svc.updater(ctx, ID, NodeActionUndelete, func(ctx context.Context, n *types.Node) error {
n.DeletedAt = nil
n.DeletedBy = 0
return nil
}, nil)
return err
}
// Pair is used on server B to send request to server A
func (svc node) Pair(ctx context.Context, nodeID uint64) error {
if !svc.ac.CanPair(ctx) {
return NodeErrNotAllowedToPair()
}
// elevate permissions for user lookup & creation!
ctx = auth.SetIdentityToContext(ctx, auth.FederationUser())
_, err := svc.updater(
ctx,
nodeID,
NodeActionPair,
func(ctx context.Context, n *types.Node) error {
// Handle federated user
u, err := svc.fetchFederatedUser(ctx, n)
if err != nil {
return err
}
var accessToken []byte
// Generate JWT token for the federated user
accessToken, err = svc.tokenIssuer(ctx, u)
if err != nil {
return err
}
n.UpdatedBy = auth.GetIdentityFromContext(ctx).Identity()
n.UpdatedAt = now()
// Start handshake initialization remote node
if err = svc.handshaker.Init(ctx, n, string(accessToken)); err != nil {
return err
}
n.Status = types.NodeStatusPairRequested
return nil
},
nil,
)
return err
}
// HandshakeInit is used on server A to handle pairing request (see Pair fn above) from server B
func (svc node) HandshakeInit(ctx context.Context, nodeID uint64, pairToken string, sharedNodeID uint64, authToken string) error {
// if !svc.ac.CanPair(ctx) {
// return NodeErrNotAllowedToPair()
// }
_, err := svc.updater(
ctx,
sharedNodeID,
NodeActionHandshakeInit,
func(ctx context.Context, n *types.Node) error {
// @todo need to check node status before we can proceed with initialization
if n.PairToken != pairToken {
return NodeErrPairingTokenInvalid()
}
n.SharedNodeID = sharedNodeID
n.AuthToken = authToken
n.UpdatedBy = auth.GetIdentityFromContext(ctx).Identity()
n.UpdatedAt = now()
n.Status = types.NodeStatusPairRequested
return nil
},
func(ctx context.Context, n *types.Node) error {
// @todo notify the node administrator about the request
return nil
},
)
return err
}
// HandshakeConfirm is used by server A to manually confirm the handshake
func (svc node) HandshakeConfirm(ctx context.Context, nodeID uint64) error {
if !svc.ac.CanPair(ctx) {
return NodeErrNotAllowedToPair()
}
_, err := svc.updater(ctx, nodeID, NodeActionHandshakeConfirm, func(ctx context.Context, n *types.Node) error {
// Handle federated user
u, err := svc.fetchFederatedUser(ctx, n)
if err != nil {
return err
}
// Generate JWT token for the federated user
var accessToken []byte
if accessToken, err = svc.tokenIssuer(ctx, u); err != nil {
return fmt.Errorf("could not confirm handshake: %w", err)
}
n.UpdatedBy = auth.GetIdentityFromContext(ctx).Identity()
n.UpdatedAt = now()
// Complete handshake on remote node
if err = svc.handshaker.Complete(ctx, n, string(accessToken)); err != nil {
return err
}
n.Status = types.NodeStatusPaired
return nil
}, nil)
return err
}
// HandshakeComplete is used by server B to handle handshake confirmation
func (svc node) HandshakeComplete(ctx context.Context, sharedNodeID uint64, token string) error {
var (
n *types.Node
err error
)
if !svc.ac.CanPair(ctx) {
return NodeErrNotAllowedToPair()
}
if n, err = store.LookupFederationNodeBySharedNodeID(ctx, svc.store, sharedNodeID); err != nil {
return err
}
_, err = svc.updater(ctx, n.ID, NodeActionHandshakeComplete, func(ctx context.Context, n *types.Node) error {
n.AuthToken = token
n.Status = types.NodeStatusPaired
n.UpdatedBy = auth.GetIdentityFromContext(ctx).Identity()
n.UpdatedAt = now()
return nil
}, nil)
return err
}
func (svc node) updater(ctx context.Context, nodeID uint64, action func(...*nodeActionProps) *nodeAction, fn, afterFn nodeUpdateHandler) (*types.Node, error) {
var (
err error
n *types.Node
aProps = &nodeActionProps{node: &types.Node{ID: nodeID}}
)
err = func() error {
n, err = loadNode(ctx, svc.store, nodeID)
if err != nil {
return err
}
aProps.setNode(n)
if err = fn(ctx, n); err != nil {
n.Status = types.NodeStatusFailed
// persist the node status change
if updateErr := store.UpdateFederationNode(ctx, svc.store, n); updateErr != nil {
return updateErr
}
return err
}
// any other type of access check
// should be done in fn() before
// if !svc.ac.CanManageNode(ctx, n) {
// return NodeErrNotAllowedToManage()
// }
if err = store.UpdateFederationNode(ctx, svc.store, n); err != nil {
return err
}
if afterFn != nil {
if err = afterFn(ctx, n); err != nil {
return err
}
}
return nil
}()
return n, svc.recordAction(ctx, aProps, action, err)
}
func (svc node) FindBySharedNodeID(ctx context.Context, sharedNodeID uint64) (*types.Node, error) {
n, err := store.LookupFederationNodeBySharedNodeID(ctx, svc.store, sharedNodeID)
if n != nil && !svc.ac.CanManageNode(ctx, n) {
return nil, NodeErrNotAllowedToManage()
}
return n, err
}
func (svc node) FindByID(ctx context.Context, nodeID uint64) (n *types.Node, err error) {
if n, err = loadNode(ctx, svc.store, nodeID); err != nil {
return
}
if !svc.ac.CanManageNode(ctx, n) {
return nil, NodeErrNotAllowedToManage()
}
return
}
// Looks for existing user or crates a new one
func (svc node) fetchFederatedUser(ctx context.Context, n *types.Node) (*sysTypes.User, error) {
// Generate handle for user that se this node
uHandle := fmt.Sprintf("federation_%d", n.ID)
u, err := svc.sysUser.FindByHandle(ctx, uHandle)
if err == nil {
// Reuse existing user
return u, nil
}
if service.UserErrNotFound().Is(err) {
user := &sysTypes.User{
Email: strconv.FormatUint(n.ID, 10) + "@federation.corteza",
Handle: uHandle,
}
AddFederationLabel(user, "federation", n.BaseURL)
// Create a user to service this node
r, err := service.DefaultRole.FindByHandle(ctx, "federation")
if err != nil {
return nil, err
}
// context with fed. user credentials
ctxfed := auth.SetIdentityToContext(ctx, auth.FederationUser())
u, err = svc.sysUser.Create(ctxfed, user)
if err != nil {
return nil, err
}
if err = service.DefaultRole.MemberAdd(ctxfed, r.ID, u.ID); err != nil {
return nil, err
}
u.SetRoles(append(u.Roles(), r.ID)...)
return u, nil
}
return nil, err
}
// decodePairingURI decodes URI (string) to federation node
//
// Four parts are collected from the given URI:
// 1. node host from URI's host
// 2. shared node ID from URI's username
// 3. shared token from URI's password
// 4. name of the node from query string param "name" (optional)
func (node) decodePairingURI(uri string) (*types.Node, error) {
var (
n = &types.Node{}
)
return n, func() error {
parsedURI, err := url.Parse(uri)
if err != nil {
return NodeErrPairingURIInvalid().Wrap(err)
}
n.PairToken, _ = parsedURI.User.Password()
if len(n.PairToken) != TokenLength {
return NodeErrPairingURITokenInvalid()
}
n.SharedNodeID, err = strconv.ParseUint(parsedURI.User.Username(), 10, 64)
if err != nil || n.SharedNodeID == 0 {
return NodeErrPairingURISourceIDInvalid().Wrap(err)
}
n.Name = parsedURI.Query().Get("name")
n.BaseURL = fmt.Sprintf("%s://%s/%s", parsedURI.Scheme, parsedURI.Host, strings.Trim(parsedURI.Path, "/"))
return nil
}()
}
// makePairingURI encodes details about this deployment and pairing token into sharable URI
//
// This URI contains info about this server (name, host, fed. api base url) and node created here
// that is used to identify remote federation server ID with pairing token
//
// URI structure:
// corteza+federation://<node ID>:<pairing token>@<this-host-where-the-api-is></path-to-federation-api>?qs-meta-data
func (svc node) makePairingURI(n *types.Node) string {
uri := url.URL{
Scheme: "https",
User: url.UserPassword(strconv.FormatUint(n.ID, 10), n.PairToken),
Host: svc.host,
Path: svc.baseURL,
}
qs := url.Values{}
if len(svc.name) > 0 {
qs.Add("name", svc.name)
}
if len(qs) > 0 {
uri.RawQuery = qs.Encode()
}
return uri.String()
}
func loadNode(ctx context.Context, s store.FederationNodes, ID uint64) (res *types.Node, err error) {
if ID == 0 {
return nil, NodeErrInvalidID()
}
if res, err = store.LookupFederationNodeByID(ctx, s, ID); errors.IsNotFound(err) {
err = NodeErrNotFound()
}
return
}

View File

@@ -0,0 +1,846 @@
package service
// 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/service/node_actions.yaml
import (
"context"
"fmt"
"github.com/cortezaproject/corteza/server/federation/types"
"github.com/cortezaproject/corteza/server/pkg/actionlog"
"github.com/cortezaproject/corteza/server/pkg/errors"
"github.com/cortezaproject/corteza/server/pkg/locale"
"strings"
"time"
)
type (
nodeActionProps struct {
node *types.Node
pairingURI string
filter *types.NodeFilter
}
nodeAction struct {
timestamp time.Time
resource string
action string
log string
severity actionlog.Severity
// prefix for error when action fails
errorMessage string
props *nodeActionProps
}
nodeLogMetaKey struct{}
nodePropsMetaKey struct{}
)
var (
// just a placeholder to cover template cases w/o fmt package use
_ = fmt.Println
)
// *********************************************************************************************************************
// *********************************************************************************************************************
// Props methods
// setNode updates nodeActionProps's node
//
// This function is auto-generated.
func (p *nodeActionProps) setNode(node *types.Node) *nodeActionProps {
p.node = node
return p
}
// setPairingURI updates nodeActionProps's pairingURI
//
// This function is auto-generated.
func (p *nodeActionProps) setPairingURI(pairingURI string) *nodeActionProps {
p.pairingURI = pairingURI
return p
}
// setFilter updates nodeActionProps's filter
//
// This function is auto-generated.
func (p *nodeActionProps) setFilter(filter *types.NodeFilter) *nodeActionProps {
p.filter = filter
return p
}
// Serialize converts nodeActionProps to actionlog.Meta
//
// This function is auto-generated.
func (p nodeActionProps) Serialize() actionlog.Meta {
var (
m = make(actionlog.Meta)
)
if p.node != nil {
m.Set("node.Name", p.node.Name, true)
m.Set("node.BaseURL", p.node.BaseURL, true)
m.Set("node.ID", p.node.ID, true)
m.Set("node.Status", p.node.Status, true)
}
m.Set("pairingURI", p.pairingURI, true)
if p.filter != nil {
m.Set("filter.query", p.filter.Query, true)
m.Set("filter.status", p.filter.Status, true)
}
return m
}
// tr translates string and replaces meta value placeholder with values
//
// This function is auto-generated.
func (p nodeActionProps) Format(in string, err error) string {
var (
pairs = []string{"{{err}}"}
// first non-empty string
fns = func(ii ...interface{}) string {
for _, i := range ii {
if s := fmt.Sprintf("%v", i); len(s) > 0 {
return s
}
}
return ""
}
)
if err != nil {
pairs = append(pairs, err.Error())
} else {
pairs = append(pairs, "nil")
}
if p.node != nil {
// replacement for "{{node}}" (in order how fields are defined)
pairs = append(
pairs,
"{{node}}",
fns(
p.node.Name,
p.node.BaseURL,
p.node.ID,
p.node.Status,
),
)
pairs = append(pairs, "{{node.Name}}", fns(p.node.Name))
pairs = append(pairs, "{{node.BaseURL}}", fns(p.node.BaseURL))
pairs = append(pairs, "{{node.ID}}", fns(p.node.ID))
pairs = append(pairs, "{{node.Status}}", fns(p.node.Status))
}
pairs = append(pairs, "{{pairingURI}}", fns(p.pairingURI))
if p.filter != nil {
// replacement for "{{filter}}" (in order how fields are defined)
pairs = append(
pairs,
"{{filter}}",
fns(
p.filter.Query,
p.filter.Status,
),
)
pairs = append(pairs, "{{filter.query}}", fns(p.filter.Query))
pairs = append(pairs, "{{filter.status}}", fns(p.filter.Status))
}
return strings.NewReplacer(pairs...).Replace(in)
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// Action methods
// String returns loggable description as string
//
// This function is auto-generated.
func (a *nodeAction) String() string {
var props = &nodeActionProps{}
if a.props != nil {
props = a.props
}
return props.Format(a.log, nil)
}
func (e *nodeAction) ToAction() *actionlog.Action {
return &actionlog.Action{
Resource: e.resource,
Action: e.action,
Severity: e.severity,
Description: e.String(),
Meta: e.props.Serialize(),
}
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// Action constructors
// NodeActionSearch returns "federation:node.search" action
//
// This function is auto-generated.
func NodeActionSearch(props ...*nodeActionProps) *nodeAction {
a := &nodeAction{
timestamp: time.Now(),
resource: "federation:node",
action: "search",
log: "searched for nodes",
severity: actionlog.Notice,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// NodeActionLookup returns "federation:node.lookup" action
//
// This function is auto-generated.
func NodeActionLookup(props ...*nodeActionProps) *nodeAction {
a := &nodeAction{
timestamp: time.Now(),
resource: "federation:node",
action: "lookup",
log: "looked-up for a {{node}}",
severity: actionlog.Info,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// NodeActionCreate returns "federation:node.create" action
//
// This function is auto-generated.
func NodeActionCreate(props ...*nodeActionProps) *nodeAction {
a := &nodeAction{
timestamp: time.Now(),
resource: "federation:node",
action: "create",
log: "created {{node}}",
severity: actionlog.Notice,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// NodeActionCreateFromPairingURI returns "federation:node.createFromPairingURI" action
//
// This function is auto-generated.
func NodeActionCreateFromPairingURI(props ...*nodeActionProps) *nodeAction {
a := &nodeAction{
timestamp: time.Now(),
resource: "federation:node",
action: "createFromPairingURI",
log: "created {{node}} from pairing URI",
severity: actionlog.Notice,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// NodeActionRecreateFromPairingURI returns "federation:node.recreateFromPairingURI" action
//
// This function is auto-generated.
func NodeActionRecreateFromPairingURI(props ...*nodeActionProps) *nodeAction {
a := &nodeAction{
timestamp: time.Now(),
resource: "federation:node",
action: "recreateFromPairingURI",
log: "recreate {{node}} from pairing URI",
severity: actionlog.Notice,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// NodeActionUpdate returns "federation:node.update" action
//
// This function is auto-generated.
func NodeActionUpdate(props ...*nodeActionProps) *nodeAction {
a := &nodeAction{
timestamp: time.Now(),
resource: "federation:node",
action: "update",
log: "updated {{node}}",
severity: actionlog.Notice,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// NodeActionDelete returns "federation:node.delete" action
//
// This function is auto-generated.
func NodeActionDelete(props ...*nodeActionProps) *nodeAction {
a := &nodeAction{
timestamp: time.Now(),
resource: "federation:node",
action: "delete",
log: "deleted {{node}}",
severity: actionlog.Notice,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// NodeActionUndelete returns "federation:node.undelete" action
//
// This function is auto-generated.
func NodeActionUndelete(props ...*nodeActionProps) *nodeAction {
a := &nodeAction{
timestamp: time.Now(),
resource: "federation:node",
action: "undelete",
log: "undeleted {{node}}",
severity: actionlog.Notice,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// NodeActionOttRegenerated returns "federation:node.ottRegenerated" action
//
// This function is auto-generated.
func NodeActionOttRegenerated(props ...*nodeActionProps) *nodeAction {
a := &nodeAction{
timestamp: time.Now(),
resource: "federation:node",
action: "ottRegenerated",
log: "regenerated one-time-token for {{node}}",
severity: actionlog.Notice,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// NodeActionPair returns "federation:node.pair" action
//
// This function is auto-generated.
func NodeActionPair(props ...*nodeActionProps) *nodeAction {
a := &nodeAction{
timestamp: time.Now(),
resource: "federation:node",
action: "pair",
log: "{{node}} pairing started",
severity: actionlog.Notice,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// NodeActionHandshakeInit returns "federation:node.handshakeInit" action
//
// This function is auto-generated.
func NodeActionHandshakeInit(props ...*nodeActionProps) *nodeAction {
a := &nodeAction{
timestamp: time.Now(),
resource: "federation:node",
action: "handshakeInit",
log: "{{node}} handshake initialized",
severity: actionlog.Notice,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// NodeActionHandshakeConfirm returns "federation:node.handshakeConfirm" action
//
// This function is auto-generated.
func NodeActionHandshakeConfirm(props ...*nodeActionProps) *nodeAction {
a := &nodeAction{
timestamp: time.Now(),
resource: "federation:node",
action: "handshakeConfirm",
log: "{{node}} handshake confirmed",
severity: actionlog.Notice,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// NodeActionHandshakeComplete returns "federation:node.handshakeComplete" action
//
// This function is auto-generated.
func NodeActionHandshakeComplete(props ...*nodeActionProps) *nodeAction {
a := &nodeAction{
timestamp: time.Now(),
resource: "federation:node",
action: "handshakeComplete",
log: "{{node}} handshake completed",
severity: actionlog.Notice,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// Error constructors
// NodeErrGeneric returns "federation:node.generic" as *errors.Error
//
// This function is auto-generated.
func NodeErrGeneric(mm ...*nodeActionProps) *errors.Error {
var p = &nodeActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("failed to complete request due to internal error", nil),
errors.Meta("type", "generic"),
errors.Meta("resource", "federation:node"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(nodeLogMetaKey{}, "{err}"),
errors.Meta(nodePropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
errors.Meta(locale.ErrorMetaKey{}, "node.errors.generic"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// NodeErrNotFound returns "federation:node.notFound" as *errors.Error
//
// This function is auto-generated.
func NodeErrNotFound(mm ...*nodeActionProps) *errors.Error {
var p = &nodeActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("node does not exist", nil),
errors.Meta("type", "notFound"),
errors.Meta("resource", "federation:node"),
errors.Meta(nodePropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
errors.Meta(locale.ErrorMetaKey{}, "node.errors.notFound"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// NodeErrInvalidID returns "federation:node.invalidID" as *errors.Error
//
// This function is auto-generated.
func NodeErrInvalidID(mm ...*nodeActionProps) *errors.Error {
var p = &nodeActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("invalid ID", nil),
errors.Meta("type", "invalidID"),
errors.Meta("resource", "federation:node"),
errors.Meta(nodePropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
errors.Meta(locale.ErrorMetaKey{}, "node.errors.invalidID"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// NodeErrPairingURIInvalid returns "federation:node.pairingURIInvalid" as *errors.Error
//
// This function is auto-generated.
func NodeErrPairingURIInvalid(mm ...*nodeActionProps) *errors.Error {
var p = &nodeActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("pairing URI invalid: {{err}}", nil),
errors.Meta("type", "pairingURIInvalid"),
errors.Meta("resource", "federation:node"),
errors.Meta(nodePropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
errors.Meta(locale.ErrorMetaKey{}, "node.errors.pairingURIInvalid"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// NodeErrPairingURITokenInvalid returns "federation:node.pairingURITokenInvalid" as *errors.Error
//
// This function is auto-generated.
func NodeErrPairingURITokenInvalid(mm ...*nodeActionProps) *errors.Error {
var p = &nodeActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("pairing URI with invalid pairing token", nil),
errors.Meta("type", "pairingURITokenInvalid"),
errors.Meta("resource", "federation:node"),
errors.Meta(nodePropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
errors.Meta(locale.ErrorMetaKey{}, "node.errors.pairingURITokenInvalid"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// NodeErrPairingURISourceIDInvalid returns "federation:node.pairingURISourceIDInvalid" as *errors.Error
//
// This function is auto-generated.
func NodeErrPairingURISourceIDInvalid(mm ...*nodeActionProps) *errors.Error {
var p = &nodeActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("pairing URI without source node ID", nil),
errors.Meta("type", "pairingURISourceIDInvalid"),
errors.Meta("resource", "federation:node"),
errors.Meta(nodePropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
errors.Meta(locale.ErrorMetaKey{}, "node.errors.pairingURISourceIDInvalid"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// NodeErrPairingTokenInvalid returns "federation:node.pairingTokenInvalid" as *errors.Error
//
// This function is auto-generated.
func NodeErrPairingTokenInvalid(mm ...*nodeActionProps) *errors.Error {
var p = &nodeActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("pairing token invalid", nil),
errors.Meta("type", "pairingTokenInvalid"),
errors.Meta("resource", "federation:node"),
errors.Meta(nodePropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
errors.Meta(locale.ErrorMetaKey{}, "node.errors.pairingTokenInvalid"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// NodeErrNotAllowedToCreate returns "federation:node.notAllowedToCreate" as *errors.Error
//
// This function is auto-generated.
func NodeErrNotAllowedToCreate(mm ...*nodeActionProps) *errors.Error {
var p = &nodeActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("not allowed to create nodes", nil),
errors.Meta("type", "notAllowedToCreate"),
errors.Meta("resource", "federation:node"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(nodeLogMetaKey{}, "could not create nodes; insufficient permissions"),
errors.Meta(nodePropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
errors.Meta(locale.ErrorMetaKey{}, "node.errors.notAllowedToCreate"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// NodeErrNotAllowedToSearch returns "federation:node.notAllowedToSearch" as *errors.Error
//
// This function is auto-generated.
func NodeErrNotAllowedToSearch(mm ...*nodeActionProps) *errors.Error {
var p = &nodeActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("not allowed to search or list nodes", nil),
errors.Meta("type", "notAllowedToSearch"),
errors.Meta("resource", "federation:node"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(nodeLogMetaKey{}, "could not search or list nodes; insufficient permissions"),
errors.Meta(nodePropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
errors.Meta(locale.ErrorMetaKey{}, "node.errors.notAllowedToSearch"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// NodeErrNotAllowedToManage returns "federation:node.notAllowedToManage" as *errors.Error
//
// This function is auto-generated.
func NodeErrNotAllowedToManage(mm ...*nodeActionProps) *errors.Error {
var p = &nodeActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("not allowed to manage this node", nil),
errors.Meta("type", "notAllowedToManage"),
errors.Meta("resource", "federation:node"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(nodeLogMetaKey{}, "could not manage {{node}}; insufficient permissions"),
errors.Meta(nodePropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
errors.Meta(locale.ErrorMetaKey{}, "node.errors.notAllowedToManage"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// NodeErrNotAllowedToPair returns "federation:node.notAllowedToPair" as *errors.Error
//
// This function is auto-generated.
func NodeErrNotAllowedToPair(mm ...*nodeActionProps) *errors.Error {
var p = &nodeActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("not allowed to pair this node", nil),
errors.Meta("type", "notAllowedToPair"),
errors.Meta("resource", "federation:node"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(nodeLogMetaKey{}, "could not pair {{node}}; insufficient permissions"),
errors.Meta(nodePropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
errors.Meta(locale.ErrorMetaKey{}, "node.errors.notAllowedToPair"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// recordAction is a service helper function wraps function that can return error
//
// It will wrap unrecognized/internal errors with generic errors.
//
// This function is auto-generated.
func (svc node) recordAction(ctx context.Context, props *nodeActionProps, actionFn func(...*nodeActionProps) *nodeAction, err error) error {
if svc.actionlog == nil || actionFn == nil {
// action log disabled or no action fn passed, return error as-is
return err
} else if err == nil {
// action completed w/o error, record it
svc.actionlog.Record(ctx, actionFn(props).ToAction())
return nil
}
a := actionFn(props).ToAction()
// Extracting error information and recording it as action
a.Error = err.Error()
switch c := err.(type) {
case *errors.Error:
m := c.Meta()
a.Error = err.Error()
a.Severity = actionlog.Severity(m.AsInt("severity"))
a.Description = props.Format(m.AsString(nodeLogMetaKey{}), err)
if p, has := m[nodePropsMetaKey{}]; has {
a.Meta = p.(*nodeActionProps).Serialize()
}
svc.actionlog.Record(ctx, a)
default:
svc.actionlog.Record(ctx, a)
}
// Original error is passed on
return err
}

View File

@@ -0,0 +1,100 @@
# List of loggable service actions
resource: federation:node
service: node
# Default sensitivity for actions
defaultActionSeverity: notice
# default severity for errors
defaultErrorSeverity: error
import:
- github.com/cortezaproject/corteza/server/federation/types
props:
- name: node
type: "*types.Node"
fields: [ Name, BaseURL, ID, Status ]
- name: pairingURI
- name: filter
type: "*types.NodeFilter"
fields: [ query, status ]
actions:
- action: search
log: "searched for nodes"
- action: lookup
log: "looked-up for a {{node}}"
severity: info
- action: create
log: "created {{node}}"
- action: createFromPairingURI
log: "created {{node}} from pairing URI"
- action: recreateFromPairingURI
log: "recreate {{node}} from pairing URI"
- action: update
log: "updated {{node}}"
- action: delete
log: "deleted {{node}}"
- action: undelete
log: "undeleted {{node}}"
- action: ottRegenerated
log: "regenerated one-time-token for {{node}}"
- action: pair
log: "{{node}} pairing started"
- action: handshakeInit
log: "{{node}} handshake initialized"
- action: handshakeConfirm
log: "{{node}} handshake confirmed"
- action: handshakeComplete
log: "{{node}} handshake completed"
errors:
- error: notFound
message: "node does not exist"
severity: warning
- error: invalidID
message: "invalid ID"
severity: warning
- error: pairingURIInvalid
message: "pairing URI invalid: {{err}}"
#
- error: pairingURITokenInvalid
message: "pairing URI with invalid pairing token"
- error: pairingURISourceIDInvalid
message: "pairing URI without source node ID"
- error: pairingTokenInvalid
message: "pairing token invalid"
- error: notAllowedToCreate
message: "not allowed to create nodes"
log: "could not create nodes; insufficient permissions"
- error: notAllowedToSearch
message: "not allowed to search or list nodes"
log: "could not search or list nodes; insufficient permissions"
- error: notAllowedToManage
message: "not allowed to manage this node"
log: "could not manage {{node}}; insufficient permissions"
- error: notAllowedToPair
message: "not allowed to pair this node"
log: "could not pair {{node}}; insufficient permissions"

View File

@@ -0,0 +1,97 @@
package service
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"strconv"
"github.com/cortezaproject/corteza/server/federation/types"
)
type (
// HttpHandshake performs HTTP handshake with remote node
httpNodeHandshake struct {
httpClient httpNodeHandshaker
}
httpNodeHandshaker interface {
Do(*http.Request) (*http.Response, error)
}
)
func HttpHandshake(client httpNodeHandshaker) *httpNodeHandshake {
return &httpNodeHandshake{httpClient: client}
}
// Init calls handshake init on a remote node via HTTP
//
// Called from node.Pair() and sends POST request to remote Corteza
// There, it is handled with node.HandshakeInit
//
// Fn is not part of the node struct to allow injection
func (h httpNodeHandshake) Init(ctx context.Context, n *types.Node, authToken string) error {
var (
endpoint = fmt.Sprintf("%s/nodes/%d/handshake", n.BaseURL, n.SharedNodeID)
payload = map[string]string{
// Share pairing token, so we can authenticate ourselves
"pairToken": n.PairToken,
// Share auth token for the federation service user for this node
"authToken": authToken,
"sharedNodeID": strconv.FormatUint(n.SharedNodeID, 10),
}
)
return h.send(ctx, endpoint, payload)
}
// Confirm calls handshake init on a remote node via HTTP
//
// Called from node.Confirm and sends POST request to remote Corteza
// There, it's handled with node.HandshakeComplete
func (h httpNodeHandshake) Complete(ctx context.Context, n *types.Node, authToken string) error {
var (
endpoint = fmt.Sprintf("%s/nodes/%d/handshake-complete", n.BaseURL, n.SharedNodeID)
payload = map[string]string{
// Share auth token for the federation service user for this node
"authToken": authToken,
}
)
// use n.AuthToken in context
ctx = context.WithValue(ctx, "authToken", n.AuthToken)
return h.send(ctx, endpoint, payload)
}
func (h httpNodeHandshake) send(ctx context.Context, endpoint string, payload map[string]string) error {
body := bytes.NewBuffer(nil)
if err := json.NewEncoder(body).Encode(payload); err != nil {
return err
}
req, err := http.NewRequest(http.MethodPost, endpoint, body)
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
if authToken := ctx.Value("authToken"); authToken != nil {
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", authToken))
}
rsp, err := h.httpClient.Do(req.WithContext(ctx))
if err != nil {
return err
}
if rsp.StatusCode != http.StatusOK {
return fmt.Errorf("pairing failed, remote host retuned non-200 response")
}
return nil
}

View File

@@ -0,0 +1,71 @@
package service
import (
"context"
"github.com/cortezaproject/corteza/server/federation/types"
"github.com/cortezaproject/corteza/server/pkg/actionlog"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/store"
)
type (
nodeSync struct {
store store.Storer
actionlog actionlog.Recorder
}
NodeSyncService interface {
Create(ctx context.Context, new *types.NodeSync) (*types.NodeSync, error)
Search(ctx context.Context, f types.NodeSyncFilter) (types.NodeSyncSet, types.NodeSyncFilter, error)
LookupLastSuccessfulSync(ctx context.Context, nodeID uint64, syncType string) (*types.NodeSync, error)
}
)
func NodeSync() NodeSyncService {
return &nodeSync{
store: DefaultStore,
actionlog: DefaultActionlog,
}
}
func (svc nodeSync) Create(ctx context.Context, new *types.NodeSync) (*types.NodeSync, error) {
var (
aProps = &nodeSyncActionProps{nodeSync: new}
)
err := store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) {
if _, err := DefaultNode.FindByID(ctx, new.NodeID); err != nil {
return NodeSyncErrNodeNotFound()
}
return store.CreateFederationNodeSync(ctx, s, new)
})
return new, svc.recordAction(ctx, aProps, NodeSyncActionCreate, err)
}
func (svc nodeSync) Search(ctx context.Context, f types.NodeSyncFilter) (types.NodeSyncSet, types.NodeSyncFilter, error) {
return store.SearchFederationNodeSyncs(ctx, svc.store, f)
}
func (svc nodeSync) LookupLastSuccessfulSync(ctx context.Context, nodeID uint64, syncType string) (ns *types.NodeSync, err error) {
// todo - filter by sync-type does not work
s, _, err := store.SearchFederationNodeSyncs(ctx, svc.store, types.NodeSyncFilter{
NodeID: nodeID,
SyncType: syncType,
SyncStatus: types.NodeSyncStatusSuccess,
Sorting: filter.Sorting{
Sort: filter.SortExprSet{
&filter.SortExpr{Column: "time_of_action", Descending: true},
},
},
Paging: filter.Paging{Limit: 1},
})
if err != nil || len(s) == 0 {
return nil, err
}
return s[0], nil
}

View File

@@ -0,0 +1,359 @@
package service
// 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/service/node_sync_actions.yaml
import (
"context"
"fmt"
"github.com/cortezaproject/corteza/server/federation/types"
"github.com/cortezaproject/corteza/server/pkg/actionlog"
"github.com/cortezaproject/corteza/server/pkg/errors"
"github.com/cortezaproject/corteza/server/pkg/locale"
"strings"
"time"
)
type (
nodeSyncActionProps struct {
nodeSync *types.NodeSync
nodeSyncFilter *types.NodeSyncFilter
}
nodeSyncAction struct {
timestamp time.Time
resource string
action string
log string
severity actionlog.Severity
// prefix for error when action fails
errorMessage string
props *nodeSyncActionProps
}
nodeSyncLogMetaKey struct{}
nodeSyncPropsMetaKey struct{}
)
var (
// just a placeholder to cover template cases w/o fmt package use
_ = fmt.Println
)
// *********************************************************************************************************************
// *********************************************************************************************************************
// Props methods
// setNodeSync updates nodeSyncActionProps's nodeSync
//
// This function is auto-generated.
func (p *nodeSyncActionProps) setNodeSync(nodeSync *types.NodeSync) *nodeSyncActionProps {
p.nodeSync = nodeSync
return p
}
// setNodeSyncFilter updates nodeSyncActionProps's nodeSyncFilter
//
// This function is auto-generated.
func (p *nodeSyncActionProps) setNodeSyncFilter(nodeSyncFilter *types.NodeSyncFilter) *nodeSyncActionProps {
p.nodeSyncFilter = nodeSyncFilter
return p
}
// Serialize converts nodeSyncActionProps to actionlog.Meta
//
// This function is auto-generated.
func (p nodeSyncActionProps) Serialize() actionlog.Meta {
var (
m = make(actionlog.Meta)
)
if p.nodeSync != nil {
m.Set("nodeSync.NodeID", p.nodeSync.NodeID, true)
m.Set("nodeSync.SyncStatus", p.nodeSync.SyncStatus, true)
m.Set("nodeSync.SyncType", p.nodeSync.SyncType, true)
m.Set("nodeSync.TimeOfAction", p.nodeSync.TimeOfAction, true)
}
if p.nodeSyncFilter != nil {
m.Set("nodeSyncFilter.Query", p.nodeSyncFilter.Query, true)
}
return m
}
// tr translates string and replaces meta value placeholder with values
//
// This function is auto-generated.
func (p nodeSyncActionProps) Format(in string, err error) string {
var (
pairs = []string{"{{err}}"}
// first non-empty string
fns = func(ii ...interface{}) string {
for _, i := range ii {
if s := fmt.Sprintf("%v", i); len(s) > 0 {
return s
}
}
return ""
}
)
if err != nil {
pairs = append(pairs, err.Error())
} else {
pairs = append(pairs, "nil")
}
if p.nodeSync != nil {
// replacement for "{{nodeSync}}" (in order how fields are defined)
pairs = append(
pairs,
"{{nodeSync}}",
fns(
p.nodeSync.NodeID,
p.nodeSync.SyncStatus,
p.nodeSync.SyncType,
p.nodeSync.TimeOfAction,
),
)
pairs = append(pairs, "{{nodeSync.NodeID}}", fns(p.nodeSync.NodeID))
pairs = append(pairs, "{{nodeSync.SyncStatus}}", fns(p.nodeSync.SyncStatus))
pairs = append(pairs, "{{nodeSync.SyncType}}", fns(p.nodeSync.SyncType))
pairs = append(pairs, "{{nodeSync.TimeOfAction}}", fns(p.nodeSync.TimeOfAction))
}
if p.nodeSyncFilter != nil {
// replacement for "{{nodeSyncFilter}}" (in order how fields are defined)
pairs = append(
pairs,
"{{nodeSyncFilter}}",
fns(
p.nodeSyncFilter.Query,
),
)
pairs = append(pairs, "{{nodeSyncFilter.Query}}", fns(p.nodeSyncFilter.Query))
}
return strings.NewReplacer(pairs...).Replace(in)
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// Action methods
// String returns loggable description as string
//
// This function is auto-generated.
func (a *nodeSyncAction) String() string {
var props = &nodeSyncActionProps{}
if a.props != nil {
props = a.props
}
return props.Format(a.log, nil)
}
func (e *nodeSyncAction) ToAction() *actionlog.Action {
return &actionlog.Action{
Resource: e.resource,
Action: e.action,
Severity: e.severity,
Description: e.String(),
Meta: e.props.Serialize(),
}
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// Action constructors
// NodeSyncActionLookup returns "federation:node_sync.lookup" action
//
// This function is auto-generated.
func NodeSyncActionLookup(props ...*nodeSyncActionProps) *nodeSyncAction {
a := &nodeSyncAction{
timestamp: time.Now(),
resource: "federation:node_sync",
action: "lookup",
log: "looked-up for the last successful sync",
severity: actionlog.Info,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// NodeSyncActionCreate returns "federation:node_sync.create" action
//
// This function is auto-generated.
func NodeSyncActionCreate(props ...*nodeSyncActionProps) *nodeSyncAction {
a := &nodeSyncAction{
timestamp: time.Now(),
resource: "federation:node_sync",
action: "create",
log: "created node_sync",
severity: actionlog.Notice,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// Error constructors
// NodeSyncErrGeneric returns "federation:node_sync.generic" as *errors.Error
//
// This function is auto-generated.
func NodeSyncErrGeneric(mm ...*nodeSyncActionProps) *errors.Error {
var p = &nodeSyncActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("failed to complete request due to internal error", nil),
errors.Meta("type", "generic"),
errors.Meta("resource", "federation:node_sync"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(nodeSyncLogMetaKey{}, "{err}"),
errors.Meta(nodeSyncPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
errors.Meta(locale.ErrorMetaKey{}, "node-sync.errors.generic"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// NodeSyncErrNotFound returns "federation:node_sync.notFound" as *errors.Error
//
// This function is auto-generated.
func NodeSyncErrNotFound(mm ...*nodeSyncActionProps) *errors.Error {
var p = &nodeSyncActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("node_sync does not exist", nil),
errors.Meta("type", "notFound"),
errors.Meta("resource", "federation:node_sync"),
errors.Meta(nodeSyncPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
errors.Meta(locale.ErrorMetaKey{}, "node-sync.errors.notFound"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// NodeSyncErrNodeNotFound returns "federation:node_sync.nodeNotFound" as *errors.Error
//
// This function is auto-generated.
func NodeSyncErrNodeNotFound(mm ...*nodeSyncActionProps) *errors.Error {
var p = &nodeSyncActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("node does not exist", nil),
errors.Meta("type", "nodeNotFound"),
errors.Meta("resource", "federation:node_sync"),
errors.Meta(nodeSyncPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
errors.Meta(locale.ErrorMetaKey{}, "node-sync.errors.nodeNotFound"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// recordAction is a service helper function wraps function that can return error
//
// It will wrap unrecognized/internal errors with generic errors.
//
// This function is auto-generated.
func (svc nodeSync) recordAction(ctx context.Context, props *nodeSyncActionProps, actionFn func(...*nodeSyncActionProps) *nodeSyncAction, err error) error {
if svc.actionlog == nil || actionFn == nil {
// action log disabled or no action fn passed, return error as-is
return err
} else if err == nil {
// action completed w/o error, record it
svc.actionlog.Record(ctx, actionFn(props).ToAction())
return nil
}
a := actionFn(props).ToAction()
// Extracting error information and recording it as action
a.Error = err.Error()
switch c := err.(type) {
case *errors.Error:
m := c.Meta()
a.Error = err.Error()
a.Severity = actionlog.Severity(m.AsInt("severity"))
a.Description = props.Format(m.AsString(nodeSyncLogMetaKey{}), err)
if p, has := m[nodeSyncPropsMetaKey{}]; has {
a.Meta = p.(*nodeSyncActionProps).Serialize()
}
svc.actionlog.Record(ctx, a)
default:
svc.actionlog.Record(ctx, a)
}
// Original error is passed on
return err
}

View File

@@ -0,0 +1,38 @@
# List of loggable service actions
resource: federation:node_sync
service: nodeSync
# Default sensitivity for actions
defaultActionSeverity: notice
# default severity for errors
defaultErrorSeverity: error
import:
- github.com/cortezaproject/corteza/server/federation/types
props:
- name: nodeSync
type: "*types.NodeSync"
fields: [ NodeID, SyncStatus, SyncType, TimeOfAction ]
- name: nodeSyncFilter
type: "*types.NodeSyncFilter"
fields: [ Query ]
actions:
- action: lookup
log: "looked-up for the last successful sync"
severity: info
- action: create
log: "created node_sync"
errors:
- error: notFound
message: "node_sync does not exist"
severity: warning
- error: nodeNotFound
message: "node does not exist"
severity: warning

View File

@@ -0,0 +1,77 @@
package service
import (
"fmt"
"github.com/cortezaproject/corteza/server/federation/types"
"github.com/stretchr/testify/require"
"testing"
)
func TestNode_decodePairingURI(t *testing.T) {
var (
tcc = []struct {
name string
uri string
node *types.Node
err error
}{
{
"happy path",
"https://42:secret-secret-secret-secret-1234@example.tld/federation?name=Noddy",
&types.Node{SharedNodeID: 42, Name: "Noddy", BaseURL: "https://example.tld/federation", PairToken: "secret-secret-secret-secret-1234"},
nil,
},
{
"no name",
"https://42:secret-secret-secret-secret-1234@example.tld",
&types.Node{SharedNodeID: 42, BaseURL: "https://example.tld/", PairToken: "secret-secret-secret-secret-1234"},
nil,
},
{
"no token",
"https://42@example.tld",
nil,
NodeErrPairingURITokenInvalid(),
},
{
"no node id",
"https://:secret-secret-secret-secret-1234@example.tld",
nil,
NodeErrPairingURISourceIDInvalid(),
},
{
"invalid URL",
"https://this is not a valid url",
nil,
NodeErrPairingURIInvalid().Wrap(fmt.Errorf(`invalid character " " in host name`)),
},
}
)
for _, tc := range tcc {
t.Run(tc.name, func(t *testing.T) {
var (
req = require.New(t)
out, err = (&node{}).decodePairingURI(tc.uri)
)
if tc.err != nil {
req.Equal(tc.err.Error(), err.Error())
} else {
req.NoError(err)
req.Equal(tc.node, out)
}
})
}
}
func TestNode_makePairingURI(t *testing.T) {
var (
svc = &node{host: "example.tld", baseURL: "federation", name: "Noddy"}
req = require.New(t)
n = &types.Node{ID: 42, PairToken: "secret"}
uri = svc.makePairingURI(n)
)
req.Equal("https://42:secret@example.tld/federation?name=Noddy", uri)
}

View File

@@ -0,0 +1,11 @@
package service
import "context"
type (
Processer interface {
Process(ctx context.Context, payload []byte) (ProcesserResponse, error)
}
ProcesserResponse interface{}
)

View File

@@ -0,0 +1,134 @@
package service
import (
"context"
"fmt"
ct "github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/federation/service/decoder"
"github.com/cortezaproject/corteza/server/federation/types"
"github.com/cortezaproject/corteza/server/pkg/auth"
st "github.com/cortezaproject/corteza/server/system/types"
)
type (
dataProcesser struct {
ID uint64
ComposeModuleID uint64
ComposeNamespaceID uint64
NodeBaseURL string
ModuleMappings *types.ModuleFieldMappingSet
ModuleMappingValues *ct.RecordValueSet
SyncService *Sync
Node *types.Node
User *st.User
}
dataProcesserResponse struct {
Processed int
}
)
// Process gets the payload from syncer and
// uses the decode package to decode the whole set, depending on
// the filtering that was used (limit)
func (dp *dataProcesser) Process(ctx context.Context, payload []byte) (ProcesserResponse, error) {
processed := 0
o, err := decoder.DecodeFederationRecordSync([]byte(payload))
if err != nil {
return dataProcesserResponse{
Processed: processed,
}, err
}
if len(o) == 0 {
return dataProcesserResponse{
Processed: processed,
}, nil
}
ctx = auth.SetIdentityToContext(ctx, auth.FederationUser())
for _, er := range o {
var (
rec *ct.Record
err error
)
dp.SyncService.mapper.Merge(&er.Values, dp.ModuleMappingValues, dp.ModuleMappings)
if er.DeletedAt != nil {
// find the record
if rec, err = dp.findRecordByFederationID(ctx, er.ID, dp.ComposeModuleID, dp.ComposeNamespaceID); err != nil {
continue
}
// Handle edge cases where the data doesn't exist anymore
if rec != nil {
dp.SyncService.DeleteRecord(ctx, rec)
}
processed++
continue
}
if er.UpdatedAt != nil {
if rec, err = dp.findRecordByFederationID(ctx, er.ID, dp.ComposeModuleID, dp.ComposeNamespaceID); err != nil {
// could not find existing record
continue
}
if rec != nil {
rec.Values = *dp.ModuleMappingValues
}
}
// if the record was updated on origin, but we somehow do not have it
// create it anyway
if rec == nil {
rec = &ct.Record{
ModuleID: dp.ComposeModuleID,
NamespaceID: dp.ComposeNamespaceID,
Values: *dp.ModuleMappingValues,
Meta: map[string]any{
"federation": dp.NodeBaseURL,
"federation_extrecord": fmt.Sprintf("%d", er.ID),
},
}
}
if rec.ID != 0 {
_, err = dp.SyncService.UpdateRecord(ctx, rec)
} else {
_, err = dp.SyncService.CreateRecord(ctx, rec)
}
if err != nil {
continue
}
processed++
}
return dataProcesserResponse{
Processed: processed,
}, nil
}
// findRecordByFederationID finds any already existing records via
// federation label
func (dp *dataProcesser) findRecordByFederationID(ctx context.Context, recordID, moduleID, namespaceID uint64) (r *ct.Record, err error) {
filter := ct.RecordFilter{
NamespaceID: namespaceID,
ModuleID: moduleID,
Meta: map[string]any{"federation_extrecord": fmt.Sprintf("%d", recordID)}}
if s, err := dp.SyncService.FindRecords(ctx, filter); err == nil {
if len(s) == 1 {
r = s[0]
}
}
return
}

View File

@@ -0,0 +1,230 @@
package service
import (
"context"
"encoding/json"
"fmt"
"testing"
cs "github.com/cortezaproject/corteza/server/compose/service"
ct "github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/federation/types"
ss "github.com/cortezaproject/corteza/server/system/service"
st "github.com/cortezaproject/corteza/server/system/types"
"github.com/stretchr/testify/require"
)
type (
testSyncService struct {
Sync
}
testSharedModuleService struct {
SharedModuleService
}
testRecordServicePersistSuccess struct {
cs.RecordService
}
testRecordServiceUpdateSuccess struct {
cs.RecordService
}
testRecordServiceDeleteSuccess struct {
cs.RecordService
}
testRecordServicePersistError struct {
cs.RecordService
}
testUserService struct {
ss.UserService
}
testRoleService struct {
ss.RoleService
}
)
func TestProcesserData_persist(t *testing.T) {
var (
tcc = []struct {
name string
payload string
mappings string
persisted int
err string
values *ct.RecordValueSet
s *Sync
}{
{
"successful persist on valid mapping",
`{"response": {"set": [{"recordID":"1","values":[{"name":"Facebook","value":"foobar"}],"createdAt":"2020-12-05T10:10:10Z"}]}}`,
`[{"origin":{"kind":"String","name":"Description","label":"Description","isMulti":false},"destination":{"kind":"String","name":"Name","label":"Description","isMulti":false}},{"origin":{"kind":"Url","name":"Facebook","label":"Facebook","isMulti":false},"destination":{"kind":"Url","name":"Fb","label":"Facebook","isMulti":false}}]`,
1,
"",
&ct.RecordValueSet{&ct.RecordValue{Name: "Fb", Value: ""}},
NewSync(
&Syncer{},
&Mapper{},
&testSharedModuleService{},
&testRecordServicePersistSuccess{},
&testUserService{},
&testRoleService{}),
},
{
"successful update on valid mapping and existing federated record",
`{"response": {"set": [{"recordID":"1","values":[{"name":"Facebook","value":"foobar"}],"createdAt":"2020-12-05T10:10:10Z", "updatedAt":"2020-12-06T10:10:10Z"}]}}`,
`[{"origin":{"kind":"String","name":"Description","label":"Description","isMulti":false},"destination":{"kind":"String","name":"Name","label":"Description","isMulti":false}},{"origin":{"kind":"Url","name":"Facebook","label":"Facebook","isMulti":false},"destination":{"kind":"Url","name":"Fb","label":"Facebook","isMulti":false}}]`,
1,
"",
&ct.RecordValueSet{&ct.RecordValue{Name: "Fb", Value: ""}},
NewSync(
&Syncer{},
&Mapper{},
&testSharedModuleService{},
&testRecordServiceUpdateSuccess{},
&testUserService{},
&testRoleService{}),
},
{
"successful delete on valid mapping and existing federated record",
`{"response": {"set": [{"recordID":"1","values":[{"name":"Facebook","value":"foobar"}],"createdAt":"2020-12-05T10:10:10Z", "deletedAt":"2020-12-07T10:10:10Z"}]}}`,
`[{"origin":{"kind":"String","name":"Description","label":"Description","isMulti":false},"destination":{"kind":"String","name":"Name","label":"Description","isMulti":false}},{"origin":{"kind":"Url","name":"Facebook","label":"Facebook","isMulti":false},"destination":{"kind":"Url","name":"Fb","label":"Facebook","isMulti":false}}]`,
1,
"",
&ct.RecordValueSet{&ct.RecordValue{Name: "Fb", Value: ""}},
NewSync(
&Syncer{},
&Mapper{},
&testSharedModuleService{},
&testRecordServiceDeleteSuccess{},
&testUserService{},
&testRoleService{}),
},
{
"persist error on valid mapping",
`{"response": {"set": [{"recordID":"1","values":[{"name":"Facebook","value":"foobar"}]}]}}`,
`[{"origin":{"kind":"String","name":"Description","label":"Description","isMulti":false},"destination":{"kind":"String","name":"Name","label":"Description","isMulti":false}},{"origin":{"kind":"Url","name":"Facebook","label":"Facebook","isMulti":false},"destination":{"kind":"Url","name":"Fb","label":"Facebook","isMulti":false}}]`,
0,
"",
&ct.RecordValueSet{&ct.RecordValue{Name: "Fb", Value: ""}},
NewSync(
&Syncer{},
&Mapper{},
&testSharedModuleService{},
&testRecordServicePersistError{},
&testUserService{},
&testRoleService{}),
},
{
"persist one record on mixed mappings",
`{"response": {"set": [{"recordID":"1","values":[{"name":"THIS_FIELD_NAME_IS_NOT_ON_ORIGIN","value":"this value will not be set"}]}, {"recordID":"1","values":[{"name":"Facebook","value":"this value WILL be set"}]}]}}`,
`[{"origin":{"kind":"String","name":"Description","label":"Description","isMulti":false},"destination":{"kind":"String","name":"Name","label":"Description","isMulti":false}},{"origin":{"kind":"Url","name":"Facebook","label":"Facebook","isMulti":false},"destination":{"kind":"Url","name":"Fb","label":"Facebook","isMulti":false}}]`,
2,
"",
&ct.RecordValueSet{&ct.RecordValue{Name: "Fb", Value: ""}},
NewSync(
&Syncer{},
&Mapper{},
&testSharedModuleService{},
&testRecordServicePersistSuccess{},
&testUserService{},
&testRoleService{}),
},
{
"no records from payload",
`{"response": {"set": []}}`,
`[{"origin":{"kind":"String","name":"Description","label":"Description","isMulti":false},"destination":{"kind":"String","name":"Name","label":"Description","isMulti":false}},{"origin":{"kind":"Url","name":"Facebook","label":"Facebook","isMulti":false},"destination":{"kind":"Url","name":"Fb","label":"Facebook","isMulti":false}}]`,
0,
"",
&ct.RecordValueSet{&ct.RecordValue{Name: "Fb", Value: ""}},
NewSync(
&Syncer{},
&Mapper{},
&testSharedModuleService{},
&testRecordServicePersistSuccess{},
&testUserService{},
&testRoleService{}),
},
{
"validation error, no persist",
`{"respon`,
`[{"origin":{"kind":"String","name":"Description","label":"Description","isMulti":false},"destination":{"kind":"String","name":"Name","label":"Description","isMulti":false}},{"origin":{"kind":"Url","name":"Facebook","label":"Facebook","isMulti":false},"destination":{"kind":"Url","name":"Fb","label":"Facebook","isMulti":false}}]`,
0,
"unexpected end of JSON input",
&ct.RecordValueSet{&ct.RecordValue{Name: "Fb", Value: ""}},
NewSync(
&Syncer{},
&Mapper{},
&testSharedModuleService{},
&testRecordServicePersistSuccess{},
&testUserService{},
&testRoleService{}),
},
}
)
for _, tc := range tcc {
t.Run(tc.name, func(t *testing.T) {
var (
ctx = context.Background()
req = require.New(t)
)
mm := &types.ModuleFieldMappingSet{}
json.Unmarshal([]byte(tc.mappings), mm)
dp := &dataProcesser{
ID: 1,
ComposeModuleID: 1,
ComposeNamespaceID: 1,
NodeBaseURL: "",
ModuleMappings: mm,
ModuleMappingValues: tc.values,
SyncService: tc.s,
Node: &types.Node{},
User: &st.User{},
}
out, err := dp.Process(ctx, []byte(tc.payload))
if tc.err != "" {
req.Equal(tc.err, err.Error())
} else {
req.NoError(err)
}
req.Equal(tc.persisted, out.(dataProcesserResponse).Processed)
})
}
}
// create success
func (s testRecordServicePersistSuccess) Create(_ context.Context, record *ct.Record) (*ct.Record, *ct.RecordValueErrorSet, error) {
return nil, nil, nil
}
func (s testRecordServicePersistSuccess) Find(_ context.Context, filter ct.RecordFilter) (ct.RecordSet, ct.RecordFilter, error) {
return ct.RecordSet{}, ct.RecordFilter{}, nil
}
// update success
func (s testRecordServiceUpdateSuccess) Update(_ context.Context, record *ct.Record) (*ct.Record, *ct.RecordValueErrorSet, error) {
return nil, nil, nil
}
func (s testRecordServiceUpdateSuccess) Find(_ context.Context, filter ct.RecordFilter) (ct.RecordSet, ct.RecordFilter, error) {
return ct.RecordSet{&ct.Record{ID: 2}}, ct.RecordFilter{}, nil
}
// delete success
func (s testRecordServiceDeleteSuccess) DeleteByID(_ context.Context, namespaceID, moduleID uint64, recordID ...uint64) error {
return nil
}
func (s testRecordServiceDeleteSuccess) Find(_ context.Context, filter ct.RecordFilter) (ct.RecordSet, ct.RecordFilter, error) {
return ct.RecordSet{&ct.Record{ID: 2, ModuleID: 2, NamespaceID: 2}}, ct.RecordFilter{}, nil
}
// create error
func (s testRecordServicePersistError) Create(_ context.Context, record *ct.Record) (*ct.Record, *ct.RecordValueErrorSet, error) {
return nil, nil, fmt.Errorf("mocked error")
}

View File

@@ -0,0 +1,120 @@
package service
import (
"context"
"github.com/cortezaproject/corteza/server/federation/service/decoder"
"github.com/cortezaproject/corteza/server/federation/types"
st "github.com/cortezaproject/corteza/server/system/types"
)
type (
structureProcesser struct {
SyncService *Sync
Node *types.Node
User *st.User
}
structureProcesserResponse struct {
ModuleID uint64
Processed int
}
)
// Process gets the payload from syncer and
// uses the decode package to decode the whole set, depending on
// the filtering that was used (limit)
func (dp *structureProcesser) Process(ctx context.Context, payload []byte) (ProcesserResponse, error) {
processed := 0
o, err := decoder.DecodeFederationModuleSync([]byte(payload))
if err != nil {
return structureProcesserResponse{
Processed: processed,
}, err
}
if len(o) == 0 {
return structureProcesserResponse{
Processed: processed,
}, nil
}
for _, em := range o {
new := &types.SharedModule{
NodeID: dp.Node.ID,
ExternalFederationModuleID: em.ID,
Fields: em.Fields,
Handle: em.Handle,
Name: em.Name,
}
existing, err := dp.SyncService.LookupSharedModule(ctx, new)
if err != nil {
return structureProcesserResponse{
ModuleID: new.ExternalFederationModuleID,
Processed: processed,
}, err
}
// create
{
if existing == nil {
_, err := dp.SyncService.CreateSharedModule(ctx, new)
if err != nil {
return structureProcesserResponse{
ModuleID: new.ExternalFederationModuleID,
Processed: processed,
}, err
}
processed++
continue
}
}
// update
{
canUpdate, err := dp.SyncService.CanUpdateSharedModule(ctx, new, existing)
if err != nil {
return structureProcesserResponse{
ModuleID: existing.ExternalFederationModuleID,
Processed: processed,
}, err
}
// stop the sync for this module
if !canUpdate {
return structureProcesserResponse{
ModuleID: new.ExternalFederationModuleID,
Processed: processed,
}, SharedModuleErrFederationSyncStructureChanged(&sharedModuleActionProps{module: existing})
}
existing.Name = new.Name
existing.Handle = new.Handle
existing.Fields = new.Fields
_, err = dp.SyncService.UpdateSharedModule(ctx, existing)
if err != nil {
return structureProcesserResponse{
ModuleID: existing.ExternalFederationModuleID,
Processed: processed,
}, err
}
processed++
continue
}
}
// use paging, but only one per-time
return structureProcesserResponse{
ModuleID: o[0].ID,
Processed: processed,
}, err
}

View File

@@ -0,0 +1,232 @@
package service
import (
"context"
"fmt"
"testing"
cs "github.com/cortezaproject/corteza/server/compose/service"
"github.com/cortezaproject/corteza/server/federation/types"
ss "github.com/cortezaproject/corteza/server/system/service"
st "github.com/cortezaproject/corteza/server/system/types"
"github.com/stretchr/testify/require"
)
type (
testSyncStructureService struct{ Sync }
testSyncStructureServiceCanUpdateFieldsError struct{ Sync }
testStructureSharedModuleService struct{ SharedModuleService }
testStructureSharedModuleServiceError struct{ SharedModuleService }
testStructureSharedModuleServiceCreateNewModule struct{ SharedModuleService }
testStructureSharedModuleServiceCreateNewModuleErr struct{ SharedModuleService }
testStructureSharedModuleServiceUpdateModule struct{ SharedModuleService }
testStructureSharedModuleServiceUpdateModuleErr struct{ SharedModuleService }
testStructureRecordServicePersistSuccess struct{ cs.RecordService }
testStructureUserService struct{ ss.UserService }
testStructureRoleService struct{ ss.RoleService }
)
func TestProcesserStructure_persist(t *testing.T) {
var (
tcc = []struct {
name string
payload string
persisted int
err string
s *Sync
}{
{
"no modules from payload",
`{"response": {"set": []}}`,
0,
"",
NewSync(
&Syncer{},
&Mapper{},
&testStructureSharedModuleService{},
&testRecordServicePersistSuccess{},
&testUserService{},
&testRoleService{}),
},
{
"validation error, no persist",
`{"respon`,
0,
"unexpected end of JSON input",
NewSync(
&Syncer{},
&Mapper{},
&testStructureSharedModuleService{},
&testRecordServicePersistSuccess{},
&testUserService{},
&testRoleService{}),
},
{
"lookup existing module error",
`{"response":{"set": [{"moduleID": "11", "nodeID": "21", "composeModuleID": "31", "composeNamespaceID":"32","handle":"handle","name":"name","fields":[]}]}}`,
0,
"db error",
NewSync(
&Syncer{},
&Mapper{},
&testStructureSharedModuleServiceError{},
&testRecordServicePersistSuccess{},
&testUserService{},
&testRoleService{}),
},
{
"create new module",
`{"response":{"set": [{"moduleID": "11", "nodeID": "21", "composeModuleID": "31", "composeNamespaceID":"32","handle":"handle","name":"name","fields":[]}]}}`,
1,
"",
NewSync(
&Syncer{},
&Mapper{},
&testStructureSharedModuleServiceCreateNewModule{},
&testRecordServicePersistSuccess{},
&testUserService{},
&testRoleService{}),
},
{
"create new module error",
`{"response":{"set": [{"moduleID": "11", "nodeID": "21", "composeModuleID": "31", "composeNamespaceID":"32","handle":"handle","name":"name","fields":[]}]}}`,
0,
"could not create new module",
NewSync(
&Syncer{},
&Mapper{},
&testStructureSharedModuleServiceCreateNewModuleErr{},
&testRecordServicePersistSuccess{},
&testUserService{},
&testRoleService{}),
},
{
"could not update module fields error",
`{"response":{"set": [{"moduleID": "11", "nodeID": "21", "composeModuleID": "31", "composeNamespaceID":"32","handle":"handle","name":"name","fields":[]}]}}`,
0,
"module structure changed",
NewSync(
&Syncer{},
&Mapper{},
&testStructureSharedModuleServiceUpdateModule{},
&testRecordServicePersistSuccess{},
&testUserService{},
&testRoleService{}),
},
{
"update module fields success",
`{"response":{"set": [{"moduleID": "11", "nodeID": "21", "composeModuleID": "31", "composeNamespaceID":"32","handle":"handle","name":"name","fields":[{"kind":"String","label":"label","isMulti":false,"name":"existingField"}]}]}}`,
1,
"",
NewSync(
&Syncer{},
&Mapper{},
&testStructureSharedModuleServiceUpdateModule{},
&testRecordServicePersistSuccess{},
&testUserService{},
&testRoleService{}),
},
{
"update module error",
`{"response":{"set": [{"moduleID": "11", "nodeID": "21", "composeModuleID": "31", "composeNamespaceID":"32","handle":"handle","name":"name","fields":[]}]}}`,
0,
"could not update module",
NewSync(
&Syncer{},
&Mapper{},
&testStructureSharedModuleServiceUpdateModuleErr{},
&testRecordServicePersistSuccess{},
&testUserService{},
&testRoleService{}),
},
}
)
for _, tc := range tcc {
t.Run(tc.name, func(t *testing.T) {
var (
ctx = context.Background()
req = require.New(t)
)
dp := &structureProcesser{
SyncService: tc.s,
Node: &types.Node{},
User: &st.User{},
}
out, err := dp.Process(ctx, []byte(tc.payload))
if tc.err != "" {
if err != nil {
req.Equal(tc.err, err.Error())
} else {
req.Fail("err should not be nil")
}
} else {
req.NoError(err)
}
req.Equal(tc.persisted, out.(structureProcesserResponse).Processed)
})
}
}
func (s testStructureSharedModuleServiceError) Find(ctx context.Context, filter types.SharedModuleFilter) (types.SharedModuleSet, types.SharedModuleFilter, error) {
return types.SharedModuleSet{}, types.SharedModuleFilter{}, fmt.Errorf("db error")
}
func (s testStructureSharedModuleServiceCreateNewModule) Find(ctx context.Context, filter types.SharedModuleFilter) (types.SharedModuleSet, types.SharedModuleFilter, error) {
return types.SharedModuleSet{}, types.SharedModuleFilter{}, nil
}
func (s testStructureSharedModuleServiceCreateNewModule) Create(ctx context.Context, new *types.SharedModule) (*types.SharedModule, error) {
return &types.SharedModule{}, nil
}
func (s testStructureSharedModuleServiceCreateNewModuleErr) Find(ctx context.Context, filter types.SharedModuleFilter) (types.SharedModuleSet, types.SharedModuleFilter, error) {
return types.SharedModuleSet{}, types.SharedModuleFilter{}, nil
}
func (s testStructureSharedModuleServiceCreateNewModuleErr) Create(ctx context.Context, new *types.SharedModule) (*types.SharedModule, error) {
return nil, fmt.Errorf("could not create new module")
}
func (s testStructureSharedModuleServiceUpdateModule) Find(ctx context.Context, filter types.SharedModuleFilter) (types.SharedModuleSet, types.SharedModuleFilter, error) {
return types.SharedModuleSet{
&types.SharedModule{
ID: 11,
NodeID: 21,
Handle: "handle",
Name: "name",
Fields: types.ModuleFieldSet{
&types.ModuleField{
Kind: "String",
Label: "label",
IsMulti: false,
Name: "existingField",
},
},
}},
types.SharedModuleFilter{},
nil
}
func (s testStructureSharedModuleServiceUpdateModule) Update(ctx context.Context, updated *types.SharedModule) (*types.SharedModule, error) {
return &types.SharedModule{}, nil
}
func (s testStructureSharedModuleServiceUpdateModuleErr) Find(ctx context.Context, filter types.SharedModuleFilter) (types.SharedModuleSet, types.SharedModuleFilter, error) {
return types.SharedModuleSet{&types.SharedModule{
Fields: types.ModuleFieldSet{},
}},
types.SharedModuleFilter{},
nil
}
func (s testStructureSharedModuleServiceUpdateModuleErr) Update(ctx context.Context, updated *types.SharedModule) (*types.SharedModule, error) {
return nil, fmt.Errorf("could not update module")
}

View File

@@ -0,0 +1,139 @@
package service
import (
"context"
cs "github.com/cortezaproject/corteza/server/compose/service"
"github.com/cortezaproject/corteza/server/pkg/actionlog"
"github.com/cortezaproject/corteza/server/pkg/auth"
"github.com/cortezaproject/corteza/server/pkg/id"
"github.com/cortezaproject/corteza/server/pkg/label"
"github.com/cortezaproject/corteza/server/pkg/logger"
"github.com/cortezaproject/corteza/server/pkg/options"
"github.com/cortezaproject/corteza/server/store"
ss "github.com/cortezaproject/corteza/server/system/service"
"github.com/cortezaproject/corteza/server/system/types"
"go.uber.org/zap"
"time"
labelTypes "github.com/cortezaproject/corteza/server/pkg/label/types"
)
type (
Config struct {
ActionLog options.ActionLogOpt
Storage options.ObjectStoreOpt
Federation options.FederationOpt
Server options.HttpServerOpt
}
)
var (
DefaultStore store.Storer
DefaultLogger *zap.Logger
DefaultAccessControl *accessControl
// CurrentSettings represents current system settings
CurrentSettings = &types.AppSettings{}
DefaultOptions options.FederationOpt
DefaultActionlog actionlog.Recorder
DefaultNode *node
DefaultNodeSync NodeSyncService
DefaultExposedModule ExposedModuleService
DefaultSharedModule SharedModuleService
DefaultModuleMapping ModuleMappingService
// wrapper around time.Now() that will aid service testing
now = func() *time.Time {
c := time.Now().Round(time.Second)
return &c
}
// wrapper around id.Next() that will aid service testing
nextID = func() uint64 {
return id.Next()
}
)
func Initialize(_ context.Context, log *zap.Logger, s store.Storer, c Config) (err error) {
DefaultOptions = c.Federation
// we're doing conversion to avoid having
// store interface exposed or generated inside app package
DefaultStore = s
DefaultLogger = log.Named("service")
// @todo add healthcheck(s) for federation sync services
{
tee := zap.NewNop()
policy := actionlog.MakeProductionPolicy()
if !c.ActionLog.Enabled {
policy = actionlog.MakeDisabledPolicy()
} else if c.ActionLog.Debug {
policy = actionlog.MakeDebugPolicy()
tee = logger.MakeDebugLogger()
}
DefaultActionlog = actionlog.NewService(DefaultStore, log, tee, policy)
}
DefaultAccessControl = AccessControl(s)
DefaultNode = Node(
DefaultStore,
ss.DefaultUser,
DefaultActionlog,
func(ctx context.Context, i auth.Identifiable) (token []byte, err error) {
return auth.TokenIssuer.Issue(
ctx,
auth.WithIdentity(i),
auth.WithScope("api"),
auth.WithAudience("federation"),
)
},
c.Federation,
c.Server,
DefaultAccessControl,
)
DefaultNodeSync = NodeSync()
DefaultExposedModule = ExposedModule()
DefaultSharedModule = SharedModule()
DefaultModuleMapping = ModuleMapping()
return
}
func Watchers(ctx context.Context) {
DefaultLogger.Info("Starting federation - warning, this is still an experimental feature")
syncService := NewSync(
&Syncer{},
&Mapper{},
DefaultSharedModule,
cs.DefaultRecord,
ss.DefaultUser,
ss.DefaultRole)
syncStructure := WorkerStructure(syncService, DefaultLogger)
syncData := WorkerData(syncService, DefaultLogger)
go syncStructure.Watch(
ctx,
DefaultOptions.StructureMonitorInterval,
DefaultOptions.StructurePageSize)
go syncData.Watch(
ctx,
DefaultOptions.DataMonitorInterval,
DefaultOptions.DataPageSize)
}
func AddFederationLabel(entity label.LabeledResource, key string, value string) {
entity.SetLabel(key, labelTypes.LabelValue{Val: value})
}

View File

@@ -0,0 +1,171 @@
package service
import (
"context"
"github.com/cortezaproject/corteza/server/pkg/errors"
composeService "github.com/cortezaproject/corteza/server/compose/service"
"github.com/cortezaproject/corteza/server/federation/types"
"github.com/cortezaproject/corteza/server/pkg/actionlog"
"github.com/cortezaproject/corteza/server/pkg/auth"
"github.com/cortezaproject/corteza/server/store"
)
type (
sharedModule struct {
node node
ac sharedModuleAccessController
compose composeService.ModuleService
store store.Storer
actionlog actionlog.Recorder
}
sharedModuleAccessController interface {
CanCreateModuleOnNode(ctx context.Context, r *types.Node) bool
}
SharedModuleService interface {
Create(ctx context.Context, new *types.SharedModule) (*types.SharedModule, error)
Update(ctx context.Context, updated *types.SharedModule) (*types.SharedModule, error)
Find(ctx context.Context, filter types.SharedModuleFilter) (types.SharedModuleSet, types.SharedModuleFilter, error)
FindByID(ctx context.Context, nodeID uint64, moduleID uint64) (*types.SharedModule, error)
}
)
func SharedModule() *sharedModule {
return &sharedModule{
ac: DefaultAccessControl,
node: *DefaultNode,
compose: composeService.DefaultModule,
store: DefaultStore,
actionlog: DefaultActionlog,
}
}
func (svc sharedModule) FindByID(ctx context.Context, nodeID uint64, moduleID uint64) (module *types.SharedModule, err error) {
err = func() error {
if module, err = loadSharedModule(ctx, svc.store, nodeID, moduleID); err != nil {
return err
}
return nil
}()
return module, err
}
func (svc sharedModule) Create(ctx context.Context, new *types.SharedModule) (*types.SharedModule, error) {
var (
aProps = &sharedModuleActionProps{changed: new}
)
err := store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) {
var (
node *types.Node
)
if node, err = svc.node.FindByID(ctx, new.NodeID); err != nil {
return SharedModuleErrNodeNotFound()
}
if !svc.ac.CanCreateModuleOnNode(ctx, node) {
return SharedModuleErrNotAllowedToCreate()
}
if err = svc.uniqueCheck(ctx, new); err != nil {
return err
}
new.ID = nextID()
new.CreatedAt = *now()
new.CreatedBy = auth.GetIdentityFromContext(ctx).Identity()
// check if Fields can be unmarshaled to the fields structure
if new.Fields != nil {
}
aProps.setModule(new)
if err = store.CreateFederationSharedModule(ctx, s, new); err != nil {
return err
}
return nil
})
return new, svc.recordAction(ctx, aProps, SharedModuleActionCreate, err)
}
func (svc sharedModule) Update(ctx context.Context, updated *types.SharedModule) (*types.SharedModule, error) {
var (
aProps = &sharedModuleActionProps{module: updated}
)
err := store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) {
updated.UpdatedAt = now()
updated.UpdatedBy = auth.GetIdentityFromContext(ctx).Identity()
aProps.setModule(updated)
if _, err = svc.node.FindByID(ctx, updated.NodeID); err != nil {
return SharedModuleErrNodeNotFound()
}
if err = store.UpdateFederationSharedModule(ctx, s, updated); err != nil {
return err
}
return nil
})
return updated, svc.recordAction(ctx, aProps, SharedModuleActionUpdate, err)
}
func (svc sharedModule) uniqueCheck(ctx context.Context, m *types.SharedModule) (err error) {
f := types.SharedModuleFilter{
NodeID: m.NodeID,
Handle: m.Handle,
Name: m.Name,
}
if set, _, err := store.SearchFederationSharedModules(ctx, svc.store, f); len(set) > 0 && err == nil {
return SharedModuleErrNotUnique()
} else if err != nil {
return err
}
return nil
}
func (svc sharedModule) Find(ctx context.Context, filter types.SharedModuleFilter) (set types.SharedModuleSet, f types.SharedModuleFilter, err error) {
var (
aProps = &sharedModuleActionProps{filter: &filter}
)
err = func() error {
if set, f, err = store.SearchFederationSharedModules(ctx, svc.store, filter); err != nil {
return err
}
return nil
}()
return set, f, svc.recordAction(ctx, aProps, SharedModuleActionSearch, err)
}
func loadSharedModule(ctx context.Context, s store.FederationSharedModules, nodeID, ID uint64) (res *types.SharedModule, err error) {
if ID == 0 || nodeID == 0 {
return nil, SharedModuleErrInvalidID()
}
if res, err = store.LookupFederationSharedModuleByID(ctx, s, ID); errors.IsNotFound(err) {
err = SharedModuleErrNotFound()
}
if err == nil && nodeID != res.NodeID {
// Make sure chart belongs to the right namespace
return nil, SharedModuleErrNotFound()
}
return
}

View File

@@ -0,0 +1,717 @@
package service
// 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/service/shared_module_actions.yaml
import (
"context"
"fmt"
"github.com/cortezaproject/corteza/server/federation/types"
"github.com/cortezaproject/corteza/server/pkg/actionlog"
"github.com/cortezaproject/corteza/server/pkg/errors"
"github.com/cortezaproject/corteza/server/pkg/locale"
"strings"
"time"
)
type (
sharedModuleActionProps struct {
module *types.SharedModule
changed *types.SharedModule
filter *types.SharedModuleFilter
node *types.Node
}
sharedModuleAction struct {
timestamp time.Time
resource string
action string
log string
severity actionlog.Severity
// prefix for error when action fails
errorMessage string
props *sharedModuleActionProps
}
sharedModuleLogMetaKey struct{}
sharedModulePropsMetaKey struct{}
)
var (
// just a placeholder to cover template cases w/o fmt package use
_ = fmt.Println
)
// *********************************************************************************************************************
// *********************************************************************************************************************
// Props methods
// setModule updates sharedModuleActionProps's module
//
// This function is auto-generated.
func (p *sharedModuleActionProps) setModule(module *types.SharedModule) *sharedModuleActionProps {
p.module = module
return p
}
// setChanged updates sharedModuleActionProps's changed
//
// This function is auto-generated.
func (p *sharedModuleActionProps) setChanged(changed *types.SharedModule) *sharedModuleActionProps {
p.changed = changed
return p
}
// setFilter updates sharedModuleActionProps's filter
//
// This function is auto-generated.
func (p *sharedModuleActionProps) setFilter(filter *types.SharedModuleFilter) *sharedModuleActionProps {
p.filter = filter
return p
}
// setNode updates sharedModuleActionProps's node
//
// This function is auto-generated.
func (p *sharedModuleActionProps) setNode(node *types.Node) *sharedModuleActionProps {
p.node = node
return p
}
// Serialize converts sharedModuleActionProps to actionlog.Meta
//
// This function is auto-generated.
func (p sharedModuleActionProps) Serialize() actionlog.Meta {
var (
m = make(actionlog.Meta)
)
if p.module != nil {
m.Set("module.ID", p.module.ID, true)
}
if p.changed != nil {
m.Set("changed.ID", p.changed.ID, true)
}
if p.filter != nil {
m.Set("filter.query", p.filter.Query, true)
m.Set("filter.sort", p.filter.Sort, true)
m.Set("filter.limit", p.filter.Limit, true)
}
if p.node != nil {
m.Set("node.ID", p.node.ID, true)
m.Set("node.Name", p.node.Name, true)
}
return m
}
// tr translates string and replaces meta value placeholder with values
//
// This function is auto-generated.
func (p sharedModuleActionProps) Format(in string, err error) string {
var (
pairs = []string{"{{err}}"}
// first non-empty string
fns = func(ii ...interface{}) string {
for _, i := range ii {
if s := fmt.Sprintf("%v", i); len(s) > 0 {
return s
}
}
return ""
}
)
if err != nil {
pairs = append(pairs, err.Error())
} else {
pairs = append(pairs, "nil")
}
if p.module != nil {
// replacement for "{{module}}" (in order how fields are defined)
pairs = append(
pairs,
"{{module}}",
fns(
p.module.ID,
),
)
pairs = append(pairs, "{{module.ID}}", fns(p.module.ID))
}
if p.changed != nil {
// replacement for "{{changed}}" (in order how fields are defined)
pairs = append(
pairs,
"{{changed}}",
fns(
p.changed.ID,
),
)
pairs = append(pairs, "{{changed.ID}}", fns(p.changed.ID))
}
if p.filter != nil {
// replacement for "{{filter}}" (in order how fields are defined)
pairs = append(
pairs,
"{{filter}}",
fns(
p.filter.Query,
p.filter.Sort,
p.filter.Limit,
),
)
pairs = append(pairs, "{{filter.query}}", fns(p.filter.Query))
pairs = append(pairs, "{{filter.sort}}", fns(p.filter.Sort))
pairs = append(pairs, "{{filter.limit}}", fns(p.filter.Limit))
}
if p.node != nil {
// replacement for "{{node}}" (in order how fields are defined)
pairs = append(
pairs,
"{{node}}",
fns(
p.node.ID,
p.node.Name,
),
)
pairs = append(pairs, "{{node.ID}}", fns(p.node.ID))
pairs = append(pairs, "{{node.Name}}", fns(p.node.Name))
}
return strings.NewReplacer(pairs...).Replace(in)
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// Action methods
// String returns loggable description as string
//
// This function is auto-generated.
func (a *sharedModuleAction) String() string {
var props = &sharedModuleActionProps{}
if a.props != nil {
props = a.props
}
return props.Format(a.log, nil)
}
func (e *sharedModuleAction) ToAction() *actionlog.Action {
return &actionlog.Action{
Resource: e.resource,
Action: e.action,
Severity: e.severity,
Description: e.String(),
Meta: e.props.Serialize(),
}
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// Action constructors
// SharedModuleActionSearch returns "federation:shared_module.search" action
//
// This function is auto-generated.
func SharedModuleActionSearch(props ...*sharedModuleActionProps) *sharedModuleAction {
a := &sharedModuleAction{
timestamp: time.Now(),
resource: "federation:shared_module",
action: "search",
log: "searched for modules",
severity: actionlog.Info,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// SharedModuleActionLookup returns "federation:shared_module.lookup" action
//
// This function is auto-generated.
func SharedModuleActionLookup(props ...*sharedModuleActionProps) *sharedModuleAction {
a := &sharedModuleAction{
timestamp: time.Now(),
resource: "federation:shared_module",
action: "lookup",
log: "looked-up for a {{module}}",
severity: actionlog.Info,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// SharedModuleActionCreate returns "federation:shared_module.create" action
//
// This function is auto-generated.
func SharedModuleActionCreate(props ...*sharedModuleActionProps) *sharedModuleAction {
a := &sharedModuleAction{
timestamp: time.Now(),
resource: "federation:shared_module",
action: "create",
log: "created {{module}}",
severity: actionlog.Notice,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// SharedModuleActionUpdate returns "federation:shared_module.update" action
//
// This function is auto-generated.
func SharedModuleActionUpdate(props ...*sharedModuleActionProps) *sharedModuleAction {
a := &sharedModuleAction{
timestamp: time.Now(),
resource: "federation:shared_module",
action: "update",
log: "updated {{module}}",
severity: actionlog.Notice,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// SharedModuleActionDelete returns "federation:shared_module.delete" action
//
// This function is auto-generated.
func SharedModuleActionDelete(props ...*sharedModuleActionProps) *sharedModuleAction {
a := &sharedModuleAction{
timestamp: time.Now(),
resource: "federation:shared_module",
action: "delete",
log: "deleted {{module}}",
severity: actionlog.Notice,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// SharedModuleActionUndelete returns "federation:shared_module.undelete" action
//
// This function is auto-generated.
func SharedModuleActionUndelete(props ...*sharedModuleActionProps) *sharedModuleAction {
a := &sharedModuleAction{
timestamp: time.Now(),
resource: "federation:shared_module",
action: "undelete",
log: "undeleted {{module}}",
severity: actionlog.Notice,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// Error constructors
// SharedModuleErrGeneric returns "federation:shared_module.generic" as *errors.Error
//
// This function is auto-generated.
func SharedModuleErrGeneric(mm ...*sharedModuleActionProps) *errors.Error {
var p = &sharedModuleActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("failed to complete request due to internal error", nil),
errors.Meta("type", "generic"),
errors.Meta("resource", "federation:shared_module"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(sharedModuleLogMetaKey{}, "{err}"),
errors.Meta(sharedModulePropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
errors.Meta(locale.ErrorMetaKey{}, "shared-module.errors.generic"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// SharedModuleErrNotFound returns "federation:shared_module.notFound" as *errors.Error
//
// This function is auto-generated.
func SharedModuleErrNotFound(mm ...*sharedModuleActionProps) *errors.Error {
var p = &sharedModuleActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("module does not exist", nil),
errors.Meta("type", "notFound"),
errors.Meta("resource", "federation:shared_module"),
errors.Meta(sharedModulePropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
errors.Meta(locale.ErrorMetaKey{}, "shared-module.errors.notFound"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// SharedModuleErrInvalidID returns "federation:shared_module.invalidID" as *errors.Error
//
// This function is auto-generated.
func SharedModuleErrInvalidID(mm ...*sharedModuleActionProps) *errors.Error {
var p = &sharedModuleActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("invalid ID", nil),
errors.Meta("type", "invalidID"),
errors.Meta("resource", "federation:shared_module"),
errors.Meta(sharedModulePropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
errors.Meta(locale.ErrorMetaKey{}, "shared-module.errors.invalidID"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// SharedModuleErrStaleData returns "federation:shared_module.staleData" as *errors.Error
//
// This function is auto-generated.
func SharedModuleErrStaleData(mm ...*sharedModuleActionProps) *errors.Error {
var p = &sharedModuleActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("module was modified by someone else or by a workflow after you've opened it. Please refresh to see the latest updated version", nil),
errors.Meta("type", "staleData"),
errors.Meta("resource", "federation:shared_module"),
errors.Meta(sharedModulePropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
errors.Meta(locale.ErrorMetaKey{}, "shared-module.errors.staleData"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// SharedModuleErrFederationSyncStructureChanged returns "federation:shared_module.federationSyncStructureChanged" as *errors.Error
//
// This function is auto-generated.
func SharedModuleErrFederationSyncStructureChanged(mm ...*sharedModuleActionProps) *errors.Error {
var p = &sharedModuleActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("module structure changed", nil),
errors.Meta("type", "federationSyncStructureChanged"),
errors.Meta("resource", "federation:shared_module"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(sharedModuleLogMetaKey{}, "could not update shared module, structure different"),
errors.Meta(sharedModulePropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
errors.Meta(locale.ErrorMetaKey{}, "shared-module.errors.federationSyncStructureChanged"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// SharedModuleErrNotUnique returns "federation:shared_module.notUnique" as *errors.Error
//
// This function is auto-generated.
func SharedModuleErrNotUnique(mm ...*sharedModuleActionProps) *errors.Error {
var p = &sharedModuleActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("node not unique", nil),
errors.Meta("type", "notUnique"),
errors.Meta("resource", "federation:shared_module"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(sharedModuleLogMetaKey{}, "used duplicate node TODO"),
errors.Meta(sharedModulePropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
errors.Meta(locale.ErrorMetaKey{}, "shared-module.errors.notUnique"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// SharedModuleErrNodeNotFound returns "federation:shared_module.nodeNotFound" as *errors.Error
//
// This function is auto-generated.
func SharedModuleErrNodeNotFound(mm ...*sharedModuleActionProps) *errors.Error {
var p = &sharedModuleActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("node does not exist", nil),
errors.Meta("type", "nodeNotFound"),
errors.Meta("resource", "federation:shared_module"),
errors.Meta(sharedModulePropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
errors.Meta(locale.ErrorMetaKey{}, "shared-module.errors.nodeNotFound"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// SharedModuleErrNotAllowedToCreate returns "federation:shared_module.notAllowedToCreate" as *errors.Error
//
// This function is auto-generated.
func SharedModuleErrNotAllowedToCreate(mm ...*sharedModuleActionProps) *errors.Error {
var p = &sharedModuleActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("not allowed to create modules", nil),
errors.Meta("type", "notAllowedToCreate"),
errors.Meta("resource", "federation:shared_module"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(sharedModuleLogMetaKey{}, "could not create modules; insufficient permissions"),
errors.Meta(sharedModulePropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
errors.Meta(locale.ErrorMetaKey{}, "shared-module.errors.notAllowedToCreate"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// SharedModuleErrNotAllowedToManage returns "federation:shared_module.notAllowedToManage" as *errors.Error
//
// This function is auto-generated.
func SharedModuleErrNotAllowedToManage(mm ...*sharedModuleActionProps) *errors.Error {
var p = &sharedModuleActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("not allowed to manage this module", nil),
errors.Meta("type", "notAllowedToManage"),
errors.Meta("resource", "federation:shared_module"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(sharedModuleLogMetaKey{}, "could not read {{module}}; insufficient permissions"),
errors.Meta(sharedModulePropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
errors.Meta(locale.ErrorMetaKey{}, "shared-module.errors.notAllowedToManage"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// SharedModuleErrNotAllowedToMap returns "federation:shared_module.notAllowedToMap" as *errors.Error
//
// This function is auto-generated.
func SharedModuleErrNotAllowedToMap(mm ...*sharedModuleActionProps) *errors.Error {
var p = &sharedModuleActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("not allowed to map this module", nil),
errors.Meta("type", "notAllowedToMap"),
errors.Meta("resource", "federation:shared_module"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(sharedModuleLogMetaKey{}, "could not map {{module}}; insufficient permissions"),
errors.Meta(sharedModulePropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "federation"),
errors.Meta(locale.ErrorMetaKey{}, "shared-module.errors.notAllowedToMap"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// recordAction is a service helper function wraps function that can return error
//
// It will wrap unrecognized/internal errors with generic errors.
//
// This function is auto-generated.
func (svc sharedModule) recordAction(ctx context.Context, props *sharedModuleActionProps, actionFn func(...*sharedModuleActionProps) *sharedModuleAction, err error) error {
if svc.actionlog == nil || actionFn == nil {
// action log disabled or no action fn passed, return error as-is
return err
} else if err == nil {
// action completed w/o error, record it
svc.actionlog.Record(ctx, actionFn(props).ToAction())
return nil
}
a := actionFn(props).ToAction()
// Extracting error information and recording it as action
a.Error = err.Error()
switch c := err.(type) {
case *errors.Error:
m := c.Meta()
a.Error = err.Error()
a.Severity = actionlog.Severity(m.AsInt("severity"))
a.Description = props.Format(m.AsString(sharedModuleLogMetaKey{}), err)
if p, has := m[sharedModulePropsMetaKey{}]; has {
a.Meta = p.(*sharedModuleActionProps).Serialize()
}
svc.actionlog.Record(ctx, a)
default:
svc.actionlog.Record(ctx, a)
}
// Original error is passed on
return err
}

View File

@@ -0,0 +1,86 @@
# List of loggable service actions
resource: federation:shared_module
service: sharedModule
# Default sensitivity for actions
defaultActionSeverity: notice
# default severity for errors
defaultErrorSeverity: error
import:
- github.com/cortezaproject/corteza/server/federation/types
props:
- name: module
type: "*types.SharedModule"
fields: [ ID ]
- name: changed
type: "*types.SharedModule"
fields: [ ID ]
- name: filter
type: "*types.SharedModuleFilter"
fields: [ query, sort, limit ]
- name: node
type: "*types.Node"
fields: [ ID, Name ]
actions:
- action: search
log: "searched for modules"
severity: info
- action: lookup
log: "looked-up for a {{module}}"
severity: info
- action: create
log: "created {{module}}"
- action: update
log: "updated {{module}}"
- action: delete
log: "deleted {{module}}"
- action: undelete
log: "undeleted {{module}}"
errors:
- error: notFound
message: "module does not exist"
severity: warning
- error: invalidID
message: "invalid ID"
severity: warning
- error: staleData
message: "module was modified by someone else or by a workflow after you've opened it. Please refresh to see the latest updated version"
severity: warning
- error: federationSyncStructureChanged
message: "module structure changed"
log: "could not update shared module, structure different"
- error: notUnique
message: "node not unique"
log: "used duplicate node TODO"
severity: warning
- error: nodeNotFound
message: "node does not exist"
severity: warning
- error: notAllowedToCreate
message: "not allowed to create modules"
log: "could not create modules; insufficient permissions"
- error: notAllowedToManage
message: "not allowed to manage this module"
log: "could not read {{module}}; insufficient permissions"
- error: notAllowedToMap
message: "not allowed to map this module"
log: "could not map {{module}}; insufficient permissions"

View File

@@ -0,0 +1,228 @@
package service
import (
"context"
"encoding/json"
"fmt"
"io"
"time"
cs "github.com/cortezaproject/corteza/server/compose/service"
ct "github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/federation/types"
"github.com/cortezaproject/corteza/server/pkg/auth"
"github.com/cortezaproject/corteza/server/pkg/filter"
ss "github.com/cortezaproject/corteza/server/system/service"
st "github.com/cortezaproject/corteza/server/system/types"
)
type (
Sync struct {
syncer *Syncer
mapper *Mapper
sharedModuleService SharedModuleService
composeRecordService cs.RecordService
systemUserService ss.UserService
systemRoleService ss.RoleService
}
)
func NewSync(s *Syncer, m *Mapper, sm SharedModuleService, cs cs.RecordService, us ss.UserService, rs ss.RoleService) *Sync {
return &Sync{
syncer: s,
mapper: m,
sharedModuleService: sm,
composeRecordService: cs,
systemUserService: us,
systemRoleService: rs,
}
}
// CanUpdateSharedModule checks the origin and destination module
// compatibility of fields
// It currently checks if all of the fields are exactly the same
// TODO - check if any of the newly missing fields are actually being used so a safe update
// is possible
func (s *Sync) CanUpdateSharedModule(ctx context.Context, new *types.SharedModule, existing *types.SharedModule) (bool, error) {
// check for mapped fields
var (
fstr, f2str []byte
err error
)
if fstr, err = json.Marshal(new.Fields); err != nil {
return false, err
}
if f2str, err = json.Marshal(existing.Fields); err != nil {
return false, err
}
return string(fstr) == string(f2str), nil
}
// ProcessPayload passes the payload to the syncer lib
func (s *Sync) ProcessPayload(ctx context.Context, payload []byte, out chan Url, url types.SyncerURI, processer Processer) (ProcesserResponse, error) {
ctx = auth.SetIdentityToContext(ctx, auth.FederationUser())
return s.syncer.Process(ctx, payload, out, url, processer)
}
// QueueUrl passes the url to the syncer
func (s *Sync) QueueUrl(url Url, out chan Url) {
s.syncer.Queue(url, out)
}
// FetchUrl passes the url to be fetched to the syncer
func (s *Sync) FetchUrl(ctx context.Context, url string) (io.Reader, error) {
return s.syncer.Fetch(ctx, url)
}
// CreateRecord wraps the compose Record service Create
func (s *Sync) CreateRecord(ctx context.Context, rec *ct.Record) (out *ct.Record, err error) {
out, _, err = s.composeRecordService.Create(ctx, rec)
return
}
// UpdateRecord wraps the compose Record service Update
func (s *Sync) UpdateRecord(ctx context.Context, rec *ct.Record) (out *ct.Record, err error) {
out, _, err = s.composeRecordService.Update(ctx, rec)
return
}
// DeleteRecord wraps the compose Record service Update
func (s *Sync) DeleteRecord(ctx context.Context, rec *ct.Record) error {
return s.composeRecordService.DeleteByID(ctx, rec.NamespaceID, rec.ModuleID, rec.ID)
}
// FindRecord find the record via federation label
func (s *Sync) FindRecords(ctx context.Context, filter ct.RecordFilter) (set ct.RecordSet, err error) {
set, _, err = s.composeRecordService.Find(ctx, filter)
return
}
// LookupSharedModule find the shared module if exists
func (s *Sync) LookupSharedModule(ctx context.Context, new *types.SharedModule) (*types.SharedModule, error) {
var sm *types.SharedModule
list, _, err := s.sharedModuleService.Find(ctx, types.SharedModuleFilter{
NodeID: new.NodeID,
ExternalFederationModuleID: new.ExternalFederationModuleID})
if err != nil {
return nil, err
}
if len(list) == 0 {
return nil, nil
}
sm = list[0]
return sm, nil
}
// UpdateSharedModule wraps the federation SharedModule service Update
func (s *Sync) UpdateSharedModule(ctx context.Context, updated *types.SharedModule) (*types.SharedModule, error) {
return s.sharedModuleService.Update(ctx, updated)
}
// CreateSharedModule wraps the federation SharedModule service Create
func (s *Sync) CreateSharedModule(ctx context.Context, new *types.SharedModule) (*types.SharedModule, error) {
return s.sharedModuleService.Create(ctx, new)
}
// GetPairedNodes finds successfuly paired nodes
func (s *Sync) GetPairedNodes(ctx context.Context) (types.NodeSet, error) {
set, _, err := DefaultNode.Search(ctx, types.NodeFilter{Status: types.NodeStatusPaired})
if err != nil {
return nil, err
}
return set, nil
}
func (s *Sync) GetSharedModules(ctx context.Context, nodeID uint64) (types.SharedModuleSet, error) {
set, _, err := DefaultSharedModule.Find(ctx, types.SharedModuleFilter{NodeID: nodeID})
if err != nil {
return nil, err
}
set, _ = set.Filter(func(sm *types.SharedModule) (bool, error) {
return len(sm.Fields) > 0, nil
})
return set, nil
}
func (s *Sync) GetModuleMappings(ctx context.Context, moduleID uint64) (out *types.ModuleMapping, err error) {
out, err = DefaultModuleMapping.FindByID(ctx, moduleID)
return
}
func (s *Sync) PrepareModuleMappings(ctx context.Context, mappings *types.ModuleMapping) (ct.RecordValueSet, error) {
return s.mapper.Prepare((*mappings).FieldMapping), nil
}
func (s *Sync) GetLastStructureSyncStatus(ctx context.Context, nodeID, externalFederationModuleID uint64) (sync types.NodeSync, err error) {
var list types.NodeSyncSet
list, _, err = DefaultNodeSync.Search(ctx, types.NodeSyncFilter{
NodeID: nodeID,
ModuleID: externalFederationModuleID,
SyncType: types.NodeSyncTypeStructure,
Sorting: filter.Sorting{
Sort: filter.SortExprSet{
&filter.SortExpr{Column: "time_of_action", Descending: true},
},
},
Paging: filter.Paging{Limit: 1},
})
if err != nil {
return
}
if len(list) == 0 {
return
}
return *list[0], nil
}
func (s *Sync) GetLastSyncTime(ctx context.Context, nodeID uint64, syncType string) (*time.Time, error) {
ns, err := DefaultNodeSync.LookupLastSuccessfulSync(ctx, nodeID, syncType)
if err != nil || ns == nil {
return nil, err
}
return &ns.TimeOfAction, nil
}
// LoadUserWithRoles gets the federation user, that was
// created at node pairing process
func (s *Sync) LoadUserWithRoles(ctx context.Context, nodeID uint64) (*st.User, error) {
var (
u *st.User
err error
)
// get the federated user, associated for this node
if u, err = s.systemUserService.FindByHandle(ctx, fmt.Sprintf("federation_%d", nodeID)); err != nil {
return nil, err
}
// attach the roles
rr, _, err := s.systemRoleService.Find(ctx, st.RoleFilter{MemberID: u.ID})
if err != nil {
return nil, err
}
u.SetRoles(rr.IDs()...)
return u, nil
}

View File

@@ -0,0 +1,18 @@
package service
import (
"context"
"time"
)
type (
worker interface {
PrepareForNodes(ctx context.Context, sync *Sync, urls chan Url)
Watch(ctx context.Context, delay time.Duration, limit int)
}
)
const (
defaultDelay = time.Second * 10
defaultPage = 10
)

View File

@@ -0,0 +1,281 @@
package service
import (
"context"
"fmt"
"io"
"time"
"github.com/cortezaproject/corteza/server/federation/types"
"github.com/cortezaproject/corteza/server/pkg/auth"
"github.com/cortezaproject/corteza/server/pkg/logger"
"go.uber.org/zap"
)
type (
syncWorkerData struct {
syncService *Sync
logger *zap.Logger
delay time.Duration
limit int
}
)
func WorkerData(sync *Sync, logger *zap.Logger) *syncWorkerData {
return &syncWorkerData{
syncService: sync,
logger: logger,
limit: defaultPage,
delay: defaultDelay,
}
}
func (w *syncWorkerData) queueUrl(url *types.SyncerURI, urls chan Url, meta Processer) {
t := Url{
Url: *url,
Meta: meta,
}
w.syncService.QueueUrl(t, urls)
}
func (w *syncWorkerData) PrepareForNodes(ctx context.Context, urls chan Url) {
nodes, err := w.syncService.GetPairedNodes(ctx)
if err != nil {
w.logger.Info("could not get paired nodes", zap.Error(err))
return
}
// get all shared modules and their module mappings
for _, n := range nodes {
// get the user, associated for this node
u, err := w.syncService.LoadUserWithRoles(ctx, n.ID)
if err != nil {
w.logger.Info("could not preload federation user, skipping",
logger.Uint64("nodeID", n.ID),
zap.Error(err))
continue
}
set, err := w.syncService.GetSharedModules(ctx, n.ID)
if err != nil {
w.logger.Info("could not get shared modules, skipping",
logger.Uint64("nodeID", n.ID),
zap.Error(err))
continue
}
if len(set) == 0 {
w.logger.Info("there are no valid shared modules, skipping",
logger.Uint64("nodeID", n.ID),
zap.Error(err))
continue
}
// go through set and prepare module mappings for it
for _, sm := range set {
z := []zap.Field{
logger.Uint64("nodeID", n.ID),
logger.Uint64("moduleID", sm.ID),
zap.String("host", n.BaseURL),
zap.String("delay", w.delay.String()),
zap.Int("pagesize", w.limit),
}
// TODO: rename lastSyncStatus to lastSybc prolly!!!!
// if the last sync was error'd, log and skip
lastSyncStatus, err := w.syncService.GetLastStructureSyncStatus(ctx, n.ID, sm.ExternalFederationModuleID)
if err != nil {
w.logger.Info("could not get last sync status, skipping", z...)
continue
}
if lastSyncStatus.SyncStatus == types.NodeSyncStatusError {
w.logger.Info("last structure sync was not complete, admin resolve needed, skipping", z...)
continue
}
mappings, _ := w.syncService.GetModuleMappings(ctx, sm.ID)
if mappings == nil {
w.logger.Info("could not prepare module mappings for shared module, skipping", z...)
continue
}
mappingValues, err := w.syncService.PrepareModuleMappings(ctx, mappings)
if err != nil || mappingValues == nil {
w.logger.Info("could not prepare module mappings for shared module, skipping", z...)
continue
}
// get the last sync per-node
lastSync, _ := w.syncService.GetLastSyncTime(ctx, n.ID, types.NodeSyncTypeData)
// todo this is where we get the last sync value. prolly the issue could be on the fetch!
basePath := fmt.Sprintf("/nodes/%d/modules/%d/records/", n.SharedNodeID, sm.ExternalFederationModuleID)
if lastSync != nil {
z = append(z, zap.Time("lastSync", *lastSync))
}
w.logger.Info("starting data sync", z...)
url := types.SyncerURI{
BaseURL: n.BaseURL,
Path: basePath,
Limit: w.limit,
LastSync: lastSync,
}
processer := &dataProcesser{
ID: sm.ExternalFederationModuleID,
ComposeModuleID: mappings.ComposeModuleID,
ComposeNamespaceID: mappings.ComposeNamespaceID,
ModuleMappings: &mappings.FieldMapping,
ModuleMappingValues: &mappingValues,
SyncService: w.syncService,
User: u,
Node: n,
}
go w.queueUrl(&url, urls, processer)
}
}
}
func (w *syncWorkerData) Watch(ctx context.Context, delay time.Duration, limit int) {
var (
urls = make(chan Url, 100)
payloads = make(chan Payload, 10)
countProcess = 0
)
w.delay = delay
w.limit = limit
ctx, cancel := context.WithCancel(ctx)
defer cancel()
ctx = auth.SetIdentityToContext(ctx, auth.FederationUser())
ticker := time.NewTicker(delay)
w.PrepareForNodes(ctx, urls)
for {
select {
case <-ctx.Done():
// do any cleanup here
w.logger.Info("stopping sync", zap.Int("processed", countProcess))
return
case <-ticker.C:
// do the whole process again
w.PrepareForNodes(ctx, urls)
case url := <-urls:
select {
case <-ctx.Done():
// do any cleanup here
w.logger.Info("stopping sync", zap.Int("processed", countProcess))
return
default:
}
s, _ := url.Url.String()
meta := url.Meta.(*dataProcesser)
// use the authToken from node pairing
ctx = context.WithValue(ctx, FederationUserToken, meta.Node.AuthToken)
// TODO: Implement logic to handle last sync time before sending request
responseBody, err := w.syncService.FetchUrl(ctx, s)
if err != nil {
w.logger.Error("could not fetch data from url, skipping",
zap.Error(err),
zap.String("url", s),
logger.Uint64("nodeID", meta.Node.ID),
zap.String("host", meta.Node.BaseURL))
continue
}
spayload := Payload{
Payload: responseBody,
Meta: url.Meta,
}
payloads <- spayload
case p := <-payloads:
meta := p.Meta.(*dataProcesser)
body, err := io.ReadAll(p.Payload)
// handle error
if err != nil {
w.logger.Error("could not read data from synced url, skipping",
zap.Error(err),
logger.Uint64("nodeID", meta.Node.ID),
zap.String("host", meta.Node.BaseURL))
continue
}
basePath := fmt.Sprintf("/nodes/%d/modules/%d/records/", meta.Node.SharedNodeID, meta.ID)
u := types.SyncerURI{
BaseURL: meta.NodeBaseURL,
Path: basePath,
Limit: w.limit,
}
// the base path is okay
processed, errProcess := w.syncService.ProcessPayload(ctx, body, urls, u, meta)
if response, ok := processed.(dataProcesserResponse); ok {
countProcess += response.Processed
} else {
w.logger.Error("unexpected response type", zap.Any("response", processed))
}
// error raised before the actual persist process
// ignore
syncStatus := types.NodeSyncStatusSuccess
if errProcess != nil {
syncStatus = types.NodeSyncStatusError
}
new := &types.NodeSync{
NodeID: meta.Node.ID,
ModuleID: meta.ID,
SyncStatus: syncStatus,
SyncType: types.NodeSyncTypeData,
TimeOfAction: time.Now().UTC(),
}
new, err = DefaultNodeSync.Create(ctx, new)
if err != nil {
w.logger.Info("could not update sync status", zap.Error(err))
}
if errProcess != nil {
w.logger.Info("error on persisting structure", zap.Error(errProcess))
} else {
w.logger.Info("processed objects",
zap.Int("processed", processed.(dataProcesserResponse).Processed),
logger.Uint64("nodeID", meta.Node.ID))
}
}
}
}

View File

@@ -0,0 +1,230 @@
package service
import (
"context"
"fmt"
"io/ioutil"
"time"
"github.com/cortezaproject/corteza/server/federation/types"
"github.com/cortezaproject/corteza/server/pkg/auth"
"github.com/cortezaproject/corteza/server/pkg/logger"
"go.uber.org/zap"
)
type (
syncWorkerStructure struct {
syncService *Sync
logger *zap.Logger
delay time.Duration
limit int
}
)
func WorkerStructure(sync *Sync, logger *zap.Logger) *syncWorkerStructure {
return &syncWorkerStructure{
syncService: sync,
logger: logger,
limit: defaultPage,
delay: defaultDelay,
}
}
func (w *syncWorkerStructure) queueUrl(url *types.SyncerURI, urls chan Url, meta Processer) {
t := Url{
Url: *url,
Meta: meta,
}
w.syncService.QueueUrl(t, urls)
}
func (w *syncWorkerStructure) PrepareForNodes(ctx context.Context, urls chan Url) {
nodes, err := w.syncService.GetPairedNodes(ctx)
if err != nil {
return
}
// get all shared modules and their module mappings
for _, n := range nodes {
// get the user, associated for this node
u, err := w.syncService.LoadUserWithRoles(ctx, n.ID)
if err != nil {
w.logger.Info("could not preload federation user, skipping",
logger.Uint64("nodeID", n.ID),
zap.Error(err))
continue
}
// get the last sync per-node
lastSync, _ := w.syncService.GetLastSyncTime(ctx, n.ID, types.NodeSyncTypeStructure)
basePath := fmt.Sprintf("/nodes/%d/modules/exposed/", n.SharedNodeID)
z := []zap.Field{
logger.Uint64("nodeID", n.ID),
zap.String("host", n.BaseURL),
zap.String("delay", w.delay.String()),
zap.Int("pagesize", w.limit),
}
if lastSync != nil {
z = append(z, zap.Time("lastSync", *lastSync))
}
w.logger.Info("starting structure sync", z...)
url := types.SyncerURI{
BaseURL: n.BaseURL,
Path: basePath,
Limit: w.limit,
LastSync: lastSync,
}
processer := &structureProcesser{
SyncService: w.syncService,
User: u,
Node: n,
}
go w.queueUrl(&url, urls, processer)
}
}
func (w *syncWorkerStructure) Watch(ctx context.Context, delay time.Duration, limit int) {
var (
urls = make(chan Url, 100)
payloads = make(chan Payload, 10)
countProcess = 0
)
w.delay = delay
w.limit = limit
ctx, cancel := context.WithCancel(ctx)
defer cancel()
ctx = auth.SetIdentityToContext(ctx, auth.FederationUser())
ticker := time.NewTicker(delay)
w.PrepareForNodes(ctx, urls)
for {
select {
case <-ctx.Done():
// do any cleanup here
w.logger.Info("stopping sync", zap.Int("processed", countProcess))
return
case <-ticker.C:
// do the whole process again
w.PrepareForNodes(ctx, urls)
case url := <-urls:
select {
case <-ctx.Done():
// do any cleanup here
w.logger.Info("stopping sync", zap.Int("processed", countProcess))
return
default:
}
s, _ := url.Url.String()
meta := url.Meta.(*structureProcesser)
// use the authToken from node pairing
ctx = context.WithValue(ctx, FederationUserToken, meta.Node.AuthToken)
responseBody, err := w.syncService.FetchUrl(ctx, s)
if err != nil {
w.logger.Error("could not fetch data from url, skipping",
zap.Error(err),
zap.String("url", s),
logger.Uint64("nodeID", meta.Node.ID),
zap.String("host", meta.Node.BaseURL))
continue
}
spayload := Payload{
Payload: responseBody,
Meta: url.Meta,
}
payloads <- spayload
case p := <-payloads:
meta := p.Meta.(*structureProcesser)
body, err := ioutil.ReadAll(p.Payload)
// handle error
if err != nil {
w.logger.Error("could not read data from synced url, skipping",
zap.Error(err),
logger.Uint64("nodeID", meta.Node.ID),
zap.String("host", meta.Node.BaseURL))
continue
}
basePath := fmt.Sprintf("/nodes/%d/modules/exposed/", meta.Node.SharedNodeID)
u := types.SyncerURI{
BaseURL: p.Meta.(*structureProcesser).Node.BaseURL,
Path: basePath,
Limit: w.limit,
}
processed, errProcess := w.syncService.ProcessPayload(ctx, body, urls, u, meta)
var processedModID uint64
switch v := processed.(type) {
case structureProcesserResponse:
processedModID = v.ModuleID
countProcess += v.Processed
case int:
countProcess += v
}
// error raised before the actual persist process
// ignore
if moduleID := processedModID; moduleID == 0 {
w.logger.Info("no structure change from last change, skipping",
logger.Uint64("nodeID", meta.Node.ID),
zap.String("host", meta.Node.BaseURL))
continue
}
syncStatus := types.NodeSyncStatusSuccess
if errProcess != nil {
syncStatus = types.NodeSyncStatusError
}
new := &types.NodeSync{
NodeID: meta.Node.ID,
ModuleID: processedModID,
SyncStatus: syncStatus,
SyncType: types.NodeSyncTypeStructure,
TimeOfAction: time.Now().UTC(),
}
new, err = DefaultNodeSync.Create(ctx, new)
if err != nil {
w.logger.Info("could not update sync status", zap.Error(err))
}
if errProcess != nil {
w.logger.Info("error on handling payload", zap.Error(errProcess))
} else {
w.logger.Info("processed objects",
zap.Int("processed", processed.(structureProcesserResponse).Processed),
logger.Uint64("nodeID", meta.Node.ID),
zap.String("host", meta.Node.BaseURL))
}
}
}
}

View File

@@ -0,0 +1,102 @@
package service
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"time"
"github.com/cortezaproject/corteza/server/federation/types"
)
type (
Syncer struct {
client http.Client
}
Url struct {
Url types.SyncerURI
Meta Processer
}
Payload struct {
Payload io.Reader
Meta Processer
}
AuxResponseSet struct {
Response struct {
Filter struct {
NodeID string
ComposeModuleID string
Query string
Limit int
NextPage string
PrevPage string
} `json:"filter"`
} `json:"response"`
}
)
const FederationUserToken string = "authToken"
func (h *Syncer) Queue(url Url, out chan Url) {
out <- url
}
func (h *Syncer) Fetch(ctx context.Context, url string) (io.Reader, error) {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
if authToken := ctx.Value(FederationUserToken); authToken != nil {
req.Header.Add("Authorization", `Bearer `+authToken.(string))
}
req.Header.Add("Accept", "application/json")
resp, err := h.client.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode != 200 {
return nil, errors.New(fmt.Sprintf("invalid return status: %d", resp.StatusCode))
}
return resp.Body, nil
}
func (h *Syncer) Process(ctx context.Context, payload []byte, out chan Url, url types.SyncerURI, processer Processer) (ProcesserResponse, error) {
aux, err := h.ParseHeader(ctx, payload)
if err != nil {
return 0, err
}
if aux.Response.Filter.NextPage != "" {
url.NextPage = aux.Response.Filter.NextPage
out <- Url{
Url: url,
Meta: processer,
}
}
return processer.Process(ctx, payload)
}
func (h *Syncer) ParseHeader(ctx context.Context, payload []byte) (aux AuxResponseSet, err error) {
err = json.Unmarshal(payload, &aux)
return
}
func NewSyncer() *Syncer {
return &Syncer{
client: http.Client{
Timeout: time.Duration(3) * time.Second,
},
}
}

View File

@@ -0,0 +1,161 @@
package service
import (
"bytes"
"context"
"io/ioutil"
"net/http"
"testing"
"github.com/cortezaproject/corteza/server/federation/types"
"github.com/stretchr/testify/require"
)
type (
RoundTripFunc func(req *http.Request) *http.Response
testProcesser struct{}
)
func (p *testProcesser) Process(ctx context.Context, payload []byte) (ProcesserResponse, error) {
return 0, nil
}
func TestSyncer_process(t *testing.T) {
var (
req = require.New(t)
syncer = &Syncer{}
ctx = context.Background()
)
c := make(chan Url, 2)
u := types.SyncerURI{
Limit: 10,
NextPage: "123",
}
tp := &testProcesser{}
go syncer.Process(ctx, []byte(`{"response":{"filter":{"limit":1, "nextPage":"456"}}}`), c, u, tp)
select {
case url := <-c:
req.Equal(url.Url.NextPage, "456")
break
}
}
func TestSyncer_parseHeader(t *testing.T) {
var (
req = require.New(t)
syncer = NewSyncer()
ctx = context.Background()
)
c := make(chan Url, 2)
u := types.SyncerURI{
Limit: 10,
NextPage: "123",
}
tp := &testProcesser{}
n, err := syncer.Process(ctx, []byte(`{"response":{"filt`), c, u, tp)
req.EqualError(err, "unexpected end of JSON input")
req.Equal(n, 0)
}
func TestSyncer_response(t *testing.T) {
var (
req = require.New(t)
)
tests := []struct {
name string
url string
expect string
syncer *Syncer
ctx context.Context
}{
{
name: "non 200 request",
url: "http://example.ltd",
syncer: &Syncer{
client: *NewHttpClient(func(req *http.Request) *http.Response {
return &http.Response{
StatusCode: 500,
Body: ioutil.NopCloser(bytes.NewBufferString(`{"error":{"message":"common error response"}}`)),
Header: make(http.Header),
}
}),
},
ctx: context.Background(),
expect: "invalid return status: 500",
},
{
name: "invalid hostname",
url: "http://examp le.ltd",
syncer: &Syncer{
client: *NewHttpClient(func(req *http.Request) *http.Response {
return &http.Response{
StatusCode: 200,
Body: ioutil.NopCloser(bytes.NewBufferString(`{"error":{"message":"common error response"}}`)),
Header: make(http.Header),
}
}),
},
ctx: context.Background(),
expect: "parse \"http://examp le.ltd\": invalid character \" \" in host name",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := tt.syncer.Fetch(tt.ctx, tt.url)
req.EqualError(err, tt.expect)
})
}
}
func TestSyncer_authTokenCorrectlySet(t *testing.T) {
var (
req = require.New(t)
expectedAuthToken = "TEST_JWT_TOKEN"
actualAuthToken = ""
)
ctx := context.WithValue(
context.Background(),
FederationUserToken,
expectedAuthToken)
syncer := &Syncer{
client: *NewHttpClient(func(r *http.Request) *http.Response {
actualAuthToken = r.Header.Get("Authorization")
return &http.Response{
StatusCode: 200,
Body: ioutil.NopCloser(bytes.NewBufferString("OK")),
Header: make(http.Header),
}
}),
}
_, err := syncer.Fetch(ctx, "http://example.ltd")
req.Equal("Bearer "+expectedAuthToken, actualAuthToken)
req.NoError(err)
}
func (f RoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
return f(req), nil
}
func NewHttpClient(rt RoundTripFunc) *http.Client {
return &http.Client{
Transport: RoundTripFunc(rt),
}
}