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

2
server/pkg/corredor/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
*.zip
*.proto

View File

@@ -0,0 +1,43 @@
.PHONY: generate
GOPATH ?= $(go env GOPATH)
BRANCH ?= develop
ZIP ?= $(BRANCH).zip
DIR ?= corteza-protobuf-$(BRANCH)
PROTO ?= service-corredor.proto
URL ?= https://github.com/cortezaproject/corteza-protobuf/archive/$(ZIP)
PARTS ?= compose system
OUTPUTS ?= $(addsuffix /static.go, $(PARTS))
PROTOC ?= /usr/local/bin/protoc
CURL ?= /usr/bin/curl
UNZIP ?= /usr/bin/unzip
PROTOGEN ?= $(GOPATH)/bin/protoc-gen-go
PROTOGEN_GRPC ?= $(GOPATH)/bin/protoc-gen-go-grpc
generate:
PATH=$(PATH):$(GOPATH)/bin protoc --go_out=. --go-grpc_out=. --go_opt=paths=source_relative --go-grpc_opt=paths=source_relative $(PROTO)
$(PROTO): $(ZIP)
$(UNZIP) -qq -o $(ZIP)
mv $(DIR)/$(PROTO) $(PROTO)
rm -rf $(DIR)
$(ZIP):
curl -o $(ZIP) -s --location "$(URL)"
clear:
rm -f $(ZIP)
deps: $(PROTOC) $(PROTOGEN) $(PROTOGEN_GRPC)
$(PROTOC):
$(MAKE) --directory=../../ $(PROTOC)
$(PROTOGEN):
$(MAKE) --directory=../../ $(PROTOGEN)
$(PROTOGEN_GRPC):
$(MAKE) --directory=../../ $(PROTOGEN_GRPC)

View File

@@ -0,0 +1,8 @@
# pkg/corredor
This package is an interface to Corredor script runner service.
.It provides:
- Connection to Corredor service
- Subscriber service to handle events raised from Corteza services
- Scheduling service for deferred events (scheduled, interval)

View File

@@ -0,0 +1,27 @@
package corredor
import (
"github.com/cortezaproject/corteza/server/system/types"
)
type (
userLookupCache struct {
err error
user *types.User
}
userLookupCacheMap map[string]userLookupCache
)
func (m userLookupCacheMap) lookup(key string, lookup func() (*types.User, error)) (*types.User, error) {
if c, ok := m[key]; ok {
return c.user, c.err
}
c := userLookupCache{}
c.user, c.err = lookup()
m[key] = c
return c.user, c.err
}

View File

@@ -0,0 +1,99 @@
package corredor
import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"io/ioutil"
"os"
"github.com/cortezaproject/corteza/server/pkg/options"
"go.uber.org/zap"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)
// Corredor standard connector to Corredor service via gRPC
func NewConnection(ctx context.Context, opt options.CorredorOpt, logger *zap.Logger) (c *grpc.ClientConn, err error) {
log := logger.Named("conn")
if !opt.Enabled {
// Do not connect when script runner is not enabled
log.Info("corredor disabled (CORREDOR_ENABLED=false)")
return
}
var (
dialOpts = make([]grpc.DialOption, 0)
paths = map[string]string{
"corredor CA public key": opt.TlsCertCA,
"corredor client public key": opt.TlsCertPublic,
"corredor client private key": opt.TlsCertPrivate,
}
expl = "\n\n" +
"Check and change configured path to Corredor TLS certificates " +
"in env var CORREDOR_CLIENT_CERTIFICATES_PATH or disable " +
"this check with CORREDOR_CLIENT_CERTIFICATES_ENABLED=false" +
"\n\n"
)
// opt.PublicKey = ""
if opt.TlsCertEnabled {
// Check paths
log.Debug("connecting with TLS certificates enabled (CORREDOR_CLIENT_CERTIFICATES_ENABLED=true)")
for label, path := range paths {
if _, err = os.Stat(path); os.IsNotExist(err) {
return nil, fmt.Errorf("%s (%s) not found"+expl, label, path)
} else if err != nil {
return nil, fmt.Errorf("%s (%s) could not be loaded: %s"+expl, label, path, err)
}
}
// Load the certificates from disk
certificate, err := tls.LoadX509KeyPair(opt.TlsCertPublic, opt.TlsCertPrivate)
if err != nil {
return nil, fmt.Errorf("could not load client key pair: %s"+expl, err)
}
// Create a certificate pool from the certificate authority
certPool := x509.NewCertPool()
ca, err := ioutil.ReadFile(opt.TlsCertCA)
if err != nil {
return nil, fmt.Errorf("could not read ca certificate: %s"+expl, err)
}
// Append the client certificates from the CA
if ok := certPool.AppendCertsFromPEM(ca); !ok {
return nil, errors.New("failed to append ca certs" + expl)
}
crds := credentials.NewTLS(&tls.Config{
ServerName: opt.TlsServerName,
Certificates: []tls.Certificate{certificate},
RootCAs: certPool,
})
dialOpts = append(dialOpts, grpc.WithTransportCredentials(crds))
} else {
log.Debug("connecting without TLS certificates " +
"(this is OK if you are using private or internal docker network)")
dialOpts = append(dialOpts, grpc.WithInsecure())
}
if opt.MaxReceiveMessageSize > 0 {
dialOpts = append(dialOpts, grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(opt.MaxReceiveMessageSize)))
}
if opt.MaxBackoffDelay > 0 {
dialOpts = append(dialOpts, grpc.WithBackoffMaxDelay(opt.MaxBackoffDelay))
}
log.Info("connecting to corredor", zap.String("addr", opt.Addr))
return grpc.DialContext(ctx, opt.Addr, dialOpts...)
}

View File

@@ -0,0 +1,93 @@
package corredor
import (
"context"
"fmt"
"go.uber.org/zap"
"math/rand"
"net"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"google.golang.org/grpc"
"google.golang.org/grpc/connectivity"
"github.com/cortezaproject/corteza/server/pkg/options"
)
func init() {
rand.Seed(time.Now().UnixNano())
}
func TestNewConnectionWithDisabled(t *testing.T) {
c, err := NewConnection(nil, options.CorredorOpt{Enabled: false}, zap.NewNop())
assert.Nil(t, c)
assert.Nil(t, err)
}
func TestNewConnection(t *testing.T) {
var (
ctx = context.Background()
a = assert.New(t)
wg = &sync.WaitGroup{}
lstnr = openListener(t)
grpcServer = grpc.NewServer()
opt = options.CorredorOpt{
Enabled: true,
MaxBackoffDelay: 1,
Addr: lstnr.Addr().String(),
}
)
a.NotNil(lstnr)
defer lstnr.Close()
wg.Add(1)
go func() {
defer wg.Done()
if err := grpcServer.Serve(lstnr); err != nil && err != grpc.ErrServerStopped {
a.NoError(err)
}
}()
grpcClientConn, err := NewConnection(ctx, opt, zap.NewNop())
a.NoError(err)
// NewService(dbgLog, opt)
grpcClientConn.WaitForStateChange(ctx, connectivity.Ready)
grpcServer.GracefulStop()
lstnr.Close()
t.Log("waiting for connection to close")
wg.Wait()
}
func openListener(t *testing.T) (ln net.Listener) {
var (
tries = 1000
minPort = 50000
maxPort = 63000
port int
err error
)
for tries > 0 {
port = minPort + rand.Intn(maxPort-minPort)
t.Logf("trying to find available port for gRPC connection: %d", port)
ln, err = net.Listen("tcp", fmt.Sprintf("localhost:%d", port))
if err == nil {
return ln
}
t.Errorf("error: %s", err)
tries--
}
return nil
}

View File

@@ -0,0 +1,122 @@
package corredor
import (
"github.com/cortezaproject/corteza/server/pkg/slice"
"strings"
)
type (
Filter struct {
ResourceTypePrefixes []string `json:"resourceTypePrefixes"`
ResourceTypes []string `json:"resourceTypes"`
EventTypes []string `json:"eventTypes"`
ExcludeServerScripts bool `json:"excludeServerScripts"`
ExcludeClientScripts bool `json:"excludeClientScripts"`
ExcludeInvalid bool `json:"excludeInvalid"`
//Page uint `json:"page"`
//PerPage uint `json:"perPage"`
Count uint `json:"count"`
}
)
func (f Filter) makeFilterFn() func(s *Script) (b bool, err error) {
collectTypes := func(tt ...*Trigger) (rr []string, ee []string) {
for _, t := range tt {
rr = append(rr, t.ResourceTypes...)
ee = append(ee, t.EventTypes...)
}
return
}
return func(s *Script) (b bool, err error) {
if f.ExcludeInvalid {
if len(s.Errors) > 0 {
// Skip scripts with errors
return
}
}
var (
resourceTypes, eventTypes = collectTypes(s.Triggers...)
)
if !f.checkPrefixes(resourceTypes...) {
return
}
if !f.checkEventTypes(eventTypes...) {
return
}
if !f.checkResourceTypes(resourceTypes...) {
return
}
return true, nil
}
}
// parses resource type prefixes, adds ui: and service prefix if not present
func (f *Filter) procRTPrefixes(service string) {
var (
hasService, hasUi bool
)
if service == "" {
return
}
for i, rtp := range f.ResourceTypePrefixes {
if rtp == service {
hasService = true
}
if strings.HasPrefix(rtp, "ui") {
hasUi = true
}
// Check all requested resource-type prefixed and make sure
// all start with either "ui" or resourcePrefix + ":"
if !strings.HasPrefix(rtp, "ui:") && !strings.HasPrefix(rtp, service) {
f.ResourceTypePrefixes[i] = service + ":" + rtp
}
}
if !hasService {
f.ResourceTypePrefixes = append(f.ResourceTypePrefixes, service)
}
if !hasUi {
f.ResourceTypePrefixes = append(f.ResourceTypePrefixes, "ui:")
}
}
func (f Filter) checkPrefixes(rr ...string) bool {
if len(f.ResourceTypePrefixes) == 0 {
return true
}
if len(rr) == 0 {
return false
}
for _, p := range f.ResourceTypePrefixes {
for _, r := range rr {
if strings.HasPrefix(r, p) {
return true
}
}
}
return false
}
func (f Filter) checkResourceTypes(rr ...string) bool {
return len(f.ResourceTypes) == 0 || len(slice.IntersectStrings(f.ResourceTypes, rr)) > 0
}
func (f Filter) checkEventTypes(ee ...string) bool {
return len(f.EventTypes) == 0 || len(slice.IntersectStrings(f.EventTypes, ee)) > 0
}

View File

@@ -0,0 +1,138 @@
package corredor
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestFilterResourcePrefixing(t *testing.T) {
f := &Filter{
ResourceTypePrefixes: []string{
"system",
"system:one",
"two",
"ui:admin:foo",
"ui:admin",
},
}
f.procRTPrefixes("system")
assert.New(t).Equal(
f.ResourceTypePrefixes,
[]string{
"system",
"system:one",
"system:two",
"ui:admin:foo",
"ui:admin",
},
)
f = &Filter{}
f.procRTPrefixes("system")
assert.New(t).Equal(
f.ResourceTypePrefixes,
[]string{
"system",
"ui:",
},
)
}
func TestResourceTypesChecker(t *testing.T) {
var (
a = assert.New(t)
)
// nothing requested in the filter
a.True(Filter{ResourceTypes: []string{}}.checkResourceTypes("c"))
// no resources
a.False(Filter{ResourceTypes: []string{"a", "b"}}.checkResourceTypes())
// no resources, no filter
a.True(Filter{}.checkResourceTypes())
a.True(Filter{ResourceTypes: []string{"a"}}.checkResourceTypes("a", "b"))
a.False(Filter{ResourceTypes: []string{"c"}}.checkResourceTypes("a", "b"))
}
func TestEventTypesChecker(t *testing.T) {
var (
a = assert.New(t)
)
// nothing requested in the filter
a.True(Filter{EventTypes: []string{}}.checkEventTypes("c"))
// no resources
a.False(Filter{EventTypes: []string{"a", "b"}}.checkEventTypes())
// no resources, no filter
a.True(Filter{}.checkEventTypes())
a.True(Filter{EventTypes: []string{"a"}}.checkEventTypes("a", "b"))
a.False(Filter{EventTypes: []string{"c"}}.checkEventTypes("a", "b"))
}
func TestScriptFilterMaker(t *testing.T) {
var (
s1 = &Script{
Triggers: []*Trigger{
&Trigger{
EventTypes: []string{"ev"},
ResourceTypes: []string{"res"},
},
},
}
s2 = &Script{
Triggers: []*Trigger{
&Trigger{
EventTypes: []string{"foo"},
ResourceTypes: []string{"bar"},
},
},
}
s3 = &Script{
Triggers: []*Trigger{
&Trigger{
EventTypes: []string{"not-a-match"},
ResourceTypes: []string{"not-a-match"},
},
},
}
a = assert.New(t)
strip = func(b bool, _ error) bool { return b }
f func(s *Script) (b bool, err error)
)
f = Filter{}.makeFilterFn()
a.True(strip(f(s1)))
a.True(strip(f(s2)))
a.True(strip(f(s3)))
f = Filter{ResourceTypes: []string{"res"}}.makeFilterFn()
a.True(strip(f(s1)))
a.False(strip(f(s2)))
a.False(strip(f(s3)))
f = Filter{EventTypes: []string{"ev"}}.makeFilterFn()
a.True(strip(f(s1)))
a.False(strip(f(s2)))
a.False(strip(f(s3)))
f = Filter{EventTypes: []string{"ev", "foo"}}.makeFilterFn()
a.True(strip(f(s1)))
a.True(strip(f(s2)))
a.False(strip(f(s3)))
}

View File

@@ -0,0 +1,34 @@
package corredor
import (
"context"
"fmt"
"google.golang.org/grpc/connectivity"
)
// Healtcheck for global
func Healthcheck(_ context.Context) error {
svc := Service()
if svc == nil {
return fmt.Errorf("uninitialized")
}
if !svc.opt.Enabled {
return nil
}
if svc.conn == nil {
// working around edge-case where health-check is called
// but conn variable is not set;
// prevents nil pointer dereference error
return nil
}
if state := svc.conn.GetState(); state != connectivity.Ready {
return fmt.Errorf("connection is %s", state)
}
return nil
}

View File

@@ -0,0 +1,16 @@
package corredor
import (
"context"
"testing"
"github.com/cortezaproject/corteza/server/pkg/options"
"go.uber.org/zap"
)
// tested with
// go test -count 10 -race -run TestDataRace ./pkg/corredor/...
func TestDataRace(t *testing.T) {
go Setup(zap.NewNop(), options.CorredorOpt{})
go Healthcheck(context.Background())
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,306 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
package corredor
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
const _ = grpc.SupportPackageIsVersion7
// ServerScriptsClient is the client API for ServerScripts service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type ServerScriptsClient interface {
// Executes server script
Exec(ctx context.Context, in *ExecRequest, opts ...grpc.CallOption) (*ExecResponse, error)
// List of server scripts
List(ctx context.Context, in *ServerScriptListRequest, opts ...grpc.CallOption) (*ServerScriptListResponse, error)
}
type serverScriptsClient struct {
cc grpc.ClientConnInterface
}
func NewServerScriptsClient(cc grpc.ClientConnInterface) ServerScriptsClient {
return &serverScriptsClient{cc}
}
func (c *serverScriptsClient) Exec(ctx context.Context, in *ExecRequest, opts ...grpc.CallOption) (*ExecResponse, error) {
out := new(ExecResponse)
err := c.cc.Invoke(ctx, "/corredor.ServerScripts/Exec", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *serverScriptsClient) List(ctx context.Context, in *ServerScriptListRequest, opts ...grpc.CallOption) (*ServerScriptListResponse, error) {
out := new(ServerScriptListResponse)
err := c.cc.Invoke(ctx, "/corredor.ServerScripts/List", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// ServerScriptsServer is the server API for ServerScripts service.
// All implementations must embed UnimplementedServerScriptsServer
// for forward compatibility
type ServerScriptsServer interface {
// Executes server script
Exec(context.Context, *ExecRequest) (*ExecResponse, error)
// List of server scripts
List(context.Context, *ServerScriptListRequest) (*ServerScriptListResponse, error)
mustEmbedUnimplementedServerScriptsServer()
}
// UnimplementedServerScriptsServer must be embedded to have forward compatible implementations.
type UnimplementedServerScriptsServer struct {
}
func (UnimplementedServerScriptsServer) Exec(context.Context, *ExecRequest) (*ExecResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Exec not implemented")
}
func (UnimplementedServerScriptsServer) List(context.Context, *ServerScriptListRequest) (*ServerScriptListResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method List not implemented")
}
func (UnimplementedServerScriptsServer) mustEmbedUnimplementedServerScriptsServer() {}
// UnsafeServerScriptsServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to ServerScriptsServer will
// result in compilation errors.
type UnsafeServerScriptsServer interface {
mustEmbedUnimplementedServerScriptsServer()
}
func RegisterServerScriptsServer(s *grpc.Server, srv ServerScriptsServer) {
s.RegisterService(&_ServerScripts_serviceDesc, srv)
}
func _ServerScripts_Exec_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ExecRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ServerScriptsServer).Exec(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/corredor.ServerScripts/Exec",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ServerScriptsServer).Exec(ctx, req.(*ExecRequest))
}
return interceptor(ctx, in, info, handler)
}
func _ServerScripts_List_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ServerScriptListRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ServerScriptsServer).List(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/corredor.ServerScripts/List",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ServerScriptsServer).List(ctx, req.(*ServerScriptListRequest))
}
return interceptor(ctx, in, info, handler)
}
var _ServerScripts_serviceDesc = grpc.ServiceDesc{
ServiceName: "corredor.ServerScripts",
HandlerType: (*ServerScriptsServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "Exec",
Handler: _ServerScripts_Exec_Handler,
},
{
MethodName: "List",
Handler: _ServerScripts_List_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "service-corredor.proto",
}
// ClientScriptsClient is the client API for ClientScripts service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type ClientScriptsClient interface {
// Bundles
Bundle(ctx context.Context, in *BundleRequest, opts ...grpc.CallOption) (*BundleResponse, error)
// List of client scripts
List(ctx context.Context, in *ClientScriptListRequest, opts ...grpc.CallOption) (*ClientScriptListResponse, error)
}
type clientScriptsClient struct {
cc grpc.ClientConnInterface
}
func NewClientScriptsClient(cc grpc.ClientConnInterface) ClientScriptsClient {
return &clientScriptsClient{cc}
}
func (c *clientScriptsClient) Bundle(ctx context.Context, in *BundleRequest, opts ...grpc.CallOption) (*BundleResponse, error) {
out := new(BundleResponse)
err := c.cc.Invoke(ctx, "/corredor.ClientScripts/Bundle", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *clientScriptsClient) List(ctx context.Context, in *ClientScriptListRequest, opts ...grpc.CallOption) (*ClientScriptListResponse, error) {
out := new(ClientScriptListResponse)
err := c.cc.Invoke(ctx, "/corredor.ClientScripts/List", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// ClientScriptsServer is the server API for ClientScripts service.
// All implementations must embed UnimplementedClientScriptsServer
// for forward compatibility
type ClientScriptsServer interface {
// Bundles
Bundle(context.Context, *BundleRequest) (*BundleResponse, error)
// List of client scripts
List(context.Context, *ClientScriptListRequest) (*ClientScriptListResponse, error)
mustEmbedUnimplementedClientScriptsServer()
}
// UnimplementedClientScriptsServer must be embedded to have forward compatible implementations.
type UnimplementedClientScriptsServer struct {
}
func (UnimplementedClientScriptsServer) Bundle(context.Context, *BundleRequest) (*BundleResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Bundle not implemented")
}
func (UnimplementedClientScriptsServer) List(context.Context, *ClientScriptListRequest) (*ClientScriptListResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method List not implemented")
}
func (UnimplementedClientScriptsServer) mustEmbedUnimplementedClientScriptsServer() {}
// UnsafeClientScriptsServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to ClientScriptsServer will
// result in compilation errors.
type UnsafeClientScriptsServer interface {
mustEmbedUnimplementedClientScriptsServer()
}
func RegisterClientScriptsServer(s *grpc.Server, srv ClientScriptsServer) {
s.RegisterService(&_ClientScripts_serviceDesc, srv)
}
func _ClientScripts_Bundle_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(BundleRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ClientScriptsServer).Bundle(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/corredor.ClientScripts/Bundle",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ClientScriptsServer).Bundle(ctx, req.(*BundleRequest))
}
return interceptor(ctx, in, info, handler)
}
func _ClientScripts_List_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ClientScriptListRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ClientScriptsServer).List(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/corredor.ClientScripts/List",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ClientScriptsServer).List(ctx, req.(*ClientScriptListRequest))
}
return interceptor(ctx, in, info, handler)
}
var _ClientScripts_serviceDesc = grpc.ServiceDesc{
ServiceName: "corredor.ClientScripts",
HandlerType: (*ClientScriptsServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "Bundle",
Handler: _ClientScripts_Bundle_Handler,
},
{
MethodName: "List",
Handler: _ClientScripts_List_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "service-corredor.proto",
}
// StorageClient is the client API for Storage service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type StorageClient interface {
}
type storageClient struct {
cc grpc.ClientConnInterface
}
func NewStorageClient(cc grpc.ClientConnInterface) StorageClient {
return &storageClient{cc}
}
// StorageServer is the server API for Storage service.
// All implementations must embed UnimplementedStorageServer
// for forward compatibility
type StorageServer interface {
mustEmbedUnimplementedStorageServer()
}
// UnimplementedStorageServer must be embedded to have forward compatible implementations.
type UnimplementedStorageServer struct {
}
func (UnimplementedStorageServer) mustEmbedUnimplementedStorageServer() {}
// UnsafeStorageServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to StorageServer will
// result in compilation errors.
type UnsafeStorageServer interface {
mustEmbedUnimplementedStorageServer()
}
func RegisterStorageServer(s *grpc.Server, srv StorageServer) {
s.RegisterService(&_Storage_serviceDesc, srv)
}
var _Storage_serviceDesc = grpc.ServiceDesc{
ServiceName: "corredor.Storage",
HandlerType: (*StorageServer)(nil),
Methods: []grpc.MethodDesc{},
Streams: []grpc.StreamDesc{},
Metadata: "service-corredor.proto",
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,243 @@
package corredor
import (
"context"
"testing"
"github.com/cortezaproject/corteza/server/pkg/auth"
"github.com/cortezaproject/corteza/server/pkg/eventbus"
"github.com/cortezaproject/corteza/server/pkg/logger"
"github.com/cortezaproject/corteza/server/pkg/options"
"github.com/cortezaproject/corteza/server/system/types"
"github.com/stretchr/testify/assert"
"go.uber.org/zap"
)
type (
mockEvent struct {
rType string
eType string
match func(matcher eventbus.ConstraintMatcher) bool
}
mockUserSvc struct {
user *types.User
err error
}
mockRoleSvc struct {
role *types.Role
err error
}
)
func (e mockEvent) ResourceType() string {
return e.rType
}
func (e mockEvent) EventType() string {
return e.eType
}
func (e mockEvent) Encode() (map[string][]byte, error) {
return nil, nil
}
func (e mockEvent) Decode(map[string][]byte) error {
return nil
}
func (e mockEvent) Match(matcher eventbus.ConstraintMatcher) bool {
if e.match == nil {
return true
}
return e.match(matcher)
}
func (u *mockUserSvc) FindByAny(context.Context, interface{}) (*types.User, error) {
return u.user, u.err
}
func (u *mockRoleSvc) FindByAny(context.Context, interface{}) (*types.Role, error) {
return u.role, u.err
}
func TestFindOnManual(t *testing.T) {
var (
ctx = context.Background()
svc = &service{
denyExec: make(map[string]map[uint64]bool),
sScripts: ScriptSet{
&Script{
Name: "s1",
Triggers: []*Trigger{
&Trigger{
EventTypes: []string{"ev"},
ResourceTypes: []string{"res"},
},
},
},
&Script{
Name: "s2",
Triggers: []*Trigger{
&Trigger{
EventTypes: []string{"foo"},
ResourceTypes: []string{"bar"},
},
},
},
//&Script{
// Triggers: []*Trigger{
// &Trigger{
// EventTypes: []string{"not-a-match"},
// ResourceTypes: []string{"not-a-match"},
// },
// },
//},
},
cScripts: ScriptSet{
&Script{
Name: "s3",
Triggers: []*Trigger{
&Trigger{
EventTypes: []string{"ev"},
ResourceTypes: []string{"res"},
},
},
},
&Script{
Name: "s4",
Triggers: []*Trigger{
&Trigger{
EventTypes: []string{"foo"},
ResourceTypes: []string{"bar"},
},
},
},
},
}
filter = Filter{
ResourceTypes: []string{"res"},
EventTypes: []string{"ev"},
ExcludeServerScripts: false,
ExcludeClientScripts: false,
}
o, _, err = svc.Find(ctx, filter)
a = assert.New(t)
)
a.NoError(err)
a.Len(o, 2)
}
func TestGlobals(t *testing.T) {
var (
a = assert.New(t)
)
gCorredor = NewService(zap.NewNop(), options.CorredorOpt{})
a.Equal(gCorredor, Service())
a.NoError(Setup(zap.NewNop(), options.CorredorOpt{}))
a.NotNil(gCorredor)
gCorredor = nil
a.NoError(Setup(zap.NewNop(), options.CorredorOpt{}))
a.Equal(gCorredor, Service())
a.NotNil(gCorredor)
}
func TestServiceBasics(t *testing.T) {
var (
svc = &service{}
)
svc.SetEventRegistry(nil)
svc.SetAuthTokenMaker(nil)
svc.SetUserFinder(nil)
svc.SetRoleFinder(nil)
}
func TestService_canExec(t *testing.T) {
var (
a = assert.New(t)
svc = &service{
users: &mockUserSvc{user: &types.User{ID: 42, Email: "dummy@mo.ck", Handle: "dummy"}},
roles: &mockRoleSvc{role: &types.Role{ID: 84, Handle: "role", Name: "ROLE"}},
denyExec: make(map[string]map[uint64]bool),
}
ctx = auth.SetIdentityToContext(context.Background(), auth.Authenticated(42, 84))
script1 = &ServerScript{
Name: "s1",
Triggers: []*Trigger{&Trigger{
EventTypes: []string{onManualEventType},
ResourceTypes: []string{"res"},
}},
Security: &Security{
RunAs: "foo",
Allow: []string{"role"},
},
}
script2 = &ServerScript{
Name: "s2",
Triggers: []*Trigger{&Trigger{
EventTypes: []string{onManualEventType},
ResourceTypes: []string{"res"},
}},
Security: &Security{
RunAs: "foo",
Deny: []string{"role"},
},
}
script3 = &ServerScript{
Name: "s3", // permissions will not be added, not a manual script
Security: &Security{
RunAs: "foo",
Deny: []string{"role"},
},
}
script4 = &ServerScript{
Name: "s3", // should not be even added, duplicated name (hence length=3)
Security: &Security{
RunAs: "foo",
Deny: []string{"role"},
},
}
)
if testing.Verbose() {
svc.log = logger.MakeDebugLogger()
} else {
svc.log = zap.NewNop()
}
svc.registerServerScripts(ctx, script1, script2, script3, script4)
a.Len(svc.sScripts, 3)
a.Len(svc.denyExec, 2)
a.True(svc.canExec(ctx, script1.Name))
a.False(svc.canExec(ctx, script2.Name))
}
func TestService_Exec(t *testing.T) {
var (
a = assert.New(t)
svc = &service{
explicit: map[string]map[string]bool{},
opt: options.CorredorOpt{Enabled: true},
}
)
a.Error(svc.Exec(nil, "script", &mockEvent{eType: "not-onManual"}))
a.Error(svc.Exec(nil, "script", &mockEvent{eType: onManualEventType}))
svc.explicit["script"] = nil
a.Error(svc.Exec(nil, "script", &mockEvent{eType: onManualEventType}))
}

47
server/pkg/corredor/type_set.gen.go generated Normal file
View File

@@ -0,0 +1,47 @@
package corredor
// 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:
// pkg/corredor/types.yaml
type (
// ScriptSet slice of Script
//
// This type is auto-generated.
ScriptSet []*Script
)
// Walk iterates through every slice item and calls w(Script) err
//
// This function is auto-generated.
func (set ScriptSet) Walk(w func(*Script) error) (err error) {
for i := range set {
if err = w(set[i]); err != nil {
return
}
}
return
}
// Filter iterates through every slice item, calls f(Script) (bool, err) and return filtered slice
//
// This function is auto-generated.
func (set ScriptSet) Filter(f func(*Script) (bool, error)) (out ScriptSet, err error) {
var ok bool
out = ScriptSet{}
for i := range set {
if ok, err = f(set[i]); err != nil {
return
} else if ok {
out = append(out, set[i])
}
}
return
}

71
server/pkg/corredor/type_set.gen_test.go generated Normal file
View File

@@ -0,0 +1,71 @@
package corredor
// 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:
// pkg/corredor/types.yaml
import (
"fmt"
"github.com/stretchr/testify/require"
"testing"
)
func TestScriptSetWalk(t *testing.T) {
var (
value = make(ScriptSet, 3)
req = require.New(t)
)
// check walk with no errors
{
err := value.Walk(func(*Script) error {
return nil
})
req.NoError(err)
}
// check walk with error
req.Error(value.Walk(func(*Script) error { return fmt.Errorf("walk error") }))
}
func TestScriptSetFilter(t *testing.T) {
var (
value = make(ScriptSet, 3)
req = require.New(t)
)
// filter nothing
{
set, err := value.Filter(func(*Script) (bool, error) {
return true, nil
})
req.NoError(err)
req.Equal(len(set), len(value))
}
// filter one item
{
found := false
set, err := value.Filter(func(*Script) (bool, error) {
if !found {
found = true
return found, nil
}
return false, nil
})
req.NoError(err)
req.Len(set, 1)
}
// filter error
{
_, err := value.Filter(func(*Script) (bool, error) {
return false, fmt.Errorf("filter error")
})
req.Error(err)
}
}

View File

@@ -0,0 +1,142 @@
package corredor
import (
"encoding/json"
"fmt"
"strings"
"github.com/cortezaproject/corteza/server/pkg/eventbus"
)
type (
// ScriptSecurity sets script security and run-as flag
//
// Determinate user (if >0) that script will run under
// and allow/deny combination for roles that can see & execute
// explicit (onManual) scripts
//
// Please note that implicit scripts cannot have RBAC protection!
// These scripts are always executed as configured no mather who runs them.
ScriptSecurity struct {
*Security
runAs uint64
}
Script struct {
Name string `json:"name"`
Label string `json:"label"`
Description string `json:"description"`
Errors []string `json:"errors,omitempty"`
Triggers []*Trigger `json:"triggers"`
Iterator *Iterator `json:"iterator"`
Security *ScriptSecurity `json:"security"`
UpdatedAt string `json:"updatedAt,omitempty"`
// If bundle or type is set, consider
// this a frontend script
Bundle string `json:"bundle,omitempty"`
Type string `json:"type,omitempty"`
}
// allows passing extra kv with event arguments
scriptArgs struct {
event ScriptArgs
extra map[string]interface{}
}
)
// FindByName returns script from the set if it exists
func (set ScriptSet) FindByName(name string) *Script {
for i := range set {
if set[i].Name == name {
return set[i]
}
}
return nil
}
// String fn to make logging a bit friendlier
func (ss *ScriptSecurity) String() (o string) {
if ss == nil {
return "<nil>"
}
return fmt.Sprintf(
"runAs: %s, allow: %s, deny: %s",
ss.RunAs,
strings.Join(ss.Allow, ","),
strings.Join(ss.Deny, ","),
)
}
func ExtendScriptArgs(ev ScriptArgs, args map[string]interface{}) ScriptArgs {
return &scriptArgs{
event: ev,
extra: args,
}
}
func (s *scriptArgs) ResourceType() string {
return s.event.ResourceType()
}
func (s *scriptArgs) EventType() string {
return s.event.EventType()
}
func (s *scriptArgs) Match(matcher eventbus.ConstraintMatcher) bool {
return s.event.Match(matcher)
}
func (s *scriptArgs) Encode() (enc map[string][]byte, err error) {
if enc, err = s.event.Encode(); err != nil {
return nil, err
}
if enc == nil {
enc = make(map[string][]byte)
}
for k, v := range s.extra {
if enc[k] != nil {
// skip all that were encoded
// by event encoder
continue
}
if enc[k], err = json.Marshal(v); err != nil {
return nil, err
}
}
return
}
func (s *scriptArgs) Decode(dec map[string][]byte) (err error) {
if err = s.event.Decode(dec); err != nil {
return
}
for k := range s.extra {
// @todo how do we omit one decoded by event?
raw, has := dec[k]
if !has || len(raw) == 0 {
// Script only returns the keys it wants to; anything it left
// out keeps the value it was invoked with
continue
}
// Decoding into s.extra[k] directly is not possible; it holds a
// value, not a pointer to one
var val interface{}
if err = json.Unmarshal(raw, &val); err != nil {
return
}
s.extra[k] = val
}
return
}

View File

@@ -0,0 +1,4 @@
package: corredor
types:
Script:
noIdField: true

View File

@@ -0,0 +1,71 @@
package corredor
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestScriptArgs_Decode(t *testing.T) {
tests := []struct {
name string
extra map[string]interface{}
dec map[string][]byte
want map[string]interface{}
}{
{
// Corredor only returns the keys the script put on the returned
// object, so this is the normal case, not an edge one
name: "script returns only some of the args",
extra: map[string]interface{}{"validated": "false", "untouched": "keep"},
dec: map[string][]byte{"validated": []byte(`"true"`)},
want: map[string]interface{}{"validated": "true", "untouched": "keep"},
},
{
name: "script returns nothing",
extra: map[string]interface{}{"validated": "false"},
dec: map[string][]byte{},
want: map[string]interface{}{"validated": "false"},
},
{
name: "script returns an empty value",
extra: map[string]interface{}{"validated": "false"},
dec: map[string][]byte{"validated": {}},
want: map[string]interface{}{"validated": "false"},
},
{
name: "script returns another type than it was given",
extra: map[string]interface{}{"count": "1"},
dec: map[string][]byte{"count": []byte(`42`)},
want: map[string]interface{}{"count": float64(42)},
},
{
name: "script returns a structured value",
extra: map[string]interface{}{"cfg": "none"},
dec: map[string][]byte{"cfg": []byte(`{"a":true}`)},
want: map[string]interface{}{"cfg": map[string]interface{}{"a": true}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var (
req = require.New(t)
sa = ExtendScriptArgs(mockEvent{}, tt.extra)
)
req.NoError(sa.Decode(tt.dec))
req.Equal(tt.want, tt.extra)
})
}
}
func TestScriptArgs_DecodeMalformed(t *testing.T) {
var (
req = require.New(t)
extra = map[string]interface{}{"validated": "false"}
sa = ExtendScriptArgs(mockEvent{}, extra)
)
req.Error(sa.Decode(map[string][]byte{"validated": []byte(`{oops`)}))
}

125
server/pkg/corredor/util.go Normal file
View File

@@ -0,0 +1,125 @@
package corredor
import (
"context"
"encoding/json"
"fmt"
"net/http"
"github.com/cortezaproject/corteza/server/pkg/eventbus"
"github.com/cortezaproject/corteza/server/pkg/slice"
)
type (
automationListSetPayload struct {
Filter Filter `json:"filter"`
Set []*Script `json:"set"`
}
)
// mapExplicitTriggers scans for explicit event types from the list of script's triggers
// and returns a hash map with resources from these explicit triggers
func mapExplicitTriggers(script *ServerScript) map[string]bool {
var (
hash = make(map[string]bool)
)
for i := range script.Triggers {
// We're modifying trigger in the loop,
// so let's make a copy we can play with
trigger := script.Triggers[i]
if len(slice.IntersectStrings(trigger.EventTypes, explicitEventTypes)) > 0 {
for _, res := range trigger.ResourceTypes {
hash[res] = true
}
}
}
return hash
}
// converts trigger's constraint to eventbus' constraint options
func triggerToHandlerOps(t *Trigger) (oo []eventbus.HandlerRegOp, err error) {
if len(t.ResourceTypes) == 0 {
return nil, fmt.Errorf("cannot generate event handler without at least one resource")
}
if len(t.EventTypes) == 0 {
return nil, fmt.Errorf("cannot generate event handler without at least one events")
}
// Make a copy of event types slice so that we do not modify it
types := slice.PluckString(t.EventTypes, onManualEventType)
// If no other event types are left on the trigger,
// no need for procede
if len(types) == 0 && len(t.EventTypes) > 0 {
return
}
oo = append(oo, eventbus.For(t.ResourceTypes...))
oo = append(oo, eventbus.On(types...))
if cc, err := constraintsToHandlerOps(t.Constraints); err != nil {
return nil, err
} else {
oo = append(oo, cc...)
}
return
}
func constraintsToHandlerOps(cc []*TConstraint) (oo []eventbus.HandlerRegOp, err error) {
for _, raw := range cc {
if c, err := eventbus.ConstraintMaker(raw.Name, raw.Op, raw.Value...); err != nil {
return nil, fmt.Errorf("cannot generate constraints: %w", err)
} else {
oo = append(oo, eventbus.Constraint(c))
}
}
return
}
// encode adds entry (with json encoded value) to hash map
// used to prepare data for transmission
func encodeArguments(args map[string]string, key string, val interface{}) (err error) {
var tmp []byte
if tmp, err = json.Marshal(val); err != nil {
return
}
args[key] = string(tmp)
return
}
// GenericListHandler returns filtered list of scripts
func GenericListHandler(ctx context.Context, svc *service, f Filter, resourcePrefix string) (p *automationListSetPayload, err error) {
if f.ExcludeInvalid {
// resource prefix filtering is only applicable when we want to
// exclude invalid scripts, because invalid scripts do not have
// (usually, depends at what level error occurred) information
// about triggers and resources
f.procRTPrefixes(resourcePrefix)
}
p = &automationListSetPayload{}
p.Set, p.Filter, err = svc.Find(ctx, f)
return p, err
}
func GenericBundleHandler(ctx context.Context, svc *service, bundleName, bundleType, ext string) (interface{}, error) {
return func(w http.ResponseWriter, req *http.Request) {
// Serve bundle directly for now
bundle := svc.GetBundle(ctx, bundleName, bundleType)
if bundle == nil {
w.WriteHeader(http.StatusNoContent)
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte(bundle.Code))
}, nil
}

View File

@@ -0,0 +1,86 @@
package corredor
import (
"github.com/cortezaproject/corteza/server/pkg/eventbus"
"testing"
"github.com/stretchr/testify/assert"
)
func TestMapManualTriggers(t *testing.T) {
var (
a = assert.New(t)
)
a.EqualValues(
map[string]bool{
"r1": true,
"r2": true,
},
mapExplicitTriggers(&ServerScript{
Triggers: []*Trigger{&Trigger{
ResourceTypes: []string{"r1", "r2"},
EventTypes: []string{"onTimestamp", onManualEventType, "onInterval"},
}},
}),
)
a.EqualValues(
map[string]bool{},
mapExplicitTriggers(&ServerScript{
Triggers: []*Trigger{&Trigger{
ResourceTypes: []string{"r1", "r2"},
EventTypes: []string{"onTimestamp", "onInterval"},
}},
}),
)
}
func TestTriggerOptsMaking(t *testing.T) {
var (
a = assert.New(t)
trg = &Trigger{
ResourceTypes: []string{"r1", "r2"},
EventTypes: []string{"onTimestamp", onManualEventType, "onInterval"},
Constraints: []*TConstraint{
&TConstraint{Name: "some1", Op: "eq", Value: []string{"other"}},
&TConstraint{Name: "some2", Op: "eq", Value: []string{"other"}},
},
}
oo []eventbus.HandlerRegOp
err error
)
oo, err = triggerToHandlerOps(trg)
a.NoError(err)
a.Len(oo, 4) // 1x all resources, 1x all events, 2x constraints
oo, err = triggerToHandlerOps(&Trigger{ResourceTypes: []string{"bar"}})
a.Error(err, "expecting to fail on trigger w/o events")
oo, err = triggerToHandlerOps(&Trigger{EventTypes: []string{"foo"}})
a.Error(err, "expecting to fail on trigger w/o resources")
}
func TestArgEncoding(t *testing.T) {
var (
a = assert.New(t)
args = map[string]string{}
)
a.NoError(encodeArguments(args, "foo", "string"))
a.NoError(encodeArguments(args, "bar", 42))
a.NoError(encodeArguments(args, "baz", true))
a.NoError(encodeArguments(args, "obj", struct{ A string }{A: "A"}))
a.Error(encodeArguments(args, "func", func() {}))
a.EqualValues(map[string]string{
"foo": `"string"`,
"bar": `42`,
"baz": `true`,
"obj": `{"A":"A"}`,
}, args)
}