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,102 @@
package system
import (
"github.com/cortezaproject/corteza/server/codegen/schema"
)
apigw_filter: {
features: {
labels: false
}
model: {
attributes: {
id: schema.IdField
route: {
sortable: true, goType: "uint64", storeIdent: "rel_route"
dal: { type: "Ref", refModelResType: "corteza::system:apigw-route" }
identAlias: ["route", "Route", "ApigwRouteID"]
envoy: {
store: {
omitRefFilter: true
}
}
}
weight: {
sortable: true,
goType: "uint64"
dal: { type: "Number", meta: { "rdbms:type": "integer" } }
}
kind: {
sortable: true
dal: { type: "Text", length: 64 }
}
ref: {
dal: { type: "Text", length: 64 }
}
enabled: {
sortable: true,
goType: "bool"
dal: { type: "Boolean" }
}
params: {
goType: "types.ApigwFilterParams"
dal: { type: "JSON", defaultEmptyObject: true }
omitSetter: true
omitGetter: true
}
created_at: schema.SortableTimestampNowField
updated_at: schema.SortableTimestampNilField
deleted_at: schema.SortableTimestampNilField
created_by: schema.AttributeUserRef
updated_by: schema.AttributeUserRef
deleted_by: schema.AttributeUserRef
}
indexes: {
"primary": { attribute: "id" }
}
}
envoy: {
yaml: {
supportMappedInput: false
omitEncoder: true
}
store: {
handleField: ""
}
}
filter: {
struct: {
apigw_filter_id: {goType: "[]uint64", ident: "apigwFilterID", storeIdent: "id"}
route_id: {goType: "uint64", ident: "routeID", storeIdent: "rel_route"}
deleted: {goType: "filter.State", storeIdent: "deleted_at"}
disabled: {goType: "filter.State", storeIdent: "enabled"}
}
byValue: ["apigw_filter_id", "route_id"]
byNilState: ["deleted"]
byFalseState: ["disabled"]
}
store: {
api: {
lookups: [
{
fields: ["id"]
description: """
searches for filter by ID
"""
}, {
fields: ["route"]
description: """
searches for filter by route
"""
},
]
}
}
}

View File

@@ -0,0 +1,132 @@
package system
import (
"github.com/cortezaproject/corteza/server/codegen/schema"
)
apigw_route: {
features: {
labels: false
}
model: {
attributes: {
id: schema.IdField
endpoint: {
sortable: true
dal: {}
}
method: {
sortable: true
dal: {}
}
enabled: {
sortable: true,
goType: "bool"
dal: { type: "Boolean" }
}
meta: {
goType: "types.ApigwRouteMeta"
dal: { type: "JSON", defaultEmptyObject: true }
omitSetter: true
omitGetter: true
}
group: {
sortable: true,
goType: "uint64",
storeIdent: "rel_group"
dal: {
type: "Ref",
// @todo what does this do?
refModelResType: "corteza::system:apigw-group"
}
envoy: {
store: {
omitRefFilter: true
}
}
}
created_at: schema.SortableTimestampNowField
updated_at: schema.SortableTimestampNilField
deleted_at: schema.SortableTimestampNilField
created_by: schema.AttributeUserRef
updated_by: schema.AttributeUserRef
deleted_by: schema.AttributeUserRef
}
indexes: {
"primary": { attribute: "id" }
}
}
envoy: {
yaml: {
supportMappedInput: true
mappedField: "Endpoint"
identKeyAlias: ["endpoints"]
extendedResourceDecoders: [{
ident: "filters"
expIdent: "Filters"
identKeys: ["filters"]
supportMappedInput: false
}]
extendedResourceEncoders: [{
ident: "apigwFilter"
expIdent: "ApigwFilter"
identKey: "filters"
}]
}
store: {
handleField: "Endpoint"
extendedDecoder: true
}
}
filter: {
struct: {
apigw_route_id: { goType: "[]uint64", ident: "apigwRouteID", storeIdent: "id" }
route: {goType: "string", storeIdent: "id"}
endpoint: {goType: "string"}
method: {goType: "string"}
deleted: {goType: "filter.State", storeIdent: "deleted_at"}
disabled: {goType: "filter.State", storeIdent: "enabled"}
}
byValue: ["apigw_route_id", "route", "method"]
byNilState: ["deleted"]
byFalseState: ["disabled"]
}
rbac: {
operations: {
read: description: "Read API Gateway route"
update: description: "Update API Gateway route"
delete: description: "Delete API Gateway route"
}
}
store: {
api: {
lookups: [
{
fields: ["id"]
description: """
searches for route by ID
It returns route even if deleted or suspended
"""
}, {
fields: ["endpoint"]
description: """
searches for route by endpoint
It returns route even if deleted or suspended
"""
},
]
}
}
}

View File

@@ -0,0 +1,120 @@
package system
import (
"github.com/cortezaproject/corteza/server/codegen/schema"
)
application: {
model: {
attributes: {
id: schema.IdField
name: {
sortable: true
dal: {}
}
enabled: {
goType: "bool"
sortable: true,
dal: { type: "Boolean", default: true }
}
weight: {
goType: "int",
sortable: true
dal: { type: "Number", default: 0, meta: { "rdbms:type": "integer" } }
}
unify: {
goType: "*types.ApplicationUnify"
dal: { type: "JSON", defaultEmptyObject: true }
omitSetter: true
omitGetter: true
}
owner_id: {
schema.AttributeUserRef,
storeIdent: "rel_owner",
ident: "ownerID"
envoy: {
store: {
omitRefFilter: true
}
yaml: {
identKeyAlias: ["owner"]
}
}
}
created_at: schema.SortableTimestampNowField
updated_at: schema.SortableTimestampNilField
deleted_at: schema.SortableTimestampNilField
}
indexes: {
"primary": { attribute: "id" }
}
}
filter: {
struct: {
name: {goType: "string"}
// not sure about the type of flagged_ids
flagged_ids: {goType: "[]uint64"}
flags: {goType: "[]string"}
inc_flags: {goType: "uint"}
deleted: {goType: "filter.State", storeIdent: "deleted_at"}
}
query: ["name"]
byValue: ["name"]
byNilState: ["deleted"]
}
features: {
flags: true
}
envoy: {
yaml: {
supportMappedInput: true
mappedField: "Name"
identKeyAlias: ["applications", "apps"]
}
store: {
handleField: "Name"
}
}
rbac: {
operations: {
read:
description: "Read application"
update:
description: "Update application"
delete:
description: "Delete application"
}
}
store: {
api: {
lookups: [
{
fields: ["id"]
description: """
searches for role by ID
It returns role even if deleted or suspended
"""
},
]
functions: [
{
expIdent: "ApplicationMetrics"
return: [ "*types.ApplicationMetrics"]
}, {
expIdent: "ReorderApplications"
// not sure about the ident
args: [ {ident: "order", goType: "[]uint64"}]
},
]
}
}
}

View File

@@ -0,0 +1,69 @@
package system
import (
"github.com/cortezaproject/corteza/server/codegen/schema"
)
attachment: {
features: {
labels: false
}
model: {
attributes: {
id: schema.IdField
owner_id: {
storeIdent: "rel_owner",
ident: "ownerID"
schema.AttributeUserRef,
}
kind: {
sortable: true
dal: {}
}
url: {
dal: {}
}
preview_url: {
dal: {}
}
name: {
sortable: true
dal: {}
}
meta: {
goType: "types.AttachmentMeta"
dal: { type: "JSON", defaultEmptyObject: true }
omitSetter: true
omitGetter: true
}
created_at: schema.SortableTimestampNowField
updated_at: schema.SortableTimestampNilField
deleted_at: schema.SortableTimestampNilField
}
indexes: {
"primary": { attribute: "id" }
}
}
envoy: {
omit: true
}
filter: {
struct: {
kind: {}
}
byValue: ["kind"]
}
store: {
api: {
lookups: [
{ fields: ["id"] },
]
}
}
}

View File

@@ -0,0 +1,137 @@
package system
import (
"github.com/cortezaproject/corteza/server/codegen/schema"
)
auth_client: {
model: {
attributes: {
id: schema.IdField
handle: schema.HandleField
meta: {
goType: "*types.AuthClientMeta"
dal: { type: "JSON", defaultEmptyObject: true }
omitSetter: true
omitGetter: true
}
secret: {
goType: "string"
dal: { type: "Text", length: 64 }
}
scope: {
goType: "string"
dal: { type: "Text", length: 512 }
}
valid_grant: {
goType: "string"
dal: { type: "Text", length: 32 }
}
redirect_uri: {
goType: "string",
ident: "redirectURI"
dal: {}
}
enabled: {
sortable: true,
goType: "bool"
dal: { type: "Boolean", default: false }
}
trusted: {
sortable: true,
goType: "bool"
dal: { type: "Boolean", default: false }
}
valid_from: schema.SortableTimestampNilField
expires_at: schema.SortableTimestampNilField
security: {
goType: "*types.AuthClientSecurity"
dal: { type: "JSON", defaultEmptyObject: true }
omitSetter: true
omitGetter: true
envoy: {
yaml: {
customDecoder: true
customEncoder: true
}
}
}
owned_by: schema.AttributeUserRef
created_at: schema.SortableTimestampNowField
updated_at: schema.SortableTimestampNilField
deleted_at: schema.SortableTimestampNilField
created_by: schema.AttributeUserRef
updated_by: schema.AttributeUserRef
deleted_by: schema.AttributeUserRef
}
indexes: {
"primary": { attribute: "id" }
}
}
filter: {
struct: {
client_id: {goType: "[]uint64"}
handle: {goType: "string"}
deleted: {goType: "filter.State", storeIdent: "deleted_at"}
}
byValue: ["handle"]
byNilState: ["deleted"]
}
confirmed_client: {
user_id: {goType: "uint64"}
client_id: {goType: "uint64"}
confirmed_at: {goType: "schema.OptTimestamp"}
}
confirmed_client_filter: {
user_id: {goType: "uint64"}
}
envoy: {
yaml: {
supportMappedInput: true
mappedField: "Handle"
identKeyAlias: ["authclients"]
}
store: {
extendedRefDecoder: true
}
}
rbac: {
operations: {
read: description: "Read authorization client"
update: description: "Update authorization client"
delete: description: "Delete authorization client"
authorize: description: "Authorize authorization client"
}
}
store: {
api: {
lookups: [
{
fields: ["id"]
description: """
searches for auth client by ID
It returns auth clint even if deleted
"""
}, {
fields: ["handle"]
nullConstraint: ["deleted_at"]
constraintCheck: true
description: """
searches for auth client by ID
It returns auth clint even if deleted
"""
},
]
}
}
}

View File

@@ -0,0 +1,59 @@
package system
import (
"github.com/cortezaproject/corteza/server/codegen/schema"
)
auth_confirmed_client: {
features: {
labels: false
paging: false
sorting: false
checkFn: false
}
model: {
omitGetterSetter: true
attributes: {
user_id: {
goType: "uint64",
ident: "userID",
storeIdent: "rel_user"
dal: { type: "Ref", refModelResType: "corteza::system:user", default: 0 }
}
client_id: {
goType: "uint64",
ident: "clientID",
storeIdent: "rel_client"
dal: { type: "Ref", refModelResType: "corteza::system:auth-client", default: 0 }
}
confirmed_at: schema.SortableTimestampNowField
}
indexes: {
"primary": { attributes: ["user_id", "client_id"] }
}
}
filter: {
struct: {
user_id: { goType: "uint64", ident: "userID", storeIdent: "rel_user" }
}
byValue: ["user_id"]
}
envoy: {
omit: true
}
store: {
api: {
lookups: [
{ fields: ["user_id", "client_id"] },
]
}
}
}

View File

@@ -0,0 +1,93 @@
package system
import (
"github.com/cortezaproject/corteza/server/codegen/schema"
)
auth_oa2token: {
features: {
labels: false
paging: false
sorting: false
checkFn: false
}
model: {
omitGetterSetter: true
attributes: {
id: schema.IdField
code: {
dal: { type: "Text", length: 48 }
}
access: {
dal: { type: "Text", length: 2048 }
}
refresh: {
dal: { type: "Text", length: 48 }
}
data: {
goType: "rawJson"
dal: { type: "JSON", defaultEmptyObject: true }
}
remote_addr: {
dal: { type: "Text", length: 64 }
}
user_agent: {
dal: {}
}
client_id: {
goType: "uint64",
ident: "clientID",
storeIdent: "rel_client"
dal: { type: "Ref", refModelResType: "corteza::system:auth-client", default: 0 }
}
user_id: {
goType: "uint64",
ident: "userID",
storeIdent: "rel_user"
dal: { type: "Ref", refModelResType: "corteza::system:user", default: 0 }
}
created_at: schema.SortableTimestampNowField
expires_at: schema.SortableTimestampField
}
indexes: {
"primary": { attribute: "id" }
"client_id": { attribute: "client_id" }
"code": { attribute: "code" }
"refresh": { attribute: "refresh" }
}
}
filter: {
struct: {
user_id: { goType: "uint64", ident: "userID" }
}
byValue: ["user_id"]
}
envoy: {
omit: true
}
store: {
api: {
lookups: [
{ fields: ["id"] },
{ fields: ["code"] },
{ fields: ["access"] },
{ fields: ["refresh"] },
]
functions: [
{ expIdent: "DeleteExpiredAuthOA2Tokens" },
{ expIdent: "DeleteAuthOA2TokenByCode", args: [ { ident: "code", goType: "string" } ] },
{ expIdent: "DeleteAuthOA2TokenByAccess", args: [ { ident: "access", goType: "string" } ] },
{ expIdent: "DeleteAuthOA2TokenByRefresh", args: [ { ident: "refresh", goType: "string" } ] },
{ expIdent: "DeleteAuthOA2TokenByUserID", args: [ { ident: "userID", goType: "uint64" } ] },
]
}
}
}

View File

@@ -0,0 +1,74 @@
package system
import (
"github.com/cortezaproject/corteza/server/codegen/schema"
)
auth_session: {
features: {
labels: false
paging: false
sorting: false
checkFn: false
}
model: {
omitGetterSetter: true
attributes: {
id: {
expIdent: "ID",
goType: "string"
dal: { length: 64 }
}
data: {
goType: "[]byte"
dal: { type: "Blob" }
}
user_id: {
goType: "uint64",
ident: "userID",
storeIdent: "rel_user"
dal: { type: "Ref", refModelResType: "corteza::system:user", default: 0 }
}
remote_addr: {
dal: {}
}
user_agent: {
dal: {}
}
expires_at: schema.SortableTimestampField
created_at: schema.SortableTimestampNowField
}
indexes: {
"primary": { attribute: "id" }
"expires_at": { attribute: "expires_at" }
}
}
filter: {
struct: {
user_id: { goType: "uint64", ident: "userID", storeIdent: "rel_user" }
}
byValue: ["user_id"]
}
envoy: {
omit: true
}
store: {
api: {
lookups: [
{ fields: ["id"] },
]
functions: [
{ expIdent: "DeleteExpiredAuthSessions" },
{ expIdent: "DeleteAuthSessionsByUserID", args: [ { ident: "userID", goType: "uint64" } ] },
]
}
}
}

View File

@@ -0,0 +1,444 @@
package automation
// 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:
// system/automation/actionlog_handler.yaml
import (
"context"
atypes "github.com/cortezaproject/corteza/server/automation/types"
"github.com/cortezaproject/corteza/server/pkg/actionlog"
"github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/cortezaproject/corteza/server/pkg/wfexec"
"time"
)
var _ wfexec.ExecResponse
type (
actionlogHandlerRegistry interface {
AddFunctions(ff ...*atypes.Function)
Type(ref string) expr.Type
}
)
func (h actionlogHandler) register() {
h.reg.AddFunctions(
h.Search(),
h.Each(),
h.Record(),
)
}
type (
actionlogSearchArgs struct {
hasFromTimestamp bool
FromTimestamp *time.Time
hasToTimestamp bool
ToTimestamp *time.Time
hasBeforeActionID bool
BeforeActionID uint64
hasActorID bool
ActorID uint64
hasOrigin bool
Origin string
hasResource bool
Resource string
hasAction bool
Action string
hasLimit bool
Limit uint64
}
actionlogSearchResults struct {
Actions []*actionlog.Action
}
)
// Search function Action log search
//
// expects implementation of search function:
//
// func (h actionlogHandler) search(ctx context.Context, args *actionlogSearchArgs) (results *actionlogSearchResults, err error) {
// return
// }
func (h actionlogHandler) Search() *atypes.Function {
return &atypes.Function{
Ref: "actionlogSearch",
Kind: "function",
Labels: map[string]string{"actionlog": "step,workflow"},
Meta: &atypes.FunctionMeta{
Short: "Action log search",
},
Parameters: []*atypes.Param{
{
Name: "fromTimestamp",
Types: []string{"DateTime"},
Meta: &atypes.ParamMeta{
Label: "From timestamp",
},
},
{
Name: "toTimestamp",
Types: []string{"DateTime"},
Meta: &atypes.ParamMeta{
Label: "To timestamp",
},
},
{
Name: "beforeActionID",
Types: []string{"ID"},
Meta: &atypes.ParamMeta{
Label: "Before action ID",
},
},
{
Name: "actorID",
Types: []string{"ID"},
Meta: &atypes.ParamMeta{
Label: "Actor ID",
},
},
{
Name: "origin",
Types: []string{"String"},
Meta: &atypes.ParamMeta{
Label: "Origin",
},
},
{
Name: "resource",
Types: []string{"String"},
Meta: &atypes.ParamMeta{
Label: "Resource",
},
},
{
Name: "action",
Types: []string{"String"},
Meta: &atypes.ParamMeta{
Label: "Action",
},
},
{
Name: "limit",
Types: []string{"UnsignedInteger"},
Meta: &atypes.ParamMeta{
Label: "Limit",
},
},
},
Results: []*atypes.Param{
{
Name: "actions",
Types: []string{"Action"},
IsArray: true,
},
},
Handler: func(ctx context.Context, in *expr.Vars) (out *expr.Vars, err error) {
var (
args = &actionlogSearchArgs{
hasFromTimestamp: in.Has("fromTimestamp"),
hasToTimestamp: in.Has("toTimestamp"),
hasBeforeActionID: in.Has("beforeActionID"),
hasActorID: in.Has("actorID"),
hasOrigin: in.Has("origin"),
hasResource: in.Has("resource"),
hasAction: in.Has("action"),
hasLimit: in.Has("limit"),
}
)
if err = in.Decode(args); err != nil {
return
}
var results *actionlogSearchResults
if results, err = h.search(ctx, args); err != nil {
return
}
out = &expr.Vars{}
{
// converting results.Actions (*actionlog.Action) to Array (of Action)
var (
tval expr.TypedValue
tarr = make([]expr.TypedValue, len(results.Actions))
)
for i := range results.Actions {
if tarr[i], err = h.reg.Type("Action").Cast(results.Actions[i]); err != nil {
return
}
}
if tval, err = expr.NewArray(tarr); err != nil {
return
} else if err = expr.Assign(out, "actions", tval); err != nil {
return
}
}
return
},
}
}
type (
actionlogEachArgs struct {
hasFromTimestamp bool
FromTimestamp *time.Time
hasToTimestamp bool
ToTimestamp *time.Time
hasBeforeActionID bool
BeforeActionID uint64
hasActorID bool
ActorID uint64
hasOrigin bool
Origin string
hasResource bool
Resource string
hasAction bool
Action string
hasLimit bool
Limit uint64
}
actionlogEachResults struct {
Action *actionlog.Action
}
)
// Each function Action log
//
// expects implementation of each function:
//
// func (h actionlogHandler) each(ctx context.Context, args *actionlogEachArgs) (results *actionlogEachResults, err error) {
// return
// }
func (h actionlogHandler) Each() *atypes.Function {
return &atypes.Function{
Ref: "actionlogEach",
Kind: "iterator",
Labels: map[string]string{"actionlog": "step,workflow"},
Meta: &atypes.FunctionMeta{
Short: "Action log",
},
Parameters: []*atypes.Param{
{
Name: "fromTimestamp",
Types: []string{"DateTime"},
Meta: &atypes.ParamMeta{
Label: "From timestamp",
},
},
{
Name: "toTimestamp",
Types: []string{"DateTime"},
Meta: &atypes.ParamMeta{
Label: "To timestamp",
},
},
{
Name: "beforeActionID",
Types: []string{"ID"},
Meta: &atypes.ParamMeta{
Label: "Before action ID",
},
},
{
Name: "actorID",
Types: []string{"ID"},
Meta: &atypes.ParamMeta{
Label: "Actor ID",
},
},
{
Name: "origin",
Types: []string{"String"},
Meta: &atypes.ParamMeta{
Label: "Origin",
},
},
{
Name: "resource",
Types: []string{"String"},
Meta: &atypes.ParamMeta{
Label: "Resource",
},
},
{
Name: "action",
Types: []string{"String"},
Meta: &atypes.ParamMeta{
Label: "Action",
},
},
{
Name: "limit",
Types: []string{"UnsignedInteger"},
Meta: &atypes.ParamMeta{
Label: "Limit",
},
},
},
Results: []*atypes.Param{
{
Name: "action",
Types: []string{"Action"},
},
},
Iterator: func(ctx context.Context, in *expr.Vars) (out wfexec.IteratorHandler, err error) {
var (
args = &actionlogEachArgs{
hasFromTimestamp: in.Has("fromTimestamp"),
hasToTimestamp: in.Has("toTimestamp"),
hasBeforeActionID: in.Has("beforeActionID"),
hasActorID: in.Has("actorID"),
hasOrigin: in.Has("origin"),
hasResource: in.Has("resource"),
hasAction: in.Has("action"),
hasLimit: in.Has("limit"),
}
)
if err = in.Decode(args); err != nil {
return
}
return h.each(ctx, args)
},
}
}
type (
actionlogRecordArgs struct {
hasAction bool
Action string
hasResource bool
Resource string
hasError bool
Error string
hasSeverity bool
Severity string
hasDescription bool
Description string
hasMeta bool
Meta *expr.Vars
}
)
// Record function Record action into action log
//
// expects implementation of record function:
//
// func (h actionlogHandler) record(ctx context.Context, args *actionlogRecordArgs) (err error) {
// return
// }
func (h actionlogHandler) Record() *atypes.Function {
return &atypes.Function{
Ref: "actionlogRecord",
Kind: "function",
Labels: map[string]string{"actionlog": "step,workflow"},
Meta: &atypes.FunctionMeta{
Short: "Record action into action log",
},
Parameters: []*atypes.Param{
{
Name: "action",
Types: []string{"String"},
Meta: &atypes.ParamMeta{
Label: "Action",
},
},
{
Name: "resource",
Types: []string{"String"},
Meta: &atypes.ParamMeta{
Label: "Resource",
},
},
{
Name: "error",
Types: []string{"String"},
Meta: &atypes.ParamMeta{
Label: "Error",
},
},
{
Name: "severity",
Types: []string{"String"},
Meta: &atypes.ParamMeta{
Label: "Severity",
Visual: map[string]interface{}{"options": []interface{}{"emergency", "alert", "critical", "err", "warning", "notice", "info", "debug"}},
},
},
{
Name: "description",
Types: []string{"String"},
Meta: &atypes.ParamMeta{
Label: "Description",
},
},
{
Name: "meta",
Types: []string{"Vars"},
Meta: &atypes.ParamMeta{
Label: "Meta",
},
},
},
Handler: func(ctx context.Context, in *expr.Vars) (out *expr.Vars, err error) {
var (
args = &actionlogRecordArgs{
hasAction: in.Has("action"),
hasResource: in.Has("resource"),
hasError: in.Has("error"),
hasSeverity: in.Has("severity"),
hasDescription: in.Has("description"),
hasMeta: in.Has("meta"),
}
)
if err = in.Decode(args); err != nil {
return
}
return out, h.record(ctx, args)
},
}
}

View File

@@ -0,0 +1,154 @@
package automation
import (
"context"
"github.com/cortezaproject/corteza/server/pkg/actionlog"
. "github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/cortezaproject/corteza/server/pkg/id"
"github.com/cortezaproject/corteza/server/pkg/wfexec"
)
type (
actionlogHandler struct {
reg actionlogHandlerRegistry
svc actionlog.Recorder
}
actionSetIterator struct {
// Item buffer, current item pointer, and total items traversed
ptr uint
buffer actionlog.ActionSet
total uint
// When filter limit is set, this constraints it
iterLimit uint
useIterLimit bool
// Item loader for additional chunks
filter actionlog.Filter
loader func() error
}
)
func ActionlogHandler(reg actionlogHandlerRegistry, svc actionlog.Recorder) *actionlogHandler {
h := &actionlogHandler{
reg: reg,
svc: svc,
}
h.register()
return h
}
func (h actionlogHandler) search(ctx context.Context, args *actionlogSearchArgs) (results *actionlogSearchResults, err error) {
results = &actionlogSearchResults{}
var (
f = actionlog.Filter{
FromTimestamp: args.FromTimestamp,
ToTimestamp: args.ToTimestamp,
BeforeActionID: args.BeforeActionID,
Origin: args.Origin,
Resource: args.Resource,
Action: args.Action,
Limit: uint(args.Limit),
}
)
if args.ActorID > 0 {
f.ActorID = id.Strings(args.ActorID)
}
results.Actions, _, err = h.svc.Find(ctx, f)
return
}
func (h actionlogHandler) each(ctx context.Context, args *actionlogEachArgs) (out wfexec.IteratorHandler, err error) {
var (
i = &actionSetIterator{}
f = actionlog.Filter{
FromTimestamp: args.FromTimestamp,
ToTimestamp: args.ToTimestamp,
BeforeActionID: args.BeforeActionID,
Origin: args.Origin,
Resource: args.Resource,
Action: args.Action,
}
)
if args.ActorID > 0 {
f.ActorID = id.Strings(args.ActorID)
}
if args.hasLimit {
i.useIterLimit = true
i.iterLimit = uint(args.Limit)
f.Limit = uint(args.Limit)
if args.Limit > uint64(wfexec.MaxIteratorBufferSize) {
f.Limit = wfexec.MaxIteratorBufferSize
}
i.iterLimit = uint(args.Limit)
} else {
f.Limit = wfexec.MaxIteratorBufferSize
}
i.filter = f
i.loader = func() (err error) {
i.total += i.ptr
i.ptr = 0
if len(i.buffer) > 0 {
i.filter.BeforeActionID = i.buffer[len(i.buffer)-1].ID
}
i.buffer, i.filter, err = h.svc.Find(ctx, i.filter)
return
}
// Initial load
return i, i.loader()
}
func (h actionlogHandler) record(ctx context.Context, args *actionlogRecordArgs) (err error) {
a := &actionlog.Action{
Resource: args.Resource,
Action: args.Action,
Error: args.Error,
Severity: actionlog.NewSeverity(args.Severity),
Description: args.Description,
}
if args.Meta != nil {
a.Meta = args.Meta.Dict()
}
ctx = actionlog.RequestOriginToContext(ctx, actionlog.RequestOrigin_Automation)
h.svc.Record(ctx, a)
return nil
}
func (i *actionSetIterator) More(context.Context, *Vars) (bool, error) {
return wfexec.GenericResourceNextCheck(i.useIterLimit, i.ptr, uint(len(i.buffer)), i.total, i.iterLimit, i.iterLimit > uint(len(i.buffer))), nil
}
func (i *actionSetIterator) Start(context.Context, *Vars) error { i.ptr = 0; return nil }
func (i *actionSetIterator) Next(context.Context, *Vars) (out *Vars, err error) {
if len(i.buffer)-int(i.ptr) <= 0 {
if err = i.loader(); err != nil {
panic(err)
}
}
out = &Vars{}
out.Set("user", Must(NewUser(i.buffer[i.ptr])))
out.Set("index", Must(NewInteger(i.total+i.ptr)))
i.ptr++
return out, nil
}

View File

@@ -0,0 +1,78 @@
imports:
- time
- github.com/cortezaproject/corteza/server/pkg/actionlog
snippets:
filterParams: &filterParams
fromTimestamp: { meta: { label: From timestamp }, types: [ { wf: DateTime } ] }
toTimestamp: { meta: { label: To timestamp }, types: [ { wf: DateTime } ] }
beforeActionID: { meta: { label: Before action ID }, types: [ { wf: ID } ] }
actorID: { meta: { label: Actor ID }, types: [ { wf: ID } ] }
origin: { meta: { label: Origin }, types: [ { wf: String } ] }
resource: { meta: { label: Resource }, types: [ { wf: String } ] }
action: { meta: { label: Action }, types: [ { wf: String } ] }
limit: { meta: { label: Limit }, types: [ { wf: UnsignedInteger } ] }
rvAction: &rvAction
wf: Action
labels: &labels
actionlog: "step,workflow"
functions:
search:
meta:
short: Action log search
params: *filterParams
labels:
<<: *labels
results:
actions:
<<: *rvAction
isArray: true
each:
kind: iterator
meta:
short: Action log
params: *filterParams
labels:
<<: *labels
results:
action: *rvAction
record:
meta:
short: Record action into action log
labels:
<<: *labels
params:
action:
meta: { label: Action }
types: [ { wf: String } ]
resource:
meta: { label: Resource }
types: [ { wf: String } ]
error:
meta: { label: Error }
types: [ { wf: String } ]
severity:
types: [ { wf: String } ]
meta:
label: Severity
visual:
options:
- "emergency"
- "alert"
- "critical"
- "err"
- "warning"
- "notice"
- "info"
- "debug"
description:
meta: { label: Description }
types: [ { wf: String } ]
meta:
meta: { label: Meta }
types: [ { wf: Vars, go: '*expr.Vars' } ]

1610
server/system/automation/expr_types.gen.go generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,325 @@
package automation
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"github.com/cortezaproject/corteza/server/pkg/actionlog"
"github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/cortezaproject/corteza/server/pkg/rbac"
"github.com/cortezaproject/corteza/server/system/types"
"github.com/spf13/cast"
)
type (
renderedDocument struct {
Document io.Reader
Name string
Type string
}
)
func CastToUser(val interface{}) (out *types.User, err error) {
switch val := val.(type) {
case expr.Iterator:
out = &types.User{}
return out, val.Each(func(k string, v expr.TypedValue) error {
return assignToUser(out, k, v)
})
}
switch val := expr.UntypedValue(val).(type) {
case *types.User:
return val, nil
case map[string]interface{}:
out = &types.User{}
m, _ := json.Marshal(val)
_ = json.Unmarshal(m, out)
return out, nil
case nil:
return &types.User{}, nil
default:
return nil, fmt.Errorf("unable to cast type %T to %T", val, out)
}
}
func CastToRole(val interface{}) (out *types.Role, err error) {
switch val := val.(type) {
case expr.Iterator:
out = &types.Role{}
return out, val.Each(func(k string, v expr.TypedValue) error {
return assignToRole(out, k, v)
})
}
switch val := expr.UntypedValue(val).(type) {
case *types.Role:
return val, nil
case map[string]interface{}:
out = &types.Role{}
m, _ := json.Marshal(val)
_ = json.Unmarshal(m, out)
return out, nil
case nil:
return &types.Role{}, nil
default:
return nil, fmt.Errorf("unable to cast type %T to %T", val, out)
}
}
func CastToReminder(val interface{}) (out *types.Reminder, err error) {
switch val := val.(type) {
case expr.Iterator:
out = &types.Reminder{}
return out, val.Each(func(k string, v expr.TypedValue) error {
return assignToReminder(out, k, v)
})
}
switch val := expr.UntypedValue(val).(type) {
case *types.Reminder:
return val, nil
case map[string]interface{}:
out = &types.Reminder{}
// m, _ := json.Marshal(val)
// _ = json.Unmarshal(m, out)
for k,v := range val {
if err = assignToReminder(out, k,v);err != nil{
return nil, err
}
}
return out, nil
case nil:
return &types.Reminder{}, nil
default:
return nil, fmt.Errorf("unable to cast type %T to %T", val, out)
}
}
func CastToTemplate(val interface{}) (out *types.Template, err error) {
switch val := val.(type) {
case expr.Iterator:
out = &types.Template{}
return out, val.Each(func(k string, v expr.TypedValue) error {
return assignToTemplate(out, k, v)
})
}
switch val := expr.UntypedValue(val).(type) {
case *types.Template:
return val, nil
case map[string]interface{}:
out = &types.Template{}
m, _ := json.Marshal(val)
_ = json.Unmarshal(m, out)
return out, nil
case nil:
return &types.Template{}, nil
default:
return nil, fmt.Errorf("unable to cast type %T to %T", val, out)
}
}
func CastToTemplateMeta(val interface{}) (out types.TemplateMeta, err error) {
switch val := val.(type) {
case expr.Iterator:
out = types.TemplateMeta{}
return out, val.Each(func(k string, v expr.TypedValue) error {
return assignToTemplateMeta(&out, k, v)
})
}
switch val := expr.UntypedValue(val).(type) {
case types.TemplateMeta:
return val, nil
case map[string]interface{}:
out = types.TemplateMeta{}
m, _ := json.Marshal(val)
_ = json.Unmarshal(m, &out)
return out, nil
case nil:
return types.TemplateMeta{}, nil
default:
return types.TemplateMeta{}, fmt.Errorf("unable to cast type %T to %T", val, out)
}
}
var _ expr.DeepFieldAssigner = &Template{}
// AssignFieldValue implements expr.DeepFieldAssigner
//
// Meta is a value struct; assigning to its fields through the generated
// selector would modify a copy, so we reroute it to the template's own meta
func (t *Template) AssignFieldValue(p expr.Pather, val expr.TypedValue) (err error) {
t.mux.Lock()
defer t.mux.Unlock()
if p.Get() != "meta" || p.IsLast() {
return assignToTemplate(t.value, p.Get(), val)
}
if err = p.Next(); err != nil {
return
}
return assignToTemplateMeta(&t.value.Meta, p.Get(), val)
}
func CastToRenderedDocument(val interface{}) (out *renderedDocument, err error) {
switch val := val.(type) {
case expr.Iterator:
out = &renderedDocument{}
return out, val.Each(func(k string, v expr.TypedValue) error {
return assignToRenderedDocument(out, k, v)
})
}
switch val := expr.UntypedValue(val).(type) {
case *renderedDocument:
return val, nil
default:
return nil, fmt.Errorf("unable to cast type %T to %T", val, out)
}
}
func CastToDocumentType(val interface{}) (out types.DocumentType, err error) {
switch val := val.(type) {
case string:
return types.DocumentType(val), nil
case *expr.String:
return types.DocumentType(val.GetValue()), nil
default:
return "", fmt.Errorf("unable to cast type %T to %T", val, out)
}
}
func CastToRenderOptions(val interface{}) (out map[string]string, err error) {
switch val := expr.UntypedValue(val).(type) {
case map[string]string:
return val, nil
case nil:
return make(map[string]string), nil
default:
out, err = cast.ToStringMapStringE(val)
if err != nil {
return nil, fmt.Errorf("unable to cast type %T to %T", val, out)
}
return out, nil
}
}
func (t *RenderOptions) AssignFieldValue(key string, val expr.TypedValue) error {
if t.value == nil {
t.value = make(map[string]string)
}
str, err := cast.ToStringE(expr.UntypedValue(val))
t.value[key] = str
return err
}
func CastToQueueMessage(val interface{}) (out *types.QueueMessage, err error) {
switch val := val.(type) {
case expr.Iterator:
out = &types.QueueMessage{}
return out, val.Each(func(k string, v expr.TypedValue) error {
return assignToQueueMessage(out, k, v)
})
}
switch val := expr.UntypedValue(val).(type) {
case *types.QueueMessage:
return val, nil
case nil:
return &types.QueueMessage{}, nil
default:
return &types.QueueMessage{}, fmt.Errorf("unable to cast type %T to %T", val, out)
}
}
func (doc renderedDocument) String() string {
aux, _ := ioutil.ReadAll(doc.Document)
return string(aux)
}
func CastToRbacResource(val interface{}) (out rbac.Resource, err error) {
switch val := expr.UntypedValue(val).(type) {
case rbac.Resource:
return val, nil
case RbacResource:
return val.value, nil
case string:
return rbac.NewResource(val), nil
default:
return nil, fmt.Errorf("unable to cast type %T to %T", val, out)
}
}
func CastToAction(val interface{}) (out *actionlog.Action, err error) {
switch val := expr.UntypedValue(val).(type) {
case *actionlog.Action:
return val, nil
default:
return nil, fmt.Errorf("unable to cast type %T to %T", val, out)
}
}
func (v *Action) Clone() (expr.TypedValue, error) {
aux := *v.value
return NewAction(&aux)
}
func (v *DocumentType) Clone() (expr.TypedValue, error) {
return NewDocumentType(v.value)
}
func (v *QueueMessage) Clone() (expr.TypedValue, error) {
aux := *v.value
return NewQueueMessage(&aux)
}
func (v *RbacResource) Clone() (expr.TypedValue, error) {
return NewRbacResource(v.value)
}
func (v *RenderOptions) Clone() (expr.TypedValue, error) {
aux := make(map[string]string)
for k, v := range v.value {
aux[k] = v
}
return NewRenderOptions(aux)
}
func (v *RenderedDocument) Clone() (expr.TypedValue, error) {
aux := *v.value
return NewRenderedDocument(&aux)
}
func (v *Reminder) Clone() (expr.TypedValue, error) {
if v.value == nil {
return NewReminder(nil)
}
aux := *v.value
return NewReminder(&aux)
}
func (v *Role) Clone() (expr.TypedValue, error) {
aux := v.value.Clone()
return NewRole(aux)
}
func (v *Template) Clone() (expr.TypedValue, error) {
aux := v.value.Clone()
return NewTemplate(aux)
}
func (v *TemplateMeta) Clone() (expr.TypedValue, error) {
return NewTemplateMeta(v.value)
}
func (v *User) Clone() (expr.TypedValue, error) {
aux := v.value.Clone()
return NewUser(aux)
}

View File

@@ -0,0 +1,97 @@
package: automation
imports:
- github.com/cortezaproject/corteza/server/system/types
- github.com/cortezaproject/corteza/server/pkg/rbac
- github.com/cortezaproject/corteza/server/pkg/actionlog
types:
Template:
as: '*types.Template'
# meta is a value struct, assigning to it needs to be rerouted
customFieldAssigner: true
struct:
- { name: 'ID', exprType: 'ID', goType: 'uint64', mode: ro, alias: 'templateID'}
- { name: 'handle', exprType: 'Handle', goType: 'string' }
- { name: 'language', exprType: 'String', goType: 'string' }
- { name: 'type', exprType: 'DocumentType', goType: 'types.DocumentType' }
- { name: 'partial', exprType: 'Boolean', goType: 'bool' }
- { name: 'meta', exprType: 'TemplateMeta', goType: 'types.TemplateMeta' }
- { name: 'template', exprType: 'String', goType: 'string' }
- { name: 'labels', exprType: 'LabelValue', goType: 'map[string]labelTypes.LabelValue' }
- { name: 'ownerID', exprType: 'ID', goType: 'uint64', mode: ro}
- { name: 'createdAt', exprType: 'DateTime', goType: 'time.Time', mode: ro}
- { name: 'updatedAt', exprType: 'DateTime', goType: '*time.Time', mode: ro }
- { name: 'deletedAt', exprType: 'DateTime', goType: '*time.Time', mode: ro }
- { name: 'lastUsedAt', exprType: 'DateTime', goType: '*time.Time', mode: ro }
User:
as: '*types.User'
struct:
- { name: 'ID', exprType: 'ID', goType: 'uint64', mode: ro, alias: 'userID' }
- { name: 'username', exprType: 'String', goType: 'string' }
- { name: 'email', exprType: 'String', goType: 'string' }
- { name: 'name', exprType: 'String', goType: 'string' }
- { name: 'handle', exprType: 'Handle', goType: 'string' }
- { name: 'emailConfirmed', exprType: 'Boolean', goType: 'bool' }
- { name: 'labels', exprType: 'LabelValue', goType: 'map[string]labelTypes.LabelValue' }
- { name: 'createdAt', exprType: 'DateTime', goType: 'time.Time', mode: ro}
- { name: 'updatedAt', exprType: 'DateTime', goType: '*time.Time', mode: ro }
- { name: 'suspendedAt', exprType: 'DateTime', goType: '*time.Time', mode: ro }
- { name: 'deletedAt', exprType: 'DateTime', goType: '*time.Time', mode: ro }
Role:
as: '*types.Role'
struct:
- { name: 'ID', exprType: 'ID', goType: 'uint64', mode: ro, alias: 'roleID' }
- { name: 'name', exprType: 'String', goType: 'string' }
- { name: 'handle', exprType: 'Handle', goType: 'string' }
- { name: 'labels', exprType: 'LabelValue', goType: 'map[string]labelTypes.LabelValue' }
- { name: 'createdAt', exprType: 'DateTime', goType: 'time.Time', mode: ro}
- { name: 'updatedAt', exprType: 'DateTime', goType: '*time.Time', mode: ro }
- { name: 'archivedAt', exprType: 'DateTime', goType: '*time.Time', mode: ro }
- { name: 'deletedAt', exprType: 'DateTime', goType: '*time.Time', mode: ro }
Reminder:
as: '*types.Reminder'
struct:
- { name: 'ID', exprType: 'ID', goType: 'uint64', mode: ro, alias: 'reminderID' }
- { name: 'resource', exprType: 'String', goType: 'string'}
- { name: 'snoozeCount', exprType: 'UnsignedInteger', goType: 'uint', mode: ro }
- { name: 'assignedTo', exprType: 'ID', goType: 'uint64' }
- { name: 'assignedBy', exprType: 'ID', goType: 'uint64', mode: ro }
- { name: 'assignedAt', exprType: 'DateTime', goType: 'time.Time', mode: ro }
- { name: 'dismissedBy', exprType: 'ID', goType: 'uint64', mode: ro }
- { name: 'dismissedAt', exprType: 'DateTime', goType: '*time.Time', mode: ro }
- { name: 'remindAt', exprType: 'DateTime', goType: '*time.Time' }
- { name: 'createdAt', exprType: 'DateTime', goType: 'time.Time', mode: ro }
- { name: 'updatedAt', exprType: 'DateTime', goType: '*time.Time', mode: ro }
- { name: 'deletedAt', exprType: 'DateTime', goType: '*time.Time', mode: ro }
- { name: 'payload', exprType: 'Bytes', goType: 'types.JSONText'}
DocumentType:
as: 'types.DocumentType'
TemplateMeta:
as: 'types.TemplateMeta'
struct:
- { name: 'short', exprType: 'String', goType: 'string'}
- { name: 'description', exprType: 'String', goType: 'string'}
- { name: 'headerTemplateID', exprType: 'ID', goType: 'uint64'}
- { name: 'footerTemplateID', exprType: 'ID', goType: 'uint64'}
RenderedDocument:
as: '*renderedDocument'
struct:
- { name: 'document', exprType: 'Reader', goType: 'io.Reader' }
- { name: 'name', exprType: 'string', goType: 'string' }
- { name: 'type', exprType: 'string', goType: 'string' }
RenderOptions:
as: 'map[string]string'
QueueMessage:
as: '*types.QueueMessage'
struct:
- { name: 'Queue', exprType: 'String', goType: 'string' }
- { name: 'Payload', exprType: 'Bytes', goType: '[]byte' }
RbacResource:
as: 'rbac.Resource'
Action:
as: '*actionlog.Action'

View File

@@ -0,0 +1,148 @@
package automation
import (
"context"
"testing"
"github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/cortezaproject/corteza/server/pkg/rbac"
"github.com/cortezaproject/corteza/server/system/types"
"github.com/stretchr/testify/require"
)
func TestUser(t *testing.T) {
var (
req = require.New(t)
u, err = NewUser(&types.User{Handle: "handle"})
)
req.NoError(err)
req.Equal("handle", u.value.Handle)
req.Error(u.AssignFieldValue("some-unexisting-field", nil))
req.NoError(u.AssignFieldValue("email", expr.Must(expr.NewString("dummy@domain.tpl"))))
req.Equal("dummy@domain.tpl", u.value.Email)
}
func TestUser_Expr(t *testing.T) {
var (
req = require.New(t)
u, _ = NewUser(&types.User{Handle: "hendl"})
scope = &expr.Vars{}
)
req.NoError(scope.Set("user", u))
eval, err := expr.NewParser().Parse("user.handle")
req.NoError(err)
res, err := eval.Eval(context.Background(), scope)
req.NoError(err)
req.Equal("hendl", res.(string))
}
func TestTemplate_metaAssign(t *testing.T) {
var (
req = require.New(t)
t1, err = NewTemplate(&types.Template{
Handle: "invoice",
Meta: types.TemplateMeta{Short: "before"},
})
)
req.NoError(err)
// fields of the meta struct
req.NoError(expr.Assign(t1, "meta.short", expr.Must(expr.NewString("after"))))
req.Equal("after", t1.value.Meta.Short)
req.NoError(expr.Assign(t1, "meta.headerTemplateID", expr.Must(expr.NewID(42))))
req.Equal(uint64(42), t1.value.Meta.HeaderTemplateID)
req.NoError(expr.Assign(t1, "meta.footerTemplateID", expr.Must(expr.NewID(43))))
req.Equal(uint64(43), t1.value.Meta.FooterTemplateID)
req.Error(expr.Assign(t1, "meta.unknown-field", expr.Must(expr.NewString("nope"))))
// whole meta struct
meta, err := NewTemplateMeta(types.TemplateMeta{Short: "whole", HeaderTemplateID: 99})
req.NoError(err)
req.NoError(expr.Assign(t1, "meta", meta))
req.Equal("whole", t1.value.Meta.Short)
req.Equal(uint64(99), t1.value.Meta.HeaderTemplateID)
// meta from a map, the way it comes in from a workflow
fromMap, err := NewTemplateMeta(map[string]interface{}{"short": "mapped", "footerTemplateID": "13"})
req.NoError(err)
req.NoError(expr.Assign(t1, "meta", fromMap))
req.Equal("mapped", t1.value.Meta.Short)
req.Equal(uint64(13), t1.value.Meta.FooterTemplateID)
// other fields still assign
req.NoError(expr.Assign(t1, "handle", expr.Must(expr.NewString("receipt"))))
req.Equal("receipt", t1.value.Handle)
req.Error(expr.Assign(t1, "some-unexisting-field", expr.Must(expr.NewString("nope"))))
}
// Assigning through a scope, the way workflow expression steps do it
func TestTemplate_metaAssignInScope(t *testing.T) {
var (
req = require.New(t)
t1, err = NewTemplate(&types.Template{Handle: "invoice"})
scope = &expr.Vars{}
)
req.NoError(err)
req.NoError(scope.Set("template", t1))
req.NoError(expr.Assign(scope, "template.meta.headerTemplateID", expr.Must(expr.NewID(42))))
req.NoError(expr.Assign(scope, "template.meta.short", expr.Must(expr.NewString("scoped"))))
selected, err := scope.Select("template")
req.NoError(err)
tpl, err := CastToTemplate(selected)
req.NoError(err)
req.Equal(uint64(42), tpl.Meta.HeaderTemplateID)
req.Equal("scoped", tpl.Meta.Short)
}
func TestCastToRbacResource(t *testing.T) {
var (
req = require.New(t)
)
t.Run("string to RbacResource", func(t *testing.T) {
resourceString := "corteza::system:user/461133310995726337"
resource, err := CastToRbacResource(resourceString)
req.NoError(err)
req.NotNil(resource)
req.Equal(resourceString, resource.RbacResource())
})
t.Run("existing RbacResource", func(t *testing.T) {
originalResource := rbac.NewResource("corteza::system:role/123")
resource, err := CastToRbacResource(originalResource)
req.NoError(err)
req.Equal(originalResource, resource)
})
t.Run("RbacResource expression type", func(t *testing.T) {
originalResource := rbac.NewResource("corteza::system:role/123")
rbacRes, err := NewRbacResource(originalResource)
req.NoError(err)
resource, err := CastToRbacResource(rbacRes)
req.NoError(err)
req.Equal(originalResource, resource)
})
t.Run("invalid type", func(t *testing.T) {
resource, err := CastToRbacResource(123)
req.Error(err)
req.Nil(resource)
req.Contains(err.Error(), "unable to cast type int to")
})
}

View File

@@ -0,0 +1,124 @@
package automation
// 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:
// system/automation/notification_handler.yaml
import (
"context"
atypes "github.com/cortezaproject/corteza/server/automation/types"
"github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/cortezaproject/corteza/server/pkg/wfexec"
)
var _ wfexec.ExecResponse
type (
notificationHandlerRegistry interface {
AddFunctions(ff ...*atypes.Function)
Type(ref string) expr.Type
}
)
func (h notificationHandler) register() {
h.reg.AddFunctions(
h.Send(),
)
}
type (
notificationSendArgs struct {
hasRecipient bool
Recipient interface{}
recipientID uint64
recipientHandle string
recipientEmail string
hasTitle bool
Title string
hasDescription bool
Description string
}
)
func (a notificationSendArgs) GetRecipient() (bool, uint64, string, string) {
return a.hasRecipient, a.recipientID, a.recipientHandle, a.recipientEmail
}
// Send function Send simple notification
//
// expects implementation of send function:
//
// func (h notificationHandler) send(ctx context.Context, args *notificationSendArgs) (err error) {
// return
// }
func (h notificationHandler) Send() *atypes.Function {
return &atypes.Function{
Ref: "notificationSend",
Kind: "function",
Labels: map[string]string(nil),
Meta: &atypes.FunctionMeta{
Short: "Send simple notification",
Description: "Sends a simple notification with title and description to a user",
},
Parameters: []*atypes.Param{
{
Name: "recipient",
Types: []string{"ID", "Handle", "String"}, Required: true,
Meta: &atypes.ParamMeta{
Label: "Recipient",
},
},
{
Name: "title",
Types: []string{"String"}, Required: true,
Meta: &atypes.ParamMeta{
Label: "Title",
},
},
{
Name: "description",
Types: []string{"String"},
Meta: &atypes.ParamMeta{
Label: "Description",
Visual: map[string]interface{}{"input": map[string]interface{}{"type": "richtext"}},
},
},
},
Handler: func(ctx context.Context, in *expr.Vars) (out *expr.Vars, err error) {
var (
args = &notificationSendArgs{
hasRecipient: in.Has("recipient"),
hasTitle: in.Has("title"),
hasDescription: in.Has("description"),
}
)
if err = in.Decode(args); err != nil {
return
}
// Converting Recipient argument
if args.hasRecipient {
aux := expr.Must(expr.Select(in, "recipient"))
switch aux.Type() {
case h.reg.Type("ID").Type():
args.recipientID = aux.Get().(uint64)
case h.reg.Type("Handle").Type():
args.recipientHandle = aux.Get().(string)
case h.reg.Type("String").Type():
args.recipientEmail = aux.Get().(string)
}
}
return out, h.send(ctx, args)
},
}
}

View File

@@ -0,0 +1,104 @@
package automation
import (
"context"
"fmt"
composeTypes "github.com/cortezaproject/corteza/server/compose/types"
systemTypes "github.com/cortezaproject/corteza/server/system/types"
"go.uber.org/zap"
)
type (
notificationHandler struct {
reg notificationHandlerRegistry
svc notificationService
uSvc notificationUserService
logger *zap.Logger
}
notificationService interface {
Create(context.Context, *systemTypes.Notification) (*systemTypes.Notification, error)
}
// Interface for user service that we need for recipient lookups
notificationUserService interface {
FindByID(ctx context.Context, userID uint64) (*systemTypes.User, error)
FindByHandle(ctx context.Context, handle string) (*systemTypes.User, error)
FindByEmail(ctx context.Context, email string) (*systemTypes.User, error)
}
notificationNamespaceService interface {
FindByHandle(ctx context.Context, handle string) (*composeTypes.Namespace, error)
}
notificationModuleService interface {
FindByHandle(ctx context.Context, handle string) (*composeTypes.Module, error)
}
)
// NotificationHandler initializes the notification handler
func NotificationHandler(reg notificationHandlerRegistry, ntfSvc notificationService, uSvc notificationUserService, log *zap.Logger) *notificationHandler {
h := &notificationHandler{
reg: reg,
svc: ntfSvc,
uSvc: uSvc,
logger: log.Named("notification"),
}
h.register()
return h
}
// send creates and sends a notification
func (h notificationHandler) send(ctx context.Context, args *notificationSendArgs) error {
// Get the recipient user ID from the input parameter
var recipientID uint64
// Check if we have a direct user object
if args.recipientID > 0 {
// Direct ID passed
recipientID = args.recipientID
} else {
// Need to look up the user
var user *systemTypes.User
var err error
if args.recipientHandle != "" {
user, err = h.uSvc.FindByHandle(ctx, args.recipientHandle)
} else if args.recipientEmail != "" {
user, err = h.uSvc.FindByEmail(ctx, args.recipientEmail)
} else {
return fmt.Errorf("invalid recipient: unable to determine user ID")
}
if err != nil {
return err
}
if user == nil {
return fmt.Errorf("recipient not found")
}
recipientID = user.ID
}
// Create notification config directly with the expected fields for a simple notification
config := systemTypes.NotificationConfig{
Simple: systemTypes.SimpleNotificationConfig{
Title: args.Title,
Description: args.Description,
},
}
// Create a simple notification
ntf := &systemTypes.Notification{
Kind: systemTypes.NotificationKindSimple,
Config: config,
Recipient: recipientID,
}
// Create the notification
_, err := h.svc.Create(ctx, ntf)
return err
}

View File

@@ -0,0 +1,30 @@
name: notification
snippets:
lookupUser: &lookupUser
meta: { label: Recipient }
required: true
types:
- { wf: ID }
- { wf: Handle }
- { wf: String, suffix: email }
functions:
send:
meta:
short: Send simple notification
description: Sends a simple notification with title and description to a user
params:
recipient: *lookupUser
title:
meta: { label: Title }
types:
- { wf: String }
required: true
description: The title of the simple notification
description:
types:
- { wf: String }
required: false
description: Optional description for the simple notification
meta: { label: Description, visual: { input: { type: richtext } } }

View File

@@ -0,0 +1,412 @@
package automation
// 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:
// system/automation/rbac_handler.yaml
import (
"context"
atypes "github.com/cortezaproject/corteza/server/automation/types"
"github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/cortezaproject/corteza/server/pkg/rbac"
"github.com/cortezaproject/corteza/server/pkg/wfexec"
"github.com/cortezaproject/corteza/server/system/types"
)
var _ wfexec.ExecResponse
type (
rbacHandlerRegistry interface {
AddFunctions(ff ...*atypes.Function)
Type(ref string) expr.Type
}
)
func (h rbacHandler) register() {
h.reg.AddFunctions(
h.Allow(),
h.Deny(),
h.Inherit(),
h.Check(),
)
}
type (
rbacAllowArgs struct {
hasResource bool
Resource rbac.Resource
hasRole bool
Role interface{}
roleID uint64
roleHandle string
roleRes *types.Role
hasOperation bool
Operation string
}
)
func (a rbacAllowArgs) GetRole() (bool, uint64, string, *types.Role) {
return a.hasRole, a.roleID, a.roleHandle, a.roleRes
}
// Allow function RBAC: Allow operation on resource to a role
//
// expects implementation of allow function:
//
// func (h rbacHandler) allow(ctx context.Context, args *rbacAllowArgs) (err error) {
// return
// }
func (h rbacHandler) Allow() *atypes.Function {
return &atypes.Function{
Ref: "rbacAllow",
Kind: "function",
Labels: map[string]string{"users": "rbac"},
Meta: &atypes.FunctionMeta{
Short: "RBAC: Allow operation on resource to a role",
},
Parameters: []*atypes.Param{
{
Name: "resource",
Types: []string{"RbacResource"}, Required: true,
Meta: &atypes.ParamMeta{
Label: "Resource",
},
},
{
Name: "role",
Types: []string{"ID", "Handle", "Role"}, Required: true,
Meta: &atypes.ParamMeta{
Label: "Role",
},
},
{
Name: "operation",
Types: []string{"String"}, Required: true,
Meta: &atypes.ParamMeta{
Label: "Operation",
},
},
},
Handler: func(ctx context.Context, in *expr.Vars) (out *expr.Vars, err error) {
var (
args = &rbacAllowArgs{
hasResource: in.Has("resource"),
hasRole: in.Has("role"),
hasOperation: in.Has("operation"),
}
)
if err = in.Decode(args); err != nil {
return
}
// Converting Role argument
if args.hasRole {
aux := expr.Must(expr.Select(in, "role"))
switch aux.Type() {
case h.reg.Type("ID").Type():
args.roleID = aux.Get().(uint64)
case h.reg.Type("Handle").Type():
args.roleHandle = aux.Get().(string)
case h.reg.Type("Role").Type():
args.roleRes = aux.Get().(*types.Role)
}
}
return out, h.allow(ctx, args)
},
}
}
type (
rbacDenyArgs struct {
hasResource bool
Resource rbac.Resource
hasRole bool
Role interface{}
roleID uint64
roleHandle string
roleRes *types.Role
hasOperation bool
Operation string
}
)
func (a rbacDenyArgs) GetRole() (bool, uint64, string, *types.Role) {
return a.hasRole, a.roleID, a.roleHandle, a.roleRes
}
// Deny function RBAC: Deny operation on resource to a role
//
// expects implementation of deny function:
//
// func (h rbacHandler) deny(ctx context.Context, args *rbacDenyArgs) (err error) {
// return
// }
func (h rbacHandler) Deny() *atypes.Function {
return &atypes.Function{
Ref: "rbacDeny",
Kind: "function",
Labels: map[string]string{"users": "rbac"},
Meta: &atypes.FunctionMeta{
Short: "RBAC: Deny operation on resource to a role",
},
Parameters: []*atypes.Param{
{
Name: "resource",
Types: []string{"RbacResource"}, Required: true,
Meta: &atypes.ParamMeta{
Label: "Resource",
},
},
{
Name: "role",
Types: []string{"ID", "Handle", "Role"}, Required: true,
Meta: &atypes.ParamMeta{
Label: "Role",
},
},
{
Name: "operation",
Types: []string{"String"}, Required: true,
Meta: &atypes.ParamMeta{
Label: "Operation",
},
},
},
Handler: func(ctx context.Context, in *expr.Vars) (out *expr.Vars, err error) {
var (
args = &rbacDenyArgs{
hasResource: in.Has("resource"),
hasRole: in.Has("role"),
hasOperation: in.Has("operation"),
}
)
if err = in.Decode(args); err != nil {
return
}
// Converting Role argument
if args.hasRole {
aux := expr.Must(expr.Select(in, "role"))
switch aux.Type() {
case h.reg.Type("ID").Type():
args.roleID = aux.Get().(uint64)
case h.reg.Type("Handle").Type():
args.roleHandle = aux.Get().(string)
case h.reg.Type("Role").Type():
args.roleRes = aux.Get().(*types.Role)
}
}
return out, h.deny(ctx, args)
},
}
}
type (
rbacInheritArgs struct {
hasResource bool
Resource rbac.Resource
hasRole bool
Role interface{}
roleID uint64
roleHandle string
roleRes *types.Role
hasOperation bool
Operation string
}
)
func (a rbacInheritArgs) GetRole() (bool, uint64, string, *types.Role) {
return a.hasRole, a.roleID, a.roleHandle, a.roleRes
}
// Inherit function RBAC: Remove allow/deny operation of a role from resource
//
// expects implementation of inherit function:
//
// func (h rbacHandler) inherit(ctx context.Context, args *rbacInheritArgs) (err error) {
// return
// }
func (h rbacHandler) Inherit() *atypes.Function {
return &atypes.Function{
Ref: "rbacInherit",
Kind: "function",
Labels: map[string]string{"users": "rbac"},
Meta: &atypes.FunctionMeta{
Short: "RBAC: Remove allow/deny operation of a role from resource",
},
Parameters: []*atypes.Param{
{
Name: "resource",
Types: []string{"RbacResource"}, Required: true,
Meta: &atypes.ParamMeta{
Label: "Resource",
},
},
{
Name: "role",
Types: []string{"ID", "Handle", "Role"}, Required: true,
Meta: &atypes.ParamMeta{
Label: "Role",
},
},
{
Name: "operation",
Types: []string{"String"}, Required: true,
Meta: &atypes.ParamMeta{
Label: "Operation",
},
},
},
Handler: func(ctx context.Context, in *expr.Vars) (out *expr.Vars, err error) {
var (
args = &rbacInheritArgs{
hasResource: in.Has("resource"),
hasRole: in.Has("role"),
hasOperation: in.Has("operation"),
}
)
if err = in.Decode(args); err != nil {
return
}
// Converting Role argument
if args.hasRole {
aux := expr.Must(expr.Select(in, "role"))
switch aux.Type() {
case h.reg.Type("ID").Type():
args.roleID = aux.Get().(uint64)
case h.reg.Type("Handle").Type():
args.roleHandle = aux.Get().(string)
case h.reg.Type("Role").Type():
args.roleRes = aux.Get().(*types.Role)
}
}
return out, h.inherit(ctx, args)
},
}
}
type (
rbacCheckArgs struct {
hasResource bool
Resource rbac.Resource
hasOperation bool
Operation string
hasUser bool
User *types.User
}
rbacCheckResults struct {
Can bool
}
)
// Check function RBAC: Can user perform an operation on a resource
//
// expects implementation of check function:
//
// func (h rbacHandler) check(ctx context.Context, args *rbacCheckArgs) (results *rbacCheckResults, err error) {
// return
// }
func (h rbacHandler) Check() *atypes.Function {
return &atypes.Function{
Ref: "rbacCheck",
Kind: "function",
Labels: map[string]string{"users": "rbac"},
Meta: &atypes.FunctionMeta{
Short: "RBAC: Can user perform an operation on a resource",
},
Parameters: []*atypes.Param{
{
Name: "resource",
Types: []string{"RbacResource"}, Required: true,
Meta: &atypes.ParamMeta{
Label: "Resource",
},
},
{
Name: "operation",
Types: []string{"String"}, Required: true,
Meta: &atypes.ParamMeta{
Label: "Operation",
},
},
{
Name: "user",
Types: []string{"User"},
Meta: &atypes.ParamMeta{
Label: "User",
},
},
},
Results: []*atypes.Param{
{
Name: "can",
Types: []string{"Boolean"},
},
},
Handler: func(ctx context.Context, in *expr.Vars) (out *expr.Vars, err error) {
var (
args = &rbacCheckArgs{
hasResource: in.Has("resource"),
hasOperation: in.Has("operation"),
hasUser: in.Has("user"),
}
)
if err = in.Decode(args); err != nil {
return
}
var results *rbacCheckResults
if results, err = h.check(ctx, args); err != nil {
return
}
out = &expr.Vars{}
{
// converting results.Can (bool) to Boolean
var (
tval expr.TypedValue
)
if tval, err = h.reg.Type("Boolean").Cast(results.Can); err != nil {
return
} else if err = expr.Assign(out, "can", tval); err != nil {
return
}
}
return
},
}
}

View File

@@ -0,0 +1,138 @@
package automation
import (
"context"
"fmt"
"github.com/cortezaproject/corteza/server/pkg/rbac"
"github.com/cortezaproject/corteza/server/system/types"
)
type (
rbacService interface {
Can(ses rbac.Session, op string, res rbac.Resource) bool
Grant(ctx context.Context, rules ...*rbac.Rule) (err error)
}
rbacUserService interface {
FindByAny(ctx context.Context, identifier interface{}) (*types.User, error)
}
rbacHandler struct {
reg rbacHandlerRegistry
rbac rbacService
user rbacUserService
role roleService
}
auxResource struct{ res string }
)
var _ rbac.Resource = &auxResource{}
func (r auxResource) RbacResource() string {
return r.res
}
func RbacHandler(reg rbacHandlerRegistry, rbac rbacService, user rbacUserService, role roleService) *rbacHandler {
h := &rbacHandler{
reg: reg,
rbac: rbac,
user: user,
role: role,
}
h.register()
return h
}
func (h rbacHandler) allow(ctx context.Context, args *rbacAllowArgs) (err error) {
r, err := lookupRole(ctx, h.role, rolesLookupArgs{
hasLookup: args.hasRole,
Lookup: args.Role,
lookupID: args.roleID,
lookupHandle: args.roleHandle,
lookupRes: args.roleRes,
})
if err != nil {
return
}
return h.grant(ctx, rbac.AllowRule(r.ID, args.Resource.RbacResource(), args.Operation))
}
func (h rbacHandler) deny(ctx context.Context, args *rbacDenyArgs) (err error) {
r, err := lookupRole(ctx, h.role, rolesLookupArgs{
hasLookup: args.hasRole,
Lookup: args.Role,
lookupID: args.roleID,
lookupHandle: args.roleHandle,
lookupRes: args.roleRes,
})
if err != nil {
return
}
return h.grant(ctx, rbac.DenyRule(r.ID, args.Resource.RbacResource(), args.Operation))
}
func (h rbacHandler) inherit(ctx context.Context, args *rbacInheritArgs) (err error) {
r, err := lookupRole(ctx, h.role, rolesLookupArgs{
hasLookup: args.hasRole,
Lookup: args.Role,
lookupID: args.roleID,
lookupHandle: args.roleHandle,
lookupRes: args.roleRes,
})
if err != nil {
return
}
return h.grant(ctx, rbac.InheritRule(r.ID, args.Resource.RbacResource(), args.Operation))
}
// verifies grant op (granter needs to be allowed to grant on the component
func (h rbacHandler) grant(ctx context.Context, r *rbac.Rule) (err error) {
// we can safely create a fake component resource since there are
// no extra metadata hidden under it,
cmpRes := &auxResource{rbac.ResourceComponent(r.Resource)}
if !h.rbac.Can(rbac.ContextToSession(ctx), "grant", cmpRes) {
return fmt.Errorf("not allowed to grant %s", r.String())
}
return h.rbac.Grant(ctx, r)
}
func (h rbacHandler) check(ctx context.Context, args *rbacCheckArgs) (b *rbacCheckResults, err error) {
var secses rbac.Session
b = &rbacCheckResults{}
if args.hasUser {
// user was explicitly set to rbac checking function
//
// load all roles that this user is member of
// and create new security session out of it
var (
mm types.RoleMemberSet
ids []uint64
)
mm, err = h.role.Membership(ctx, args.User.ID)
_ = mm.Walk(func(m *types.RoleMember) error {
ids = append(ids, m.RoleID)
return nil
})
args.User.SetRoles(ids...)
secses = rbac.NewSession(ctx, args.User)
} else {
secses = rbac.ContextToSession(ctx)
}
b.Can = h.rbac.Can(secses, args.Operation, args.Resource)
return
}

View File

@@ -0,0 +1,72 @@
imports:
- github.com/cortezaproject/corteza/server/system/types
- github.com/cortezaproject/corteza/server/pkg/rbac
params:
roleLookup: &roleLookup
meta: { label: Role }
required: true
types:
- { wf: ID, }
- { wf: Handle, }
- { wf: Role, suffix: res }
manageParams: &manageParams
resource:
meta: { label: Resource }
required: true
types:
# Note that we can not accept string here because
# we need whole resource with all the info loaded
- { wf: RbacResource }
role: *roleLookup
operation:
meta: { label: Operation }
required: true
types:
- { wf: String }
labels: &labels
users: "rbac"
functions:
allow:
meta:
short: 'RBAC: Allow operation on resource to a role'
labels:
<<: *labels
params: *manageParams
deny:
meta:
short: 'RBAC: Deny operation on resource to a role'
labels:
<<: *labels
params: *manageParams
inherit:
meta:
short: 'RBAC: Remove allow/deny operation of a role from resource'
labels:
<<: *labels
params: *manageParams
check:
meta:
short: 'RBAC: Can user perform an operation on a resource'
labels:
<<: *labels
params:
resource:
meta: { label: Resource }
required: true
types: [ { wf: RbacResource } ]
operation:
meta: { label: Operation }
required: true
types: [ { wf: String } ]
user:
meta: { label: User }
types: [ { wf: User } ]
results:
can: { wf: Boolean }

View File

@@ -0,0 +1,956 @@
package automation
// 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:
// system/automation/reminders_handler.yaml
import (
"context"
atypes "github.com/cortezaproject/corteza/server/automation/types"
"github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/cortezaproject/corteza/server/pkg/wfexec"
"github.com/cortezaproject/corteza/server/system/types"
"time"
)
var _ wfexec.ExecResponse
type (
remindersHandlerRegistry interface {
AddFunctions(ff ...*atypes.Function)
Type(ref string) expr.Type
}
)
func (h remindersHandler) register() {
h.reg.AddFunctions(
h.Lookup(),
h.Search(),
h.Each(),
h.Create(),
h.Update(),
h.Dismiss(),
h.Snooze(),
h.Delete(),
)
}
type (
remindersLookupArgs struct {
hasLookup bool
Lookup interface{}
lookupID uint64
lookupRes *types.Reminder
}
remindersLookupResults struct {
Reminder *types.Reminder
}
)
func (a remindersLookupArgs) GetLookup() (bool, uint64, *types.Reminder) {
return a.hasLookup, a.lookupID, a.lookupRes
}
// Lookup function Reminder lookup
//
// expects implementation of lookup function:
//
// func (h remindersHandler) lookup(ctx context.Context, args *remindersLookupArgs) (results *remindersLookupResults, err error) {
// return
// }
func (h remindersHandler) Lookup() *atypes.Function {
return &atypes.Function{
Ref: "remindersLookup",
Kind: "function",
Labels: map[string]string{"reminders": "step,workflow"},
Meta: &atypes.FunctionMeta{
Short: "Reminder lookup",
Description: "Find specific reminder by ID",
},
Parameters: []*atypes.Param{
{
Name: "lookup",
Types: []string{"ID", "Reminder"}, Required: true,
Meta: &atypes.ParamMeta{
Label: "Lookup",
},
},
},
Results: []*atypes.Param{
{
Name: "reminder",
Types: []string{"Reminder"},
},
},
Handler: func(ctx context.Context, in *expr.Vars) (out *expr.Vars, err error) {
var (
args = &remindersLookupArgs{
hasLookup: in.Has("lookup"),
}
)
if err = in.Decode(args); err != nil {
return
}
// Converting Lookup argument
if args.hasLookup {
aux := expr.Must(expr.Select(in, "lookup"))
switch aux.Type() {
case h.reg.Type("ID").Type():
args.lookupID = aux.Get().(uint64)
case h.reg.Type("Reminder").Type():
args.lookupRes = aux.Get().(*types.Reminder)
}
}
var results *remindersLookupResults
if results, err = h.lookup(ctx, args); err != nil {
return
}
out = &expr.Vars{}
{
// converting results.Reminder (*types.Reminder) to Reminder
var (
tval expr.TypedValue
)
if tval, err = h.reg.Type("Reminder").Cast(results.Reminder); err != nil {
return
} else if err = expr.Assign(out, "reminder", tval); err != nil {
return
}
}
return
},
}
}
type (
remindersSearchArgs struct {
hasResource bool
Resource string
hasAssignedTo bool
AssignedTo uint64
hasExcludeDismissed bool
ExcludeDismissed bool
hasScheduledOnly bool
ScheduledOnly bool
hasSort bool
Sort string
hasLimit bool
Limit uint64
hasIncTotal bool
IncTotal bool
hasPageCursor bool
PageCursor string
}
remindersSearchResults struct {
Reminders []*types.Reminder
Total uint64
}
)
// Search function Reminders search
//
// expects implementation of search function:
//
// func (h remindersHandler) search(ctx context.Context, args *remindersSearchArgs) (results *remindersSearchResults, err error) {
// return
// }
func (h remindersHandler) Search() *atypes.Function {
return &atypes.Function{
Ref: "remindersSearch",
Kind: "function",
Labels: map[string]string{"reminders": "step,workflow"},
Meta: &atypes.FunctionMeta{
Short: "Reminders search",
Description: "Search reminders",
},
Parameters: []*atypes.Param{
{
Name: "resource",
Types: []string{"String"},
Meta: &atypes.ParamMeta{
Label: "Resource",
},
},
{
Name: "assignedTo",
Types: []string{"ID"},
Meta: &atypes.ParamMeta{
Label: "Assigned to",
},
},
{
Name: "excludeDismissed",
Types: []string{"Boolean"},
Meta: &atypes.ParamMeta{
Label: "Exclude dismissed",
},
},
{
Name: "scheduledOnly",
Types: []string{"Boolean"},
Meta: &atypes.ParamMeta{
Label: "Scheduled only",
},
},
{
Name: "sort",
Types: []string{"String"},
Meta: &atypes.ParamMeta{
Label: "Sort",
},
},
{
Name: "limit",
Types: []string{"UnsignedInteger"},
Meta: &atypes.ParamMeta{
Label: "Limit",
},
},
{
Name: "incTotal",
Types: []string{"Boolean"},
Meta: &atypes.ParamMeta{
Label: "Include total",
},
},
{
Name: "pageCursor",
Types: []string{"String"},
Meta: &atypes.ParamMeta{
Label: "Page cursor",
},
},
},
Results: []*atypes.Param{
{
Name: "reminders",
Types: []string{"Reminder"},
IsArray: true,
},
{
Name: "total",
Types: []string{"UnsignedInteger"},
},
},
Handler: func(ctx context.Context, in *expr.Vars) (out *expr.Vars, err error) {
var (
args = &remindersSearchArgs{
hasResource: in.Has("resource"),
hasAssignedTo: in.Has("assignedTo"),
hasExcludeDismissed: in.Has("excludeDismissed"),
hasScheduledOnly: in.Has("scheduledOnly"),
hasSort: in.Has("sort"),
hasLimit: in.Has("limit"),
hasIncTotal: in.Has("incTotal"),
hasPageCursor: in.Has("pageCursor"),
}
)
if err = in.Decode(args); err != nil {
return
}
var results *remindersSearchResults
if results, err = h.search(ctx, args); err != nil {
return
}
out = &expr.Vars{}
{
// converting results.Reminders (*types.Reminder) to Array (of Reminder)
var (
tval expr.TypedValue
tarr = make([]expr.TypedValue, len(results.Reminders))
)
for i := range results.Reminders {
if tarr[i], err = h.reg.Type("Reminder").Cast(results.Reminders[i]); err != nil {
return
}
}
if tval, err = expr.NewArray(tarr); err != nil {
return
} else if err = expr.Assign(out, "reminders", tval); err != nil {
return
}
}
{
// converting results.Total (uint64) to UnsignedInteger
var (
tval expr.TypedValue
)
if tval, err = h.reg.Type("UnsignedInteger").Cast(results.Total); err != nil {
return
} else if err = expr.Assign(out, "total", tval); err != nil {
return
}
}
return
},
}
}
type (
remindersEachArgs struct {
hasResource bool
Resource string
hasAssignedTo bool
AssignedTo uint64
hasExcludeDismissed bool
ExcludeDismissed bool
hasScheduledOnly bool
ScheduledOnly bool
hasSort bool
Sort string
hasLimit bool
Limit uint64
hasIncTotal bool
IncTotal bool
hasPageCursor bool
PageCursor string
}
remindersEachResults struct {
Reminder *types.Reminder
Total uint64
}
)
// Each function Iterate reminders
//
// expects implementation of each function:
//
// func (h remindersHandler) each(ctx context.Context, args *remindersEachArgs) (results *remindersEachResults, err error) {
// return
// }
func (h remindersHandler) Each() *atypes.Function {
return &atypes.Function{
Ref: "remindersEach",
Kind: "iterator",
Labels: map[string]string(nil),
Meta: &atypes.FunctionMeta{
Short: "Iterate reminders",
},
Parameters: []*atypes.Param{
{
Name: "resource",
Types: []string{"String"},
Meta: &atypes.ParamMeta{
Label: "Resource",
},
},
{
Name: "assignedTo",
Types: []string{"ID"},
Meta: &atypes.ParamMeta{
Label: "Assigned to",
},
},
{
Name: "excludeDismissed",
Types: []string{"Boolean"},
Meta: &atypes.ParamMeta{
Label: "Exclude dismissed",
},
},
{
Name: "scheduledOnly",
Types: []string{"Boolean"},
Meta: &atypes.ParamMeta{
Label: "Scheduled only",
},
},
{
Name: "sort",
Types: []string{"String"},
Meta: &atypes.ParamMeta{
Label: "Sort",
},
},
{
Name: "limit",
Types: []string{"UnsignedInteger"},
Meta: &atypes.ParamMeta{
Label: "Limit",
},
},
{
Name: "incTotal",
Types: []string{"Boolean"},
Meta: &atypes.ParamMeta{
Label: "Include total",
},
},
{
Name: "pageCursor",
Types: []string{"String"},
Meta: &atypes.ParamMeta{
Label: "Page cursor",
},
},
},
Results: []*atypes.Param{
{
Name: "reminder",
Types: []string{"Reminder"},
},
{
Name: "total",
Types: []string{"UnsignedInteger"},
},
},
Iterator: func(ctx context.Context, in *expr.Vars) (out wfexec.IteratorHandler, err error) {
var (
args = &remindersEachArgs{
hasResource: in.Has("resource"),
hasAssignedTo: in.Has("assignedTo"),
hasExcludeDismissed: in.Has("excludeDismissed"),
hasScheduledOnly: in.Has("scheduledOnly"),
hasSort: in.Has("sort"),
hasLimit: in.Has("limit"),
hasIncTotal: in.Has("incTotal"),
hasPageCursor: in.Has("pageCursor"),
}
)
if err = in.Decode(args); err != nil {
return
}
return h.each(ctx, args)
},
}
}
type (
remindersCreateArgs struct {
hasResource bool
Resource string
hasAssignedTo bool
AssignedTo uint64
hasAssignedBy bool
AssignedBy uint64
hasRemindAt bool
RemindAt *time.Time
hasPayload bool
Payload []byte
}
remindersCreateResults struct {
Reminder *types.Reminder
}
)
// Create function Reminder create
//
// expects implementation of create function:
//
// func (h remindersHandler) create(ctx context.Context, args *remindersCreateArgs) (results *remindersCreateResults, err error) {
// return
// }
func (h remindersHandler) Create() *atypes.Function {
return &atypes.Function{
Ref: "remindersCreate",
Kind: "function",
Labels: map[string]string{"create": "step", "reminders": "step,workflow"},
Meta: &atypes.FunctionMeta{
Short: "Reminder create",
},
Parameters: []*atypes.Param{
{
Name: "resource",
Types: []string{"String"},
Meta: &atypes.ParamMeta{
Label: "Resource",
},
},
{
Name: "assignedTo",
Types: []string{"ID"},
Meta: &atypes.ParamMeta{
Label: "Assigned to",
},
},
{
Name: "assignedBy",
Types: []string{"ID"},
Meta: &atypes.ParamMeta{
Label: "Assigned by",
},
},
{
Name: "remindAt",
Types: []string{"DateTime"}, Required: true,
Meta: &atypes.ParamMeta{
Label: "Remind at",
},
},
{
Name: "payload",
Types: []string{"Bytes"},
Meta: &atypes.ParamMeta{
Label: "Payload",
},
},
},
Results: []*atypes.Param{
{
Name: "reminder",
Types: []string{"Reminder"},
},
},
Handler: func(ctx context.Context, in *expr.Vars) (out *expr.Vars, err error) {
var (
args = &remindersCreateArgs{
hasResource: in.Has("resource"),
hasAssignedTo: in.Has("assignedTo"),
hasAssignedBy: in.Has("assignedBy"),
hasRemindAt: in.Has("remindAt"),
hasPayload: in.Has("payload"),
}
)
if err = in.Decode(args); err != nil {
return
}
var results *remindersCreateResults
if results, err = h.create(ctx, args); err != nil {
return
}
out = &expr.Vars{}
{
// converting results.Reminder (*types.Reminder) to Reminder
var (
tval expr.TypedValue
)
if tval, err = h.reg.Type("Reminder").Cast(results.Reminder); err != nil {
return
} else if err = expr.Assign(out, "reminder", tval); err != nil {
return
}
}
return
},
}
}
type (
remindersUpdateArgs struct {
hasLookup bool
Lookup interface{}
lookupID uint64
lookupRes *types.Reminder
hasResource bool
Resource string
hasAssignedTo bool
AssignedTo uint64
hasAssignedBy bool
AssignedBy uint64
hasRemindAt bool
RemindAt *time.Time
hasPayload bool
Payload []byte
}
remindersUpdateResults struct {
Reminder *types.Reminder
}
)
func (a remindersUpdateArgs) GetLookup() (bool, uint64, *types.Reminder) {
return a.hasLookup, a.lookupID, a.lookupRes
}
// Update function Reminder update
//
// expects implementation of update function:
//
// func (h remindersHandler) update(ctx context.Context, args *remindersUpdateArgs) (results *remindersUpdateResults, err error) {
// return
// }
func (h remindersHandler) Update() *atypes.Function {
return &atypes.Function{
Ref: "remindersUpdate",
Kind: "function",
Labels: map[string]string{"reminders": "step,workflow", "update": "step"},
Meta: &atypes.FunctionMeta{
Short: "Reminder update",
},
Parameters: []*atypes.Param{
{
Name: "lookup",
Types: []string{"ID", "Reminder"}, Required: true,
Meta: &atypes.ParamMeta{
Label: "Lookup",
},
},
{
Name: "resource",
Types: []string{"String"},
Meta: &atypes.ParamMeta{
Label: "Resource",
},
},
{
Name: "assignedTo",
Types: []string{"ID"},
Meta: &atypes.ParamMeta{
Label: "Assigned to",
},
},
{
Name: "assignedBy",
Types: []string{"ID"},
Meta: &atypes.ParamMeta{
Label: "Assigned by",
},
},
{
Name: "remindAt",
Types: []string{"DateTime"},
Meta: &atypes.ParamMeta{
Label: "Remind at",
},
},
{
Name: "payload",
Types: []string{"Bytes"},
Meta: &atypes.ParamMeta{
Label: "Payload",
},
},
},
Results: []*atypes.Param{
{
Name: "reminder",
Types: []string{"Reminder"},
},
},
Handler: func(ctx context.Context, in *expr.Vars) (out *expr.Vars, err error) {
var (
args = &remindersUpdateArgs{
hasLookup: in.Has("lookup"),
hasResource: in.Has("resource"),
hasAssignedTo: in.Has("assignedTo"),
hasAssignedBy: in.Has("assignedBy"),
hasRemindAt: in.Has("remindAt"),
hasPayload: in.Has("payload"),
}
)
if err = in.Decode(args); err != nil {
return
}
// Converting Lookup argument
if args.hasLookup {
aux := expr.Must(expr.Select(in, "lookup"))
switch aux.Type() {
case h.reg.Type("ID").Type():
args.lookupID = aux.Get().(uint64)
case h.reg.Type("Reminder").Type():
args.lookupRes = aux.Get().(*types.Reminder)
}
}
var results *remindersUpdateResults
if results, err = h.update(ctx, args); err != nil {
return
}
out = &expr.Vars{}
{
// converting results.Reminder (*types.Reminder) to Reminder
var (
tval expr.TypedValue
)
if tval, err = h.reg.Type("Reminder").Cast(results.Reminder); err != nil {
return
} else if err = expr.Assign(out, "reminder", tval); err != nil {
return
}
}
return
},
}
}
type (
remindersDismissArgs struct {
hasLookup bool
Lookup interface{}
lookupID uint64
lookupRes *types.Reminder
}
)
func (a remindersDismissArgs) GetLookup() (bool, uint64, *types.Reminder) {
return a.hasLookup, a.lookupID, a.lookupRes
}
// Dismiss function Dismiss reminder
//
// expects implementation of dismiss function:
//
// func (h remindersHandler) dismiss(ctx context.Context, args *remindersDismissArgs) (err error) {
// return
// }
func (h remindersHandler) Dismiss() *atypes.Function {
return &atypes.Function{
Ref: "remindersDismiss",
Kind: "function",
Labels: map[string]string{"dismiss": "step", "reminders": "step,workflow"},
Meta: &atypes.FunctionMeta{
Short: "Dismiss reminder",
},
Parameters: []*atypes.Param{
{
Name: "lookup",
Types: []string{"ID", "Reminder"}, Required: true,
Meta: &atypes.ParamMeta{
Label: "Lookup",
},
},
},
Handler: func(ctx context.Context, in *expr.Vars) (out *expr.Vars, err error) {
var (
args = &remindersDismissArgs{
hasLookup: in.Has("lookup"),
}
)
if err = in.Decode(args); err != nil {
return
}
// Converting Lookup argument
if args.hasLookup {
aux := expr.Must(expr.Select(in, "lookup"))
switch aux.Type() {
case h.reg.Type("ID").Type():
args.lookupID = aux.Get().(uint64)
case h.reg.Type("Reminder").Type():
args.lookupRes = aux.Get().(*types.Reminder)
}
}
return out, h.dismiss(ctx, args)
},
}
}
type (
remindersSnoozeArgs struct {
hasLookup bool
Lookup interface{}
lookupID uint64
lookupRes *types.Reminder
hasRemindAt bool
RemindAt *time.Time
}
)
func (a remindersSnoozeArgs) GetLookup() (bool, uint64, *types.Reminder) {
return a.hasLookup, a.lookupID, a.lookupRes
}
// Snooze function Snooze reminder
//
// expects implementation of snooze function:
//
// func (h remindersHandler) snooze(ctx context.Context, args *remindersSnoozeArgs) (err error) {
// return
// }
func (h remindersHandler) Snooze() *atypes.Function {
return &atypes.Function{
Ref: "remindersSnooze",
Kind: "function",
Labels: map[string]string{"reminders": "step,workflow", "snooze": "step"},
Meta: &atypes.FunctionMeta{
Short: "Snooze reminder",
},
Parameters: []*atypes.Param{
{
Name: "lookup",
Types: []string{"ID", "Reminder"}, Required: true,
Meta: &atypes.ParamMeta{
Label: "Lookup",
},
},
{
Name: "remindAt",
Types: []string{"DateTime"}, Required: true,
Meta: &atypes.ParamMeta{
Label: "Remind at",
},
},
},
Handler: func(ctx context.Context, in *expr.Vars) (out *expr.Vars, err error) {
var (
args = &remindersSnoozeArgs{
hasLookup: in.Has("lookup"),
hasRemindAt: in.Has("remindAt"),
}
)
if err = in.Decode(args); err != nil {
return
}
// Converting Lookup argument
if args.hasLookup {
aux := expr.Must(expr.Select(in, "lookup"))
switch aux.Type() {
case h.reg.Type("ID").Type():
args.lookupID = aux.Get().(uint64)
case h.reg.Type("Reminder").Type():
args.lookupRes = aux.Get().(*types.Reminder)
}
}
return out, h.snooze(ctx, args)
},
}
}
type (
remindersDeleteArgs struct {
hasLookup bool
Lookup interface{}
lookupID uint64
lookupRes *types.Reminder
}
)
func (a remindersDeleteArgs) GetLookup() (bool, uint64, *types.Reminder) {
return a.hasLookup, a.lookupID, a.lookupRes
}
// Delete function Reminder delete
//
// expects implementation of delete function:
//
// func (h remindersHandler) delete(ctx context.Context, args *remindersDeleteArgs) (err error) {
// return
// }
func (h remindersHandler) Delete() *atypes.Function {
return &atypes.Function{
Ref: "remindersDelete",
Kind: "function",
Labels: map[string]string{"delete": "step", "reminders": "step,workflow"},
Meta: &atypes.FunctionMeta{
Short: "Reminder delete",
},
Parameters: []*atypes.Param{
{
Name: "lookup",
Types: []string{"ID", "Reminder"}, Required: true,
Meta: &atypes.ParamMeta{
Label: "Lookup",
},
},
},
Handler: func(ctx context.Context, in *expr.Vars) (out *expr.Vars, err error) {
var (
args = &remindersDeleteArgs{
hasLookup: in.Has("lookup"),
}
)
if err = in.Decode(args); err != nil {
return
}
// Converting Lookup argument
if args.hasLookup {
aux := expr.Must(expr.Select(in, "lookup"))
switch aux.Type() {
case h.reg.Type("ID").Type():
args.lookupID = aux.Get().(uint64)
case h.reg.Type("Reminder").Type():
args.lookupRes = aux.Get().(*types.Reminder)
}
}
return out, h.delete(ctx, args)
},
}
}

View File

@@ -0,0 +1,288 @@
package automation
import (
"context"
"fmt"
"time"
intAuth "github.com/cortezaproject/corteza/server/pkg/auth"
"github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/cortezaproject/corteza/server/pkg/filter"
. "github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/cortezaproject/corteza/server/pkg/wfexec"
"github.com/cortezaproject/corteza/server/system/types"
sqlxtypes "github.com/jmoiron/sqlx/types"
)
type(
reminderService interface {
FindByID(ctx context.Context,ID uint64) (*types.Reminder, error)
Find(ctx context.Context, filter types.ReminderFilter) (types.ReminderSet,types.ReminderFilter,error)
Create(ctx context.Context, reminder *types.Reminder) (*types.Reminder,error)
Update(ctx context.Context, reminder *types.Reminder) (*types.Reminder,error)
Delete(ctx context.Context, ID uint64) error
Dismiss(ctx context.Context, ID uint64) error
Snooze(ctx context.Context, ID uint64,remindAt *time.Time) error
}
remindersHandler struct {
reg remindersHandlerRegistry
rSvc reminderService
}
reminderSetIterator struct {
// Item buffer, current item pointer, and total items traversed
ptr uint
buffer types.ReminderSet
total uint
// When filter limit is set, this constraints it
iterLimit uint
useIterLimit bool
// Item loader for additional chunks
filter types.ReminderFilter
loader func() error
// Total reported by the first page
resTotal uint
}
reminderLookup interface {
GetLookup() (bool, uint64, *types.Reminder)
}
)
func RemindersHandler(reg remindersHandlerRegistry,rSvc reminderService) *remindersHandler {
h := &remindersHandler{
reg : reg,
rSvc: rSvc,
}
h.register()
return h
}
func (h remindersHandler) lookup(ctx context.Context, args *remindersLookupArgs) (results *remindersLookupResults, err error){
results = &remindersLookupResults{}
results.Reminder, err = lookupReminder(ctx, h.rSvc, args)
return
}
func (h remindersHandler) search(ctx context.Context,args *remindersSearchArgs) (results *remindersSearchResults,err error){
results = &remindersSearchResults{}
var (
f=types.ReminderFilter{
Resource: args.Resource,
AssignedTo: args.AssignedTo,
ExcludeDismissed: args.ExcludeDismissed,
ScheduledOnly: args.ScheduledOnly,
}
)
if args.hasSort {
if err = f.Sort.Set(args.Sort); err != nil {
return
}
}
if args.hasPageCursor && args.PageCursor != "" {
f.PageCursor = &filter.PagingCursor{}
if err = f.PageCursor.UnmarshalJSON([]byte(args.PageCursor)); err != nil {
return
}
}
if args.hasLimit {
f.Limit = uint(args.Limit)
}
// Total cannot be fetched together with a page cursor
f.IncTotal = args.IncTotal && f.PageCursor == nil
var auxf types.ReminderFilter
results.Reminders, auxf, err = h.rSvc.Find(ctx, f)
results.Total=uint64(auxf.Total)
return
}
func (h remindersHandler) each(ctx context.Context, args *remindersEachArgs) (out wfexec.IteratorHandler, err error){
var(
i = &reminderSetIterator{}
f=types.ReminderFilter{
Resource: args.Resource,
AssignedTo: args.AssignedTo,
ExcludeDismissed: args.ExcludeDismissed,
ScheduledOnly: args.ScheduledOnly,
}
)
if args.hasSort {
if err = f.Sort.Set(args.Sort); err != nil {
return
}
}
if args.hasPageCursor && args.PageCursor != "" {
f.NextPage = &filter.PagingCursor{}
if err = f.NextPage.UnmarshalJSON([]byte(args.PageCursor)); err != nil {
return
}
}
f.IncTotal = args.IncTotal
if args.hasLimit {
i.useIterLimit = true
i.iterLimit = uint(args.Limit)
f.Limit = uint(args.Limit)
if args.Limit > uint64(wfexec.MaxIteratorBufferSize) {
f.Limit = wfexec.MaxIteratorBufferSize
}
i.iterLimit = uint(args.Limit)
} else {
f.Limit = wfexec.MaxIteratorBufferSize
}
i.filter = f
i.loader = func() (err error) {
if i.filter.PageCursor != nil && i.filter.NextPage == nil {
return
}
i.total += i.ptr
i.ptr = 0
i.filter.PageCursor = i.filter.NextPage
i.filter.NextPage = nil
// Total is fetched with the first page only; a paged query cannot carry it
i.filter.IncTotal = i.filter.IncTotal && i.filter.PageCursor == nil
i.buffer, i.filter, err = h.rSvc.Find(ctx, i.filter)
if i.filter.IncTotal {
i.resTotal = i.filter.Total
}
return
}
return i, i.loader()
}
func (h remindersHandler) create(ctx context.Context, args *remindersCreateArgs) (results *remindersCreateResults, err error) {
results = &remindersCreateResults{}
currentUser := intAuth.GetIdentityFromContext(ctx).Identity()
reminder := &types.Reminder{
Resource:args.Resource,
AssignedTo: args.AssignedTo,
AssignedBy: args.AssignedBy,
RemindAt: args.RemindAt,
Payload: args.Payload,
}
if reminder.AssignedTo == 0 {
reminder.AssignedTo = currentUser
}
if reminder.AssignedBy == 0 {
reminder.AssignedBy = currentUser
}
if len(reminder.Payload) == 0 {
reminder.Payload = sqlxtypes.JSONText(`{"title": "", "note": ""}`)
}
if reminder.RemindAt == nil {
return nil, fmt.Errorf("remindAt is required")
}
results.Reminder, err = h.rSvc.Create(ctx, reminder)
return
}
func (h remindersHandler) update(ctx context.Context, args *remindersUpdateArgs) (results *remindersUpdateResults, err error) {
results = &remindersUpdateResults{}
reminder, err := lookupReminder(ctx, h.rSvc, args)
if err != nil {
return nil, err
}
if args.hasResource {
reminder.Resource = args.Resource
}
if args.hasAssignedTo {
reminder.AssignedTo = args.AssignedTo
}
if args.hasAssignedBy {
reminder.AssignedBy = args.AssignedBy
}
if args.hasRemindAt {
reminder.RemindAt = args.RemindAt
}
if args.hasPayload {
reminder.Payload = args.Payload
}
results.Reminder, err = h.rSvc.Update(ctx, reminder)
return
}
func (h remindersHandler) dismiss(ctx context.Context, args *remindersDismissArgs) error {
if id, err := getReminderID(ctx, h.rSvc, args); err != nil {
return err
} else {
return h.rSvc.Dismiss(ctx, id)
}
}
func (h remindersHandler) snooze(ctx context.Context, args *remindersSnoozeArgs) error {
if id, err := getReminderID(ctx, h.rSvc, args); err != nil {
return err
} else {
return h.rSvc.Snooze(ctx, id, args.RemindAt)
}
}
func (h remindersHandler) delete(ctx context.Context, args *remindersDeleteArgs) error {
if id, err := getReminderID(ctx, h.rSvc, args); err != nil {
return err
} else {
return h.rSvc.Delete(ctx, id)
}
}
func getReminderID(ctx context.Context, svc reminderService, args reminderLookup) (uint64, error) {
_, ID, reminder := args.GetLookup()
switch {
case reminder != nil:
return reminder.ID, nil
case ID > 0:
return ID, nil
}
lookupResult, err := lookupReminder(ctx, svc, args)
if err != nil {
return 0, err
}
return lookupResult.ID, nil
}
func lookupReminder(ctx context.Context, svc reminderService, args reminderLookup) (*types.Reminder, error) {
_, ID, reminder := args.GetLookup()
switch {
case reminder != nil:
return reminder, nil
case ID > 0:
return svc.FindByID(ctx, ID)
}
return nil, fmt.Errorf("empty lookup params")
}
func (i *reminderSetIterator) More(context.Context, *expr.Vars) (bool, error) {
a := wfexec.GenericResourceNextCheck(i.useIterLimit, i.ptr, uint(len(i.buffer)), i.total, i.iterLimit, i.filter.NextPage != nil)
return a, nil
}
func (i *reminderSetIterator) Start(context.Context, *expr.Vars) error {
i.ptr = 0
return nil
}
func (i *reminderSetIterator) Next(context.Context, *expr.Vars) (out *expr.Vars, err error) {
if len(i.buffer)-int(i.ptr) <= 0 {
if err = i.loader(); err != nil {
panic(err)
}
}
out = &expr.Vars{}
reminder := *i.buffer[i.ptr] // Make a copy
reminder.Payload = sqlxtypes.JSONText(string(i.buffer[i.ptr].Payload))
out.Set("reminder", Must(NewReminder(&reminder)))
out.Set("total", Must(NewInteger(i.resTotal)))
i.ptr++
return out, nil
}

View File

@@ -0,0 +1,173 @@
imports:
- github.com/cortezaproject/corteza/server/system/types
- time
params:
lookup: &lookup
meta: { label: Lookup }
required: true
types:
- { wf: ID, }
- { wf: Reminder, suffix: res }
reminder: &reminder
types:
- { wf: Reminder }
rvReminder: &rvReminder
wf: Reminder
rvTotal: &rvTotal
wf: UnsignedInteger
filterParams: &filterParams
resource:
meta: { label: Resource }
types:
- { wf: String }
assignedTo:
meta: { label: Assigned to }
types:
- { wf: ID }
excludeDismissed:
meta: { label: Exclude dismissed }
types:
- { wf: Boolean }
scheduledOnly:
meta: { label: Scheduled only }
types:
- { wf: Boolean }
sort:
meta: { label: Sort }
types:
- { wf: String }
limit:
meta: { label: Limit }
types:
- { wf: UnsignedInteger }
incTotal:
meta: { label: Include total }
types:
- { wf: Boolean }
pageCursor:
meta: { label: Page cursor }
types:
- { wf: String }
labels: &labels
reminders: "step,workflow"
functions:
lookup:
meta:
short: Reminder lookup
description: Find specific reminder by ID
labels:
<<: *labels
params:
lookup: *lookup
results:
reminder: *rvReminder
search:
meta:
short: Reminders search
description: Search reminders
labels:
<<: *labels
params: *filterParams
results:
reminders:
<<: *rvReminder
isArray: true
total: *rvTotal
each:
kind: iterator
meta:
short: Iterate reminders
params: *filterParams
results:
reminder: *rvReminder
total: *rvTotal
create:
meta:
short: Reminder create
labels:
<<: *labels
create: "step"
params:
resource:
meta: { label: Resource }
types:
- { wf: String}
assignedTo:
meta: { label: Assigned to }
types:
- { wf: ID }
assignedBy:
meta: { label: Assigned by }
types:
- { wf: ID }
remindAt:
meta: { label: Remind at }
types:
- { wf: DateTime}
required: true
payload:
meta: { label: Payload }
types:
- { wf: Bytes }
results:
reminder: *rvReminder
update:
meta:
short: Reminder update
labels:
<<: *labels
update: "step"
params:
lookup: *lookup
resource:
meta: { label: Resource }
types:
- { wf: String}
assignedTo:
meta: { label: Assigned to }
types:
- { wf: ID }
assignedBy:
meta: { label: Assigned by }
types:
- { wf: ID }
remindAt:
meta: { label: Remind at }
types:
- { wf: DateTime}
payload:
meta: { label: Payload }
types:
- { wf: Bytes }
results:
reminder: *rvReminder
dismiss:
meta:
short: Dismiss reminder
labels:
<<: *labels
dismiss: "step"
params:
lookup: *lookup
snooze:
meta:
short: Snooze reminder
labels:
<<: *labels
snooze: "step"
params:
lookup: *lookup
remindAt:
meta: { label: Remind at }
types:
- { wf: DateTime }
required: true
delete:
meta:
short: Reminder delete
labels:
<<: *labels
delete: "step"
params:
lookup: *lookup

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,434 @@
package automation
import (
"context"
"fmt"
. "github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/pkg/rbac"
"github.com/cortezaproject/corteza/server/pkg/wfexec"
"github.com/cortezaproject/corteza/server/system/types"
"github.com/spf13/cast"
)
type (
roleService interface {
FindByID(ctx context.Context, roleID uint64) (*types.Role, error)
FindByHandle(ctx context.Context, handle string) (*types.Role, error)
Find(ctx context.Context, filter types.RoleFilter) (set types.RoleSet, f types.RoleFilter, err error)
Create(ctx context.Context, role *types.Role) (*types.Role, error)
Update(ctx context.Context, role *types.Role) (*types.Role, error)
Delete(ctx context.Context, id uint64) error
Archive(ctx context.Context, id uint64) error
Unarchive(ctx context.Context, id uint64) error
Undelete(ctx context.Context, id uint64) error
Membership(ctx context.Context, userID uint64) (types.RoleMemberSet, error)
MemberList(ctx context.Context, roleID uint64) (types.RoleMemberSet, error)
MemberAdd(ctx context.Context, roleID, userID uint64) error
MemberRemove(ctx context.Context, roleID, userID uint64) error
}
rolesHandler struct {
reg rolesHandlerRegistry
rSvc roleService
uSvc userService
}
roleSetIterator struct {
// Item buffer, current item pointer, and total items traversed
ptr uint
buffer types.RoleSet
total uint
// When filter limit is set, this constraints it
iterLimit uint
useIterLimit bool
// Item loader for additional chunks
filter types.RoleFilter
loader func() error
// Total reported by the first page
resTotal uint
}
roleLookup interface {
GetLookup() (bool, uint64, string, *types.Role)
}
)
func RolesHandler(reg rolesHandlerRegistry, rSvc roleService, uSvc userService) *rolesHandler {
h := &rolesHandler{
reg: reg,
rSvc: rSvc,
uSvc: uSvc,
}
h.register()
return h
}
func (h rolesHandler) lookup(ctx context.Context, args *rolesLookupArgs) (results *rolesLookupResults, err error) {
results = &rolesLookupResults{}
results.Role, err = lookupRole(ctx, h.rSvc, args)
return
}
func (h rolesHandler) searchMembers(ctx context.Context, args *rolesSearchMembersArgs) (results *rolesSearchMembersResults, err error) {
results = &rolesSearchMembersResults{}
rl, err := lookupRole(ctx, h.rSvc, args)
if err != nil {
return
}
if rl == nil {
return nil, fmt.Errorf("role not found")
}
// Get membership info
mm, err := h.rSvc.MemberList(ctx, rl.ID)
if err != nil {
return
}
if len(mm) == 0 {
results.Users = []*types.User{}
return
}
// Get actual users
uu := make([]string, len(mm))
for i, m := range mm {
uu[i] = cast.ToString(rbac.ResourceID(m.Resource))
}
results.Users, _, err = h.uSvc.Find(ctx, types.UserFilter{
UserID: uu,
})
results.Total = uint64(len(results.Users))
return
}
func (h rolesHandler) eachMember(ctx context.Context, args *rolesEachMemberArgs) (out wfexec.IteratorHandler, err error) {
var (
i = &userSetIterator{}
)
rl, err := lookupRole(ctx, h.rSvc, args)
if err != nil {
return
}
if rl == nil {
return nil, fmt.Errorf("role not found")
}
// Get membership info
mm, err := h.rSvc.MemberList(ctx, rl.ID)
if err != nil {
return
}
if len(mm) == 0 {
i.buffer = []*types.User{}
i.filter = types.UserFilter{}
return i, nil
}
// Get actual users
uu := make([]string, len(mm))
for i, m := range mm {
uu[i] = cast.ToString(rbac.ResourceID(m.Resource))
}
i.buffer, i.filter, err = h.uSvc.Find(ctx, types.UserFilter{
UserID: uu,
})
return i, err
}
func (h rolesHandler) addMember(ctx context.Context, args *rolesAddMemberArgs) (err error) {
role, err := lookupRole(ctx, h.rSvc, &rolesLookupArgs{
hasLookup: args.hasRole,
Lookup: args.Role,
lookupID: args.roleID,
lookupHandle: args.roleHandle,
lookupRes: args.roleRes,
})
if err != nil {
return
}
if role == nil {
return fmt.Errorf("role not found")
}
user, err := lookupUser(ctx, h.uSvc, &usersLookupArgs{
hasLookup: args.hasUser,
Lookup: args.User,
lookupID: args.userID,
lookupHandle: args.userHandle,
lookupEmail: args.userEmail,
lookupRes: args.userRes,
})
if err != nil {
return
}
if role == nil {
return fmt.Errorf("user not found")
}
return h.rSvc.MemberAdd(ctx, role.ID, user.ID)
}
func (h rolesHandler) removeMember(ctx context.Context, args *rolesRemoveMemberArgs) (err error) {
role, err := lookupRole(ctx, h.rSvc, &rolesLookupArgs{
hasLookup: args.hasRole,
Lookup: args.Role,
lookupID: args.roleID,
lookupHandle: args.roleHandle,
lookupRes: args.roleRes,
})
if err != nil {
return
}
if role == nil {
return fmt.Errorf("role not found")
}
user, err := lookupUser(ctx, h.uSvc, &usersLookupArgs{
hasLookup: args.hasUser,
Lookup: args.User,
lookupID: args.userID,
lookupHandle: args.userHandle,
lookupEmail: args.userEmail,
lookupRes: args.userRes,
})
if err != nil {
return
}
if role == nil {
return fmt.Errorf("user not found")
}
return h.rSvc.MemberRemove(ctx, role.ID, user.ID)
}
func (h rolesHandler) search(ctx context.Context, args *rolesSearchArgs) (results *rolesSearchResults, err error) {
results = &rolesSearchResults{}
var (
f = types.RoleFilter{
Query: args.Query,
MemberID: args.MemberID,
Name: args.Name,
Handle: args.Handle,
Labels: args.Labels,
Deleted: filter.State(args.Deleted),
Archived: filter.State(args.Archived),
}
)
if args.hasSort {
if err = f.Sort.Set(args.Sort); err != nil {
return
}
}
if args.hasPageCursor && args.PageCursor != "" {
f.PageCursor = &filter.PagingCursor{}
if err = f.PageCursor.UnmarshalJSON([]byte(args.PageCursor)); err != nil {
return
}
}
if args.hasLabels {
f.Labels = args.Labels
}
if args.hasLimit {
f.Limit = uint(args.Limit)
}
// Total cannot be fetched together with a page cursor
f.IncTotal = args.IncTotal && f.PageCursor == nil
var auxf types.RoleFilter
results.Roles, auxf, err = h.rSvc.Find(ctx, f)
results.Total = uint64(auxf.Total)
return
}
func (h rolesHandler) each(ctx context.Context, args *rolesEachArgs) (out wfexec.IteratorHandler, err error) {
var (
i = &roleSetIterator{}
f = types.RoleFilter{
Query: args.Query,
MemberID: args.MemberID,
Name: args.Name,
Handle: args.Handle,
Labels: args.Labels,
Deleted: filter.State(args.Deleted),
Archived: filter.State(args.Archived),
}
)
if args.hasSort {
if err = f.Sort.Set(args.Sort); err != nil {
return
}
}
if args.hasPageCursor && args.PageCursor != "" {
f.NextPage = &filter.PagingCursor{}
if err = f.NextPage.UnmarshalJSON([]byte(args.PageCursor)); err != nil {
return
}
}
if args.hasLabels {
f.Labels = args.Labels
}
if args.hasLimit {
i.useIterLimit = true
i.iterLimit = uint(args.Limit)
f.Limit = uint(args.Limit)
if args.Limit > uint64(wfexec.MaxIteratorBufferSize) {
f.Limit = wfexec.MaxIteratorBufferSize
}
i.iterLimit = uint(args.Limit)
} else {
f.Limit = wfexec.MaxIteratorBufferSize
}
f.IncTotal = args.IncTotal
i.filter = f
i.loader = func() (err error) {
// Edgecase
if i.filter.PageCursor != nil && i.filter.NextPage == nil {
return
}
i.total += i.ptr
i.ptr = 0
i.filter.PageCursor = i.filter.NextPage
i.filter.NextPage = nil
// Total is fetched with the first page only; a paged query cannot carry it
i.filter.IncTotal = i.filter.IncTotal && i.filter.PageCursor == nil
i.buffer, i.filter, err = h.rSvc.Find(ctx, i.filter)
if i.filter.IncTotal {
i.resTotal = i.filter.Total
}
return
}
// Initial load
return i, i.loader()
}
func (h rolesHandler) create(ctx context.Context, args *rolesCreateArgs) (results *rolesCreateResults, err error) {
results = &rolesCreateResults{}
results.Role, err = h.rSvc.Create(ctx, args.Role)
return
}
func (h rolesHandler) update(ctx context.Context, args *rolesUpdateArgs) (results *rolesUpdateResults, err error) {
results = &rolesUpdateResults{}
results.Role, err = h.rSvc.Update(ctx, args.Role)
return
}
func (h rolesHandler) delete(ctx context.Context, args *rolesDeleteArgs) error {
if id, err := getRoleID(ctx, h.rSvc, args); err != nil {
return err
} else {
return h.rSvc.Delete(ctx, id)
}
}
func (h rolesHandler) recover(ctx context.Context, args *rolesRecoverArgs) error {
if id, err := getRoleID(ctx, h.rSvc, args); err != nil {
return err
} else {
return h.rSvc.Undelete(ctx, id)
}
}
func (h rolesHandler) archive(ctx context.Context, args *rolesArchiveArgs) error {
if id, err := getRoleID(ctx, h.rSvc, args); err != nil {
return err
} else {
return h.rSvc.Archive(ctx, id)
}
}
func (h rolesHandler) unarchive(ctx context.Context, args *rolesUnarchiveArgs) error {
if id, err := getRoleID(ctx, h.rSvc, args); err != nil {
return err
} else {
return h.rSvc.Unarchive(ctx, id)
}
}
func getRoleID(ctx context.Context, svc roleService, args roleLookup) (uint64, error) {
_, ID, _, _ := args.GetLookup()
if ID > 0 {
return ID, nil
}
role, err := lookupRole(ctx, svc, args)
if err != nil {
return 0, err
}
return role.ID, nil
}
func lookupRole(ctx context.Context, svc roleService, args roleLookup) (*types.Role, error) {
_, ID, handle, role := args.GetLookup()
switch {
case role != nil:
return role, nil
case ID > 0:
return svc.FindByID(ctx, ID)
case len(handle) > 0:
return svc.FindByHandle(ctx, handle)
}
return nil, fmt.Errorf("empty lookup params")
}
func (i *roleSetIterator) More(context.Context, *Vars) (bool, error) {
a := wfexec.GenericResourceNextCheck(i.useIterLimit, i.ptr, uint(len(i.buffer)), i.total, i.iterLimit, i.filter.NextPage != nil)
return a, nil
}
func (i *roleSetIterator) Start(context.Context, *Vars) error { i.ptr = 0; return nil }
func (i *roleSetIterator) Next(context.Context, *Vars) (out *Vars, err error) {
if len(i.buffer)-int(i.ptr) <= 0 {
if err = i.loader(); err != nil {
panic(err)
}
}
out = &Vars{}
out.Set("role", Must(NewRole(i.buffer[i.ptr])))
out.Set("index", Must(NewInteger(i.total+i.ptr)))
out.Set("total", Must(NewInteger(i.resTotal)))
i.ptr++
return out, nil
}

View File

@@ -0,0 +1,237 @@
imports:
- github.com/cortezaproject/corteza/server/system/types
- labelTypes github.com/cortezaproject/corteza/server/pkg/label/types
params:
lookup: &lookup
meta: { label: Lookup }
required: true
types:
- { wf: ID, }
- { wf: Handle, }
- { wf: Role, suffix: res }
role: &role
meta: { label: Role }
types:
- { wf: Role }
rvRole: &rvRole
wf: Role
rvUser: &rvUser
wf: User
rvTotal: &rvTotal
wf: UnsignedInteger
rvPageCursor: &rvPageCursor
wf: String
lookupUser: &lookupUser
meta: { label: User }
required: true
types:
- { wf: ID }
- { wf: Handle }
- { wf: String, suffix: email }
- { wf: User, suffix: res }
filterParams: &filterParams
query:
meta: { label: Query }
types:
- { wf: String }
memberID:
meta: { label: Member ID }
types:
- { wf: ID }
handle:
meta: { label: Handle }
types:
- { wf: String }
name:
meta: { label: Name }
types:
- { wf: String }
labels:
meta: { label: Labels }
types:
- { wf: LabelValue }
deleted:
meta: { label: Deleted }
types:
- { wf: UnsignedInteger }
archived:
meta: { label: Archived }
types:
- { wf: UnsignedInteger }
sort:
meta: { label: Sort }
types:
- { wf: String }
limit:
meta: { label: Limit }
types:
- { wf: UnsignedInteger }
incTotal:
meta: { label: Include total }
types:
- { wf: Boolean }
incPageNavigation:
meta: { label: Include page navigation }
types:
- { wf: Boolean }
pageCursor:
meta: { label: Page cursor }
types:
- { wf: String }
labels: &labels
users: "step,workflow"
functions:
lookup:
meta:
short: Role lookup
description: Find specific role by ID or handle
labels:
<<: *labels
params:
lookup: *lookup
results:
role: *rvRole
searchMembers:
meta:
short: Role members search
description: Find members for a specific roleby ID or handle
labels:
<<: *labels
params:
lookup: *lookup
results:
users:
<<: *rvUser
isArray: true
total: *rvTotal
eachMember:
kind: iterator
meta:
short: Iterate over role members
labels:
<<: *labels
params:
lookup: *lookup
results:
user: *rvUser
total: *rvTotal
addMember:
meta:
short: Role membership add
labels:
<<: *labels
params:
role:
<<: *lookup
meta: { label: Role }
user: *lookupUser
removeMember:
meta:
short: Role membership remove
labels:
<<: *labels
params:
role:
<<: *lookup
meta: { label: Role }
user: *lookupUser
search:
meta:
short: Roles search
labels:
<<: *labels
params: *filterParams
results:
roles:
<<: *rvRole
isArray: true
total: *rvTotal
each:
kind: iterator
meta:
short: Roles
params: *filterParams
results:
role: *rvRole
total: *rvTotal
create:
meta:
short: Role creator
labels:
<<: *labels
create: "step"
params:
role:
<<: *role
meta: { label: Role }
required: true
results:
role: *rvRole
update:
meta:
short: Role update
labels:
<<: *labels
update: "step"
params:
role:
<<: *role
meta: { label: Role }
required: true
results:
role: *rvRole
delete:
meta:
short: Role delete
labels:
<<: *labels
delete: "step"
params:
lookup: *lookup
recover:
meta:
short: Role recover
labels:
<<: *labels
recover: "step"
params:
lookup: *lookup
archive:
meta:
short: Role archive
labels:
<<: *labels
archive: "step"
params:
lookup: *lookup
unarchive:
meta:
short: Role unarchive
labels:
<<: *labels
unarchive: "step"
params:
lookup: *lookup

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,341 @@
package automation
import (
"context"
"fmt"
"io"
. "github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/pkg/wfexec"
"github.com/cortezaproject/corteza/server/system/types"
"github.com/spf13/cast"
)
type (
templateService interface {
FindByID(ctx context.Context, ID uint64) (*types.Template, error)
FindByHandle(ct context.Context, handle string) (*types.Template, error)
FindByAny(ctx context.Context, identifier interface{}) (*types.Template, error)
Search(context.Context, types.TemplateFilter) (types.TemplateSet, types.TemplateFilter, error)
Create(ctx context.Context, tpl *types.Template) (*types.Template, error)
Update(ctx context.Context, tpl *types.Template) (*types.Template, error)
DeleteByID(ctx context.Context, ID uint64) error
UndeleteByID(ctx context.Context, ID uint64) error
Render(ctx context.Context, templateID uint64, dstType string, variables map[string]interface{}, options map[string]string, aux types.TemplateRenderAux) (io.ReadSeeker, error)
}
templatesHandler struct {
reg templatesHandlerRegistry
tSvc templateService
}
templateSetIterator struct {
// Item buffer, current item pointer, and total items traversed
ptr uint
buffer types.TemplateSet
total uint
// When filter limit is set, this constraints it
iterLimit uint
useIterLimit bool
// Item loader for additional chunks
filter types.TemplateFilter
loader func() error
// Total reported by the first page
resTotal uint
}
templateLookup interface {
GetLookup() (bool, uint64, string, *types.Template)
}
)
func TemplatesHandler(reg templatesHandlerRegistry, tSvc templateService) *templatesHandler {
h := &templatesHandler{
reg: reg,
tSvc: tSvc,
}
h.register()
return h
}
func (h templatesHandler) lookup(ctx context.Context, args *templatesLookupArgs) (results *templatesLookupResults, err error) {
results = &templatesLookupResults{}
results.Template, err = lookupTemplate(ctx, h.tSvc, args)
return
}
func (h templatesHandler) search(ctx context.Context, args *templatesSearchArgs) (results *templatesSearchResults, err error) {
results = &templatesSearchResults{}
var (
f = types.TemplateFilter{
Handle: args.Handle,
Type: args.Type,
OwnerID: args.OwnerID,
Partial: args.Partial,
Labels: args.Labels,
}
)
if args.hasSort {
if err = f.Sort.Set(args.Sort); err != nil {
return
}
}
if args.hasPageCursor && args.PageCursor != "" {
f.PageCursor = &filter.PagingCursor{}
if err = f.PageCursor.UnmarshalJSON([]byte(args.PageCursor)); err != nil {
return
}
}
if args.hasLabels {
f.Labels = args.Labels
}
if args.hasLimit {
f.Limit = uint(args.Limit)
}
// Total cannot be fetched together with a page cursor
f.IncTotal = args.IncTotal && f.PageCursor == nil
var auxf types.TemplateFilter
results.Templates, auxf, err = h.tSvc.Search(ctx, f)
results.Total = uint64(auxf.Total)
return
}
func (h templatesHandler) each(ctx context.Context, args *templatesEachArgs) (out wfexec.IteratorHandler, err error) {
var (
i = &templateSetIterator{}
f = types.TemplateFilter{
Handle: args.Handle,
Type: args.Type,
OwnerID: args.OwnerID,
Partial: args.Partial,
Labels: args.Labels,
}
)
if args.hasSort {
if err = f.Sort.Set(args.Sort); err != nil {
return
}
}
if args.hasPageCursor && args.PageCursor != "" {
f.NextPage = &filter.PagingCursor{}
if err = f.NextPage.UnmarshalJSON([]byte(args.PageCursor)); err != nil {
return
}
}
if args.hasLabels {
f.Labels = args.Labels
}
if args.hasLimit {
i.useIterLimit = true
i.iterLimit = uint(args.Limit)
f.Limit = uint(args.Limit)
if args.Limit > uint64(wfexec.MaxIteratorBufferSize) {
f.Limit = wfexec.MaxIteratorBufferSize
}
i.iterLimit = uint(args.Limit)
} else {
f.Limit = wfexec.MaxIteratorBufferSize
}
f.IncTotal = args.IncTotal
i.filter = f
i.loader = func() (err error) {
// Edgecase
if i.filter.PageCursor != nil && i.filter.NextPage == nil {
return
}
i.total += i.ptr
i.ptr = 0
i.filter.PageCursor = i.filter.NextPage
i.filter.NextPage = nil
// Total is fetched with the first page only; a paged query cannot carry it
i.filter.IncTotal = i.filter.IncTotal && i.filter.PageCursor == nil
i.buffer, i.filter, err = h.tSvc.Search(ctx, i.filter)
if i.filter.IncTotal {
i.resTotal = i.filter.Total
}
return
}
// Initial load
return i, i.loader()
}
func (h templatesHandler) create(ctx context.Context, args *templatesCreateArgs) (results *templatesCreateResults, err error) {
results = &templatesCreateResults{}
results.Template, err = h.tSvc.Create(ctx, args.Template)
return
}
func (h templatesHandler) update(ctx context.Context, args *templatesUpdateArgs) (results *templatesUpdateResults, err error) {
results = &templatesUpdateResults{}
results.Template, err = h.tSvc.Update(ctx, args.Template)
return
}
func (h templatesHandler) delete(ctx context.Context, args *templatesDeleteArgs) error {
if id, err := getTemplateID(ctx, h.tSvc, args); err != nil {
return err
} else {
return h.tSvc.DeleteByID(ctx, id)
}
}
func (h templatesHandler) recover(ctx context.Context, args *templatesRecoverArgs) error {
if id, err := getTemplateID(ctx, h.tSvc, args); err != nil {
return err
} else {
return h.tSvc.UndeleteByID(ctx, id)
}
}
func (h templatesHandler) render(ctx context.Context, args *templatesRenderArgs) (*templatesRenderResults, error) {
var err error
opts := make(map[string]string)
if args.hasOptions {
opts, err = cast.ToStringMapStringE(args.Options)
if err != nil {
return nil, err
}
}
tplID, err := getTemplateID(ctx, h.tSvc, args)
if err != nil {
return nil, err
}
var vars map[string]interface{}
if args.hasVariables {
vars = args.Variables.Dict()
}
aux := types.TemplateRenderAux{}
aux.HeaderTemplateID, err = resolveTemplateID(ctx, h.tSvc, args.hasHeaderTemplate, args.headerTemplateID, args.headerTemplateHandle, args.headerTemplateRes)
if err != nil {
return nil, err
}
aux.FooterTemplateID, err = resolveTemplateID(ctx, h.tSvc, args.hasFooterTemplate, args.footerTemplateID, args.footerTemplateHandle, args.footerTemplateRes)
if err != nil {
return nil, err
}
doc, err := h.tSvc.Render(ctx, tplID, args.DocumentType, vars, opts, aux)
if err != nil {
return nil, err
}
rr := &templatesRenderResults{
Document: &renderedDocument{
Document: doc,
Name: args.DocumentName,
Type: args.DocumentType,
},
}
return rr, nil
}
func (i *templateSetIterator) More(context.Context, *Vars) (bool, error) {
return wfexec.GenericResourceNextCheck(i.useIterLimit, i.ptr, uint(len(i.buffer)), i.total, i.iterLimit, i.filter.NextPage != nil), nil
}
func (i *templateSetIterator) Start(context.Context, *Vars) error { i.ptr = 0; return nil }
func (i *templateSetIterator) Next(context.Context, *Vars) (out *Vars, err error) {
if len(i.buffer)-int(i.ptr) <= 0 {
if err = i.loader(); err != nil {
panic(err)
}
}
out = &Vars{}
out.Set("template", Must(NewTemplate(i.buffer[i.ptr])))
out.Set("index", Must(NewInteger(i.total+i.ptr)))
out.Set("total", Must(NewInteger(i.resTotal)))
i.ptr++
return out, nil
}
func lookupTemplate(ctx context.Context, svc templateService, args templateLookup) (*types.Template, error) {
_, ID, handle, template := args.GetLookup()
switch {
case template != nil:
return template, nil
case ID > 0:
return svc.FindByID(ctx, ID)
case len(handle) > 0:
return svc.FindByHandle(ctx, handle)
}
return nil, fmt.Errorf("empty lookup params")
}
func getTemplateID(ctx context.Context, svc templateService, args templateLookup) (uint64, error) {
_, ID, _, _ := args.GetLookup()
if ID > 0 {
return ID, nil
}
tpl, err := lookupTemplate(ctx, svc, args)
if err != nil {
return 0, err
}
return tpl.ID, nil
}
// resolveTemplateID resolves ID, handle or template argument into a template ID
//
// Unset argument resolves into zero ID
func resolveTemplateID(ctx context.Context, svc templateService, has bool, ID uint64, handle string, tpl *types.Template) (uint64, error) {
switch {
case !has:
return 0, nil
case tpl != nil:
return tpl.ID, nil
case ID > 0:
return ID, nil
case len(handle) > 0:
tpl, err := svc.FindByHandle(ctx, handle)
if err != nil {
return 0, err
}
return tpl.ID, nil
}
return 0, nil
}

View File

@@ -0,0 +1,207 @@
imports:
- github.com/cortezaproject/corteza/server/system/types
- labelTypes github.com/cortezaproject/corteza/server/pkg/label/types
snippets:
lookup: &lookup
meta: { label: Lookup }
required: true
types:
- { wf: ID }
- { wf: Handle }
- { wf: Template, suffix: res }
# same as lookup but labeled for the render function where it is the body of the document
bodyLookup: &bodyLookup
required: true
meta:
label: Body template
description: Template rendered as the body of the document
types:
- { wf: ID }
- { wf: Handle }
- { wf: Template, suffix: res }
auxTemplate: &auxTemplate
types:
- { wf: ID }
- { wf: Handle }
- { wf: Template, suffix: res }
template: &template
types:
- { wf: Template }
rvTemplate: &rvTemplate
wf: Template
rvRenderedDocument: &rvRenderedDocument
wf: RenderedDocument
rvTotal: &rvTotal
wf: UnsignedInteger
rvPageCursor: &rvPageCursor
wf: String
filterParams: &filterParams
handle:
meta: { label: Handle }
types:
- { wf: String }
type:
meta: { label: Type }
types:
- { wf: String }
ownerID:
meta: { label: Owner ID }
types:
- { wf: ID }
partial:
meta: { label: Partial }
types:
- { wf: Boolean }
labels:
meta: { label: Labels }
types:
- { wf: LabelValue }
sort:
meta: { label: Sort }
types:
- { wf: String }
limit:
meta: { label: Limit }
types:
- { wf: UnsignedInteger }
incTotal:
meta: { label: Include total }
types:
- { wf: Boolean }
incPageNavigation:
meta: { label: Include page navigation }
types:
- { wf: Boolean }
pageCursor:
meta: { label: Page cursor }
types:
- { wf: String }
labels: &labels
templates: "step,workflow"
functions:
lookup:
meta:
short: Template lookup
description: Find specific template by ID or handle
params:
lookup: *lookup
labels:
<<: *labels
results:
template: *rvTemplate
search:
meta:
short: Templates search
params: *filterParams
labels:
<<: *labels
results:
templates:
<<: *rvTemplate
isArray: true
total: *rvTotal
each:
kind: iterator
meta:
short: Templates
params: *filterParams
labels:
<<: *labels
results:
template: *rvTemplate
total: *rvTotal
create:
meta:
short: Template create
labels:
<<: *labels
params:
template:
<<: *template
meta: { label: Template }
required: true
results:
template: *rvTemplate
update:
meta:
short: Template update
labels:
<<: *labels
params:
template:
<<: *template
meta: { label: Template }
required: true
results:
template: *rvTemplate
delete:
labels:
<<: *labels
delete: "step"
meta:
short: Template delete
params:
lookup: *lookup
recover:
meta:
short: Template recover
labels:
<<: *labels
recover: "step"
params:
lookup: *lookup
render:
meta:
short: Template render
labels:
<<: *labels
render: "step"
params:
lookup: *bodyLookup
headerTemplate:
<<: *auxTemplate
meta:
label: Header template
description: Overrides the header template set on the body template; used by drivers that support it
footerTemplate:
<<: *auxTemplate
meta:
label: Footer template
description: Overrides the footer template set on the body template; used by drivers that support it
documentName:
meta: { label: Document name }
types:
- { wf: String }
documentType:
meta: { label: Document type }
types:
- { wf: String }
variables:
meta: { label: Variables }
types:
- { wf: Vars, go: '*expr.Vars' }
options:
meta: { label: Options }
types:
- { wf: RenderOptions }
results:
document: *rvRenderedDocument

View File

@@ -0,0 +1,141 @@
package automation
import (
"context"
"fmt"
"io"
"testing"
"github.com/cortezaproject/corteza/server/system/types"
"github.com/stretchr/testify/require"
)
type (
templateServiceMocked struct {
templateService
byHandle func(context.Context, string) (*types.Template, error)
rendered func(uint64, types.TemplateRenderAux)
}
)
func (svc templateServiceMocked) FindByHandle(ctx context.Context, h string) (*types.Template, error) {
return svc.byHandle(ctx, h)
}
func (svc templateServiceMocked) Render(_ context.Context, templateID uint64, _ string, _ map[string]interface{}, _ map[string]string, aux types.TemplateRenderAux) (io.ReadSeeker, error) {
svc.rendered(templateID, aux)
return nil, nil
}
func TestResolveTemplateID(t *testing.T) {
var (
ctx = context.Background()
svc = templateServiceMocked{
byHandle: func(_ context.Context, h string) (*types.Template, error) {
if h != "header" {
return nil, fmt.Errorf("unknown template %q", h)
}
return &types.Template{ID: 200}, nil
},
}
)
t.Run("unset argument", func(t *testing.T) {
req := require.New(t)
ID, err := resolveTemplateID(ctx, svc, false, 0, "", nil)
req.NoError(err)
req.Zero(ID)
})
t.Run("by ID", func(t *testing.T) {
req := require.New(t)
ID, err := resolveTemplateID(ctx, svc, true, 100, "", nil)
req.NoError(err)
req.Equal(uint64(100), ID)
})
t.Run("by handle", func(t *testing.T) {
req := require.New(t)
ID, err := resolveTemplateID(ctx, svc, true, 0, "header", nil)
req.NoError(err)
req.Equal(uint64(200), ID)
})
t.Run("by template", func(t *testing.T) {
req := require.New(t)
ID, err := resolveTemplateID(ctx, svc, true, 0, "", &types.Template{ID: 300})
req.NoError(err)
req.Equal(uint64(300), ID)
})
t.Run("unknown handle", func(t *testing.T) {
req := require.New(t)
_, err := resolveTemplateID(ctx, svc, true, 0, "missing", nil)
req.Error(err)
})
}
func TestRenderPassesAuxTemplates(t *testing.T) {
var (
req = require.New(t)
renderedID uint64
renderedAux types.TemplateRenderAux
svc = templateServiceMocked{
byHandle: func(_ context.Context, h string) (*types.Template, error) {
return &types.Template{ID: 200}, nil
},
rendered: func(ID uint64, aux types.TemplateRenderAux) {
renderedID, renderedAux = ID, aux
},
}
h = templatesHandler{tSvc: svc}
)
args := &templatesRenderArgs{
hasLookup: true,
lookupID: 100,
DocumentType: "application/pdf",
hasHeaderTemplate: true,
headerTemplateID: 300,
hasFooterTemplate: true,
footerTemplateHandle: "footer",
}
_, err := h.render(context.Background(), args)
req.NoError(err)
req.Equal(uint64(100), renderedID)
req.Equal(uint64(300), renderedAux.HeaderTemplateID)
req.Equal(uint64(200), renderedAux.FooterTemplateID)
}
func TestRenderWithoutAuxTemplates(t *testing.T) {
var (
req = require.New(t)
renderedAux types.TemplateRenderAux
svc = templateServiceMocked{
rendered: func(_ uint64, aux types.TemplateRenderAux) {
renderedAux = aux
},
}
h = templatesHandler{tSvc: svc}
)
_, err := h.render(context.Background(), &templatesRenderArgs{hasLookup: true, lookupID: 100})
req.NoError(err)
// nothing set, service falls back to the template's own meta
req.Zero(renderedAux.HeaderTemplateID)
req.Zero(renderedAux.FooterTemplateID)
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,383 @@
package automation
import (
"context"
"fmt"
"strconv"
. "github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/pkg/wfexec"
"github.com/cortezaproject/corteza/server/system/types"
)
type (
userService interface {
FindByID(ctx context.Context, userID uint64) (*types.User, error)
FindByHandle(ctx context.Context, handle string) (*types.User, error)
FindByEmail(ctx context.Context, email string) (*types.User, error)
Find(ctx context.Context, filter types.UserFilter) (set types.UserSet, f types.UserFilter, err error)
Create(ctx context.Context, user *types.User) (*types.User, error)
Update(ctx context.Context, user *types.User) (*types.User, error)
Delete(ctx context.Context, id uint64) error
Suspend(ctx context.Context, id uint64) error
Unsuspend(ctx context.Context, id uint64) error
Undelete(ctx context.Context, id uint64) error
}
usersHandler struct {
reg usersHandlerRegistry
uSvc userService
rSvc roleService
}
userSetIterator struct {
// Item buffer, current item pointer, and total items traversed
ptr uint
buffer types.UserSet
total uint
// When filter limit is set, this constraints it
iterLimit uint
useIterLimit bool
// Item loader for additional chunks
filter types.UserFilter
loader func() error
// Total reported by the first page
resTotal uint
}
userLookup interface {
GetLookup() (bool, uint64, string, string, *types.User)
}
)
func UsersHandler(reg usersHandlerRegistry, uSvc userService, rSvc roleService) *usersHandler {
h := &usersHandler{
reg: reg,
uSvc: uSvc,
rSvc: rSvc,
}
h.register()
return h
}
func (h usersHandler) lookup(ctx context.Context, args *usersLookupArgs) (results *usersLookupResults, err error) {
results = &usersLookupResults{}
results.User, err = lookupUser(ctx, h.uSvc, args)
return
}
func (h usersHandler) searchMembership(ctx context.Context, args *usersSearchMembershipArgs) (results *usersSearchMembershipResults, err error) {
results = &usersSearchMembershipResults{}
u, err := lookupUser(ctx, h.uSvc, args)
if err != nil {
return
}
if u == nil {
return nil, fmt.Errorf("user not found")
}
// Get the roles
mm, err := h.rSvc.Membership(ctx, u.ID)
if err != nil {
return
}
if len(mm) == 0 {
results.Roles = []*types.Role{}
return
}
rr := make([]string, len(mm))
for i, r := range mm {
rr[i] = strconv.FormatUint(r.RoleID, 10)
}
results.Roles, _, err = h.rSvc.Find(ctx, types.RoleFilter{
RoleID: rr,
})
results.Total = uint64(len(results.Roles))
return
}
func (h usersHandler) checkMembership(ctx context.Context, args *usersCheckMembershipArgs) (results *usersCheckMembershipResults, err error) {
results = &usersCheckMembershipResults{}
// Resolve user
u, err := lookupUser(ctx, h.uSvc, usersLookupArgs{
hasLookup: args.hasUser,
Lookup: args.User,
lookupID: args.userID,
lookupHandle: args.userHandle,
lookupEmail: args.userEmail,
lookupRes: args.userRes,
})
if err != nil {
return
}
if u == nil {
return nil, fmt.Errorf("user not found")
}
// Get user membershio
mm, err := h.rSvc.Membership(ctx, u.ID)
if err != nil {
return
}
if len(mm) == 0 {
return &usersCheckMembershipResults{
Member: false,
}, nil
}
// Resolve role
r, err := lookupRole(ctx, h.rSvc, rolesLookupArgs{
hasLookup: args.hasRole,
Lookup: args.Role,
lookupID: args.roleID,
lookupHandle: args.roleHandle,
lookupRes: args.roleRes,
})
if err != nil {
return
}
if r == nil {
return nil, fmt.Errorf("role not found")
}
// Check if there
for _, m := range mm {
if m.RoleID == r.ID {
return &usersCheckMembershipResults{
Member: true,
}, nil
}
}
return &usersCheckMembershipResults{
Member: false,
}, nil
}
func (h usersHandler) search(ctx context.Context, args *usersSearchArgs) (results *usersSearchResults, err error) {
results = &usersSearchResults{}
var (
f = types.UserFilter{
Query: args.Query,
Email: args.Email,
Handle: args.Handle,
Labels: args.Labels,
Deleted: filter.State(args.Deleted),
Suspended: filter.State(args.Suspended),
}
)
if args.hasSort {
if err = f.Sort.Set(args.Sort); err != nil {
return
}
}
if args.hasPageCursor && args.PageCursor != "" {
f.PageCursor = &filter.PagingCursor{}
if err = f.PageCursor.UnmarshalJSON([]byte(args.PageCursor)); err != nil {
return
}
}
if args.hasLabels {
f.Labels = args.Labels
}
if args.hasLimit {
f.Limit = uint(args.Limit)
}
// Total cannot be fetched together with a page cursor
f.IncTotal = args.IncTotal && f.PageCursor == nil
var auxf types.UserFilter
results.Users, auxf, err = h.uSvc.Find(ctx, f)
results.Total = uint64(auxf.Total)
return
}
func (h usersHandler) each(ctx context.Context, args *usersEachArgs) (out wfexec.IteratorHandler, err error) {
var (
i = &userSetIterator{}
f = types.UserFilter{
Query: args.Query,
Email: args.Email,
Handle: args.Handle,
Labels: args.Labels,
Deleted: filter.State(args.Deleted),
Suspended: filter.State(args.Suspended),
}
)
if args.hasSort {
if err = f.Sort.Set(args.Sort); err != nil {
return
}
}
if args.hasPageCursor && args.PageCursor != "" {
f.NextPage = &filter.PagingCursor{}
if err = f.NextPage.UnmarshalJSON([]byte(args.PageCursor)); err != nil {
return
}
}
f.IncTotal = args.IncTotal
if args.hasLabels {
f.Labels = args.Labels
}
if args.hasLimit {
i.useIterLimit = true
i.iterLimit = uint(args.Limit)
f.Limit = uint(args.Limit)
if args.Limit > uint64(wfexec.MaxIteratorBufferSize) {
f.Limit = wfexec.MaxIteratorBufferSize
}
i.iterLimit = uint(args.Limit)
} else {
f.Limit = wfexec.MaxIteratorBufferSize
}
i.filter = f
i.loader = func() (err error) {
// Edgecase
if i.filter.PageCursor != nil && i.filter.NextPage == nil {
return
}
i.total += i.ptr
i.ptr = 0
i.filter.PageCursor = i.filter.NextPage
i.filter.NextPage = nil
// Total is fetched with the first page only; a paged query cannot carry it
i.filter.IncTotal = i.filter.IncTotal && i.filter.PageCursor == nil
i.buffer, i.filter, err = h.uSvc.Find(ctx, i.filter)
if i.filter.IncTotal {
i.resTotal = i.filter.Total
}
return
}
// Initial load
return i, i.loader()
}
func (h usersHandler) create(ctx context.Context, args *usersCreateArgs) (results *usersCreateResults, err error) {
results = &usersCreateResults{}
results.User, err = h.uSvc.Create(ctx, args.User)
return
}
func (h usersHandler) update(ctx context.Context, args *usersUpdateArgs) (results *usersUpdateResults, err error) {
results = &usersUpdateResults{}
results.User, err = h.uSvc.Update(ctx, args.User)
return
}
func (h usersHandler) delete(ctx context.Context, args *usersDeleteArgs) error {
if id, err := getUserID(ctx, h.uSvc, args); err != nil {
return err
} else {
return h.uSvc.Delete(ctx, id)
}
}
func (h usersHandler) recover(ctx context.Context, args *usersRecoverArgs) error {
if id, err := getUserID(ctx, h.uSvc, args); err != nil {
return err
} else {
return h.uSvc.Undelete(ctx, id)
}
}
func (h usersHandler) suspend(ctx context.Context, args *usersSuspendArgs) error {
if id, err := getUserID(ctx, h.uSvc, args); err != nil {
return err
} else {
return h.uSvc.Suspend(ctx, id)
}
}
func (h usersHandler) unsuspend(ctx context.Context, args *usersUnsuspendArgs) error {
if id, err := getUserID(ctx, h.uSvc, args); err != nil {
return err
} else {
return h.uSvc.Unsuspend(ctx, id)
}
}
func getUserID(ctx context.Context, svc userService, args userLookup) (uint64, error) {
_, ID, _, _, _ := args.GetLookup()
if ID > 0 {
return ID, nil
}
user, err := lookupUser(ctx, svc, args)
if err != nil {
return 0, err
}
return user.ID, nil
}
func lookupUser(ctx context.Context, svc userService, args userLookup) (*types.User, error) {
_, ID, handle, email, user := args.GetLookup()
switch {
case user != nil:
return user, nil
case ID > 0:
return svc.FindByID(ctx, ID)
case len(email) > 0:
return svc.FindByEmail(ctx, email)
case len(handle) > 0:
return svc.FindByHandle(ctx, handle)
}
return nil, fmt.Errorf("empty lookup params")
}
func (i *userSetIterator) More(context.Context, *Vars) (bool, error) {
return wfexec.GenericResourceNextCheck(i.useIterLimit, i.ptr, uint(len(i.buffer)), i.total, i.iterLimit, i.filter.NextPage != nil), nil
}
func (i *userSetIterator) Start(context.Context, *Vars) error { i.ptr = 0; return nil }
func (i *userSetIterator) Next(context.Context, *Vars) (out *Vars, err error) {
if len(i.buffer)-int(i.ptr) <= 0 {
if err = i.loader(); err != nil {
panic(err)
}
}
out = &Vars{}
out.Set("user", Must(NewUser(i.buffer[i.ptr])))
out.Set("index", Must(NewInteger(i.total+i.ptr)))
out.Set("total", Must(NewInteger(i.resTotal)))
i.ptr++
return out, nil
}

View File

@@ -0,0 +1,215 @@
imports:
- github.com/cortezaproject/corteza/server/system/types
- labelTypes github.com/cortezaproject/corteza/server/pkg/label/types
snippets:
lookup: &lookup
meta: { label: Lookup }
required: true
types:
- { wf: ID }
- { wf: Handle }
- { wf: String, suffix: email }
- { wf: User, suffix: res }
lookupRole: &lookupRole
meta: { label: Role }
required: true
types:
- { wf: ID, }
- { wf: Handle, }
- { wf: Role, suffix: res }
user: &user
types:
- { wf: User }
rvUser: &rvUser
wf: User
rvRole: &rvRole
wf: Role
rvTotal: &rvTotal
wf: UnsignedInteger
rvInclusion: &rvInclusion
wf: Boolean
rvPageCursor: &rvPageCursor
wf: String
filterParams: &filterParams
query:
meta: { label: Query }
types:
- { wf: String }
email:
meta: { label: Email }
types:
- { wf: String }
handle:
meta: { label: Handle }
types:
- { wf: String }
labels:
meta: { label: Labels }
types:
- { wf: LabelValue }
deleted:
meta: { label: Deleted }
types:
- { wf: UnsignedInteger }
suspended:
meta: { label: Suspended }
types:
- { wf: UnsignedInteger }
sort:
meta: { label: Sort }
types:
- { wf: String }
limit:
meta: { label: Limit }
types:
- { wf: UnsignedInteger }
incTotal:
meta: { label: Include total }
types:
- { wf: Boolean }
incPageNavigation:
meta: { label: Include page navigation }
types:
- { wf: Boolean }
pageCursor:
meta: { label: Page cursor }
types:
- { wf: String }
labels: &labels
users: "step,workflow"
functions:
lookup:
meta:
short: User lookup
description: Find specific user by ID, handle or string
params:
lookup: *lookup
labels:
<<: *labels
results:
user: *rvUser
searchMembership:
meta:
short: User role search
description: Search user role membership by ID, handle or string
params:
lookup: *lookup
labels:
<<: *labels
results:
roles:
<<: *rvRole
isArray: true
total: *rvTotal
checkMembership:
meta:
short: User membership check
description: Find user role membership by ID, handle or string
params:
user:
<<: *lookup
meta: { label: User }
role: *lookupRole
labels:
<<: *labels
results:
member: *rvInclusion
search:
meta:
short: User search
params: *filterParams
labels:
<<: *labels
results:
users:
<<: *rvUser
isArray: true
total: *rvTotal
each:
kind: iterator
meta:
short: Users
params: *filterParams
labels:
<<: *labels
results:
user: *rvUser
total: *rvTotal
create:
meta:
short: User create
labels:
<<: *labels
params:
user:
<<: *user
meta: { label: User }
required: true
results:
user: *rvUser
update:
meta:
short: User update
labels:
<<: *labels
params:
user:
<<: *user
meta: { label: User }
required: true
results:
user: *rvUser
delete:
labels:
<<: *labels
delete: "step"
meta:
short: User delete
params:
lookup: *lookup
recover:
labels:
<<: *labels
recover: "step"
meta:
short: User recover
params:
lookup: *lookup
suspend:
labels:
<<: *labels
suspend: "step"
meta:
short: User suspend
params:
lookup: *lookup
unsuspend:
labels:
<<: *labels
unsuspend: "step"
meta:
short: User unsuspend
params:
lookup: *lookup

View File

@@ -0,0 +1,113 @@
package automation
// 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:
// system/automation/valuestore_handler.yaml
import (
"context"
atypes "github.com/cortezaproject/corteza/server/automation/types"
"github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/cortezaproject/corteza/server/pkg/wfexec"
)
var _ wfexec.ExecResponse
type (
valuestoreHandlerRegistry interface {
AddFunctions(ff ...*atypes.Function)
Type(ref string) expr.Type
}
)
func (h valuestoreHandler) register() {
h.reg.AddFunctions(
h.Env(),
)
}
type (
valuestoreEnvArgs struct {
hasKey bool
Key string
}
valuestoreEnvResults struct {
Value interface{}
}
)
// Env function Get ENV variable
//
// expects implementation of env function:
//
// func (h valuestoreHandler) env(ctx context.Context, args *valuestoreEnvArgs) (results *valuestoreEnvResults, err error) {
// return
// }
func (h valuestoreHandler) Env() *atypes.Function {
return &atypes.Function{
Ref: "valuestoreEnv",
Kind: "function",
Labels: map[string]string{"templates": "step,workflow"},
Meta: &atypes.FunctionMeta{
Short: "Get ENV variable",
Description: "Get ENV variable for the specified key. If the key doesn't correspond to any value, nil is returned",
},
Parameters: []*atypes.Param{
{
Name: "key",
Types: []string{"String"}, Required: true,
Meta: &atypes.ParamMeta{
Label: "Key",
},
},
},
Results: []*atypes.Param{
{
Name: "value",
Types: []string{"Any"},
},
},
Handler: func(ctx context.Context, in *expr.Vars) (out *expr.Vars, err error) {
var (
args = &valuestoreEnvArgs{
hasKey: in.Has("key"),
}
)
if err = in.Decode(args); err != nil {
return
}
var results *valuestoreEnvResults
if results, err = h.env(ctx, args); err != nil {
return
}
out = &expr.Vars{}
{
// converting results.Value (interface{}) to Any
var (
tval expr.TypedValue
)
if tval, err = h.reg.Type("Any").Cast(results.Value); err != nil {
return
} else if err = expr.Assign(out, "value", tval); err != nil {
return
}
}
return
},
}
}

View File

@@ -0,0 +1,33 @@
package automation
import (
"context"
)
type (
envGetter interface {
Env(k string) (v any)
}
valuestoreHandler struct {
reg valuestoreHandlerRegistry
getter envGetter
}
)
func ValuestoreHandler(reg valuestoreHandlerRegistry, getter envGetter) *valuestoreHandler {
h := &valuestoreHandler{
reg: reg,
getter: getter,
}
h.register()
return h
}
func (h valuestoreHandler) env(ctx context.Context, args *valuestoreEnvArgs) (results *valuestoreEnvResults, err error) {
results = &valuestoreEnvResults{
Value: h.getter.Env(args.Key),
}
return
}

View File

@@ -0,0 +1,21 @@
snippets:
labels: &labels
templates: "step,workflow"
functions:
env:
meta:
short: Get ENV variable
description: Get ENV variable for the specified key. If the key doesn't correspond to any value, nil is returned
params:
key:
meta: { label: Key }
required: true
types:
- { wf: String }
labels:
<<: *labels
results:
value:
wf: Any

View File

@@ -0,0 +1,27 @@
package commands
import (
"context"
"github.com/cortezaproject/corteza/server/pkg/cli"
"github.com/spf13/cobra"
)
type (
serviceInitializer interface {
InitServices(ctx context.Context) error
Activate(ctx context.Context) error
}
)
func commandPreRunInitService(app serviceInitializer) func(*cobra.Command, []string) error {
return func(_ *cobra.Command, _ []string) error {
return app.InitServices(cli.Context())
}
}
func commandPreRunInitActivate(app serviceInitializer) func(*cobra.Command, []string) error {
return func(_ *cobra.Command, _ []string) error {
return app.Activate(cli.Context())
}
}

View File

@@ -0,0 +1,237 @@
package commands
import (
"context"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/auth"
"github.com/cortezaproject/corteza/server/pkg/dal"
"github.com/cortezaproject/corteza/server/pkg/envoyx"
"github.com/cortezaproject/corteza/server/pkg/rbac"
systemTypes "github.com/cortezaproject/corteza/server/system/types"
"github.com/spf13/cobra"
"github.com/cortezaproject/corteza/server/pkg/cli"
"github.com/cortezaproject/corteza/server/store"
)
type (
storeInitFnc func(ctx context.Context) (store.Storer, error)
dalInitFnc func(ctx context.Context) (dal.FullService, error)
envoyInitFnc func(ctx context.Context) (*envoyx.Service, error)
)
var (
omitModules bool
omitPages bool
omitCharts bool
inclLocale bool
inclRbac bool
)
func Export(ctx context.Context, storeInit storeInitFnc, dalInit dalInitFnc, envoyInit envoyInitFnc) *cobra.Command {
cmd := &cobra.Command{
Use: "export",
Short: "Export",
Long: `Export data`,
}
exportNsCommand := &cobra.Command{
Use: "compose-namespace",
Short: "Export compose namespace",
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
ctx = auth.SetIdentityToContext(ctx, auth.ServiceUser())
// Init all of the sub services
s, err := storeInit(ctx)
cli.HandleError(err)
dalSvc, err := dalInit(ctx)
cli.HandleError(err)
envoySvc, err := envoyInit(ctx)
cli.HandleError(err)
scp := envoyx.Scope{
ResourceType: types.NamespaceResourceType,
Identifiers: envoyx.MakeIdentifiers(args[0]),
}
f := map[string]envoyx.ResourceFilter{
types.NamespaceResourceType: {
Identifiers: envoyx.MakeIdentifiers(args[0]),
Scope: scp,
},
}
if !omitModules {
f[types.ModuleResourceType] = envoyx.ResourceFilter{
Scope: scp,
Refs: map[string]envoyx.Ref{
"NamespaceID": {
ResourceType: types.NamespaceResourceType,
Identifiers: envoyx.MakeIdentifiers(args[0]),
Scope: scp,
},
},
}
}
if !omitPages {
f[types.PageResourceType] = envoyx.ResourceFilter{
Scope: scp,
Refs: map[string]envoyx.Ref{
"NamespaceID": {
ResourceType: types.NamespaceResourceType,
Identifiers: envoyx.MakeIdentifiers(args[0]),
Scope: scp,
},
},
}
}
if !omitCharts {
f[types.ChartResourceType] = envoyx.ResourceFilter{
Scope: scp,
Refs: map[string]envoyx.Ref{
"NamespaceID": {
ResourceType: types.NamespaceResourceType,
Identifiers: envoyx.MakeIdentifiers(args[0]),
Scope: scp,
},
},
}
}
nodes, _, err := envoySvc.Decode(ctx, envoyx.DecodeParams{
Type: envoyx.DecodeTypeStore,
Params: map[string]any{
"storer": s,
"dal": dalSvc,
},
Filter: f,
})
cli.HandleError(err)
if inclRbac {
var aux envoyx.NodeSet
aux, err = encodeRbacRules(ctx, s, nodes)
cli.HandleError(err)
nodes = append(nodes, aux...)
}
if inclLocale {
var aux envoyx.NodeSet
aux, err = encodeLocale(ctx, s, nodes)
cli.HandleError(err)
nodes = append(nodes, aux...)
}
rf := map[string]envoyx.ResourceFilter{
systemTypes.RoleResourceType: {},
}
refNodes, _, err := envoySvc.Decode(ctx, envoyx.DecodeParams{
Type: envoyx.DecodeTypeStore,
Params: map[string]any{
"storer": s,
"dal": dalSvc,
},
Filter: rf,
})
cli.HandleError(err)
for _, n := range refNodes {
n.Placeholder = true
}
nodes = append(nodes, refNodes...)
gg, err := envoySvc.Bake(ctx, envoyx.EncodeParams{
Type: envoyx.EncodeTypeStore,
Params: map[string]any{
"storer": s,
"dal": dalSvc,
},
}, nil, nodes...)
cli.HandleError(err)
err = envoySvc.Encode(ctx, envoyx.EncodeParams{
Type: envoyx.EncodeTypeIo,
Params: map[string]any{
"writer": cmd.OutOrStdout(),
},
}, gg)
cli.HandleError(err)
},
}
exportNsCommand.Flags().BoolVar(
&omitModules,
"omit-modules",
false,
"Omit modules from output",
)
exportNsCommand.Flags().BoolVar(
&omitPages,
"omit-pages",
false,
"Omit pages from output",
)
exportNsCommand.Flags().BoolVar(
&omitCharts,
"omit-charts",
false,
"Omit charts from output",
)
exportNsCommand.Flags().BoolVar(
&inclLocale,
"include-locale",
false,
"Include resource translations",
)
exportNsCommand.Flags().BoolVar(
&inclRbac,
"include-rbac",
false,
"Include RBAC rules",
)
cmd.AddCommand(
exportNsCommand,
)
return cmd
}
func encodeRbacRules(ctx context.Context, s store.Storer, nn envoyx.NodeSet) (rules envoyx.NodeSet, err error) {
rr, _, err := store.SearchRbacRules(ctx, s, rbac.RuleFilter{})
if err != nil {
return
}
rules, err = envoyx.RBACRulesForNodes(rr, nn...)
if err != nil {
return
}
return
}
func encodeLocale(ctx context.Context, s store.Storer, nn envoyx.NodeSet) (translations envoyx.NodeSet, err error) {
tt, _, err := store.SearchResourceTranslations(ctx, s, systemTypes.ResourceTranslationFilter{})
if err != nil {
return
}
translations, err = envoyx.ResourceTranslationsForNodes(tt, nn...)
if err != nil {
return
}
return
}

View File

@@ -0,0 +1,133 @@
package commands
import (
"context"
"errors"
"os"
"github.com/spf13/cobra"
"github.com/cortezaproject/corteza/server/pkg/cli"
"github.com/cortezaproject/corteza/server/pkg/envoyx"
)
func Import(ctx context.Context, storeInit storeInitFnc, dalInit dalInitFnc, envoyInit envoyInitFnc) *cobra.Command {
var (
replaceOnConflict bool
skipOnConflict bool
panicOnConflict bool
)
cmd := &cobra.Command{
Use: "import",
Short: "Import data from yaml sources.",
Run: func(cmd *cobra.Command, args []string) {
// Init all of the sub services
s, err := storeInit(ctx)
cli.HandleError(err)
dalSvc, err := dalInit(ctx)
cli.HandleError(err)
envoySvc, err := envoyInit(ctx)
cli.HandleError(err)
var (
nodes envoyx.NodeSet
providers []envoyx.Provider
auxNodes envoyx.NodeSet
auxProviders []envoyx.Provider
)
if len(args) > 0 {
for _, a := range args {
auxNodes, auxProviders, err = envoySvc.Decode(ctx, envoyx.DecodeParams{
Type: envoyx.DecodeTypeURI,
Params: map[string]any{
"uri": "file://" + a,
},
})
cli.HandleError(err)
nodes = append(nodes, auxNodes...)
providers = append(providers, auxProviders...)
}
} else {
auxNodes, auxProviders, err = envoySvc.Decode(ctx, envoyx.DecodeParams{
Type: envoyx.DecodeTypeIO,
Params: map[string]any{
"reader": os.Stdin,
"mime": "text/yaml",
},
})
cli.HandleError(err)
// @todo consider changing this up
if len(auxProviders) > 0 {
cli.HandleError(errors.New("cannot define providers when importing from stdin"))
}
nodes = append(nodes, auxNodes...)
// providers = append(providers, auxProviders...)
}
ep := envoyx.EncodeParams{
Type: envoyx.EncodeTypeStore,
Params: map[string]any{
"storer": s,
"dal": dalSvc,
},
}
if replaceOnConflict {
ep.Envoy.MergeAlg = envoyx.OnConflictReplace
}
if skipOnConflict {
ep.Envoy.MergeAlg = envoyx.OnConflictSkip
}
if panicOnConflict {
ep.Envoy.MergeAlg = envoyx.OnConflictPanic
}
gg, err := envoySvc.Bake(ctx, ep,
providers,
nodes...,
)
cli.HandleError(err)
err = envoySvc.Encode(ctx, envoyx.EncodeParams{
Type: envoyx.EncodeTypeStore,
Params: map[string]any{
"storer": s,
"dal": dalSvc,
},
}, gg)
cli.HandleError(err)
},
}
cmd.Flags().BoolVar(
&replaceOnConflict,
"replace-existing",
false,
"replace on conflict existing resources. Default skips",
)
cmd.Flags().BoolVar(
&skipOnConflict,
"skip-existing",
false,
"skip on conflict existing resources. Default skips",
)
cmd.Flags().BoolVar(
&panicOnConflict,
"panic-existing",
false,
"panic on conflict existing resources. Default skips",
)
return cmd
}

View File

@@ -0,0 +1,246 @@
package commands
import (
"context"
"sort"
"strconv"
"strings"
"github.com/cortezaproject/corteza/server/pkg/cli"
"github.com/cortezaproject/corteza/server/pkg/rbac"
"github.com/cortezaproject/corteza/server/pkg/slice"
"github.com/cortezaproject/corteza/server/store"
"github.com/cortezaproject/corteza/server/system/types"
"github.com/spf13/cobra"
)
func RBAC(ctx context.Context, storeInit func(ctx context.Context) (store.Storer, error)) *cobra.Command {
cmd := &cobra.Command{
Use: "rbac",
Short: "RBAC tools",
Long: "Check and manipulates permissions",
}
cmd.AddCommand(rbacList(ctx, storeInit))
// @todo command that can grant/revoke/reset-all permissions
// in a similar format(s) as we do listing so that users can
// copy-paste the whole output, modify it and import it back
return cmd
}
func rbacList(ctx context.Context, storeInit func(ctx context.Context) (store.Storer, error)) (cmd *cobra.Command) {
var (
resources []string
roles []string
operations []string
groupBy string
allow, deny bool
matchResources = func(r *rbac.Rule) bool {
if len(resources) == 0 {
return true
}
for _, res := range resources {
if strings.HasPrefix(r.Resource, res) {
return true
}
}
return false
}
// makes a simple utiliy function for matching rules according to the used flags
ruleMatcher = func(s store.Storer) (map[uint64]*types.Role, func(r *rbac.Rule) bool) {
var (
opsMap = slice.ToStringBoolMap(operations)
rr []*types.Role
rolAuxMap = slice.ToStringBoolMap(roles)
rolMap = make(map[uint64]*types.Role)
rolMatch = make(map[uint64]bool)
err error
)
rr, _, err = store.SearchRoles(ctx, s, types.RoleFilter{})
for _, r := range rr {
rolMap[r.ID] = r
if rolAuxMap[r.Name] || rolAuxMap[r.Handle] || rolAuxMap[strconv.FormatUint(r.ID, 10)] {
rolMatch[r.ID] = true
}
}
cli.HandleError(err)
return rolMap, func(r *rbac.Rule) bool {
if !matchResources(r) {
return false
}
if len(opsMap) > 0 && !opsMap[r.Operation] {
return false
}
// this use of rolAuxMap to check if there are roles specified in the flags
// and rolMap for checking if for actual role map is intentional!
if len(rolAuxMap) > 0 && !rolMatch[r.RoleID] {
return false
}
if allow && r.Access != rbac.Allow {
return false
}
if deny && r.Access != rbac.Deny {
return false
}
return true
}
}
ruleSorter = func(rr []*rbac.Rule) {
sort.SliceStable(rr, func(i, j int) bool {
switch groupBy {
case "role", "rMap":
return rr[i].RoleID < rr[j].RoleID
case "res", "resource", "resources":
return strings.Compare(rr[i].Resource, rr[i].Resource) < 0
case "op", "ops", "operation", "operations":
return strings.Compare(rr[i].Operation, rr[j].Operation) < 0
}
return rr[i].Access < rr[j].Access
})
}
roleDisplayName = func(rMap map[uint64]*types.Role, r *rbac.Rule) (role string) {
if rMap[r.RoleID] != nil {
role = rMap[r.RoleID].Name
if role == "" {
role = rMap[r.RoleID].Handle
}
}
if role == "" {
return strconv.FormatUint(r.RoleID, 10)
}
return
}
lengths = func(rr []*rbac.Rule, rMap map[uint64]*types.Role) (op, res, role int) {
for _, r := range rr {
if len(r.Operation) > op {
op = len(r.Operation)
}
if len(r.Resource) > res {
res = len(r.Resource)
}
}
for _, r := range rMap {
if len(r.Name) > role {
role = len(r.Name)
}
if len(r.Handle) > role {
role = len(r.Handle)
}
}
return
}
)
cmd = &cobra.Command{
Use: "list",
Short: "Check applied permissions against given file (only supports compose permissions for now)",
Run: func(cmd *cobra.Command, args []string) {
var (
rr []*rbac.Rule
s, err = storeInit(ctx)
role string
gBucket string
)
cli.HandleError(err)
rr, _, err = store.SearchRbacRules(cli.Context(), s, rbac.RuleFilter{})
cli.HandleError(err)
ruleSorter(rr)
rMap, matchRule := ruleMatcher(s)
longestOp, longestRes, longestRole := lengths(rr, rMap)
hr := strings.Repeat("-", longestOp+longestRes+longestRole+20)
for i, r := range rr {
if !matchRule(r) {
continue
}
r.Operation = r.Operation + strings.Repeat(" ", longestOp-len(r.Operation))
r.Resource = r.Resource + strings.Repeat(" ", longestRes-len(r.Resource))
role = roleDisplayName(rMap, r)
role = role + strings.Repeat(" ", longestRole-len(role))
bOp := strings.Repeat(" ", longestOp)
bRes := strings.Repeat(" ", longestRes)
bRole := strings.Repeat(" ", longestRole)
switch groupBy {
case "role", "roles":
if gBucket != role {
if i != 0 {
cmd.Println(hr)
}
cmd.Printf("%s %7s %s on %s\n", role, r.Access, r.Operation, r.Resource)
} else {
cmd.Printf("%s %7s %s on %s\n", bRole, r.Access, r.Operation, r.Resource)
}
gBucket = role
case "res", "resource", "resources":
if gBucket != r.Resource {
if i != 0 {
cmd.Println(hr)
}
cmd.Printf("on %s %7s %s to %s\n", r.Resource, r.Access, role, r.Operation)
} else {
cmd.Printf(" %s %7s %s to %s\n", bRes, r.Access, role, r.Operation)
}
gBucket = r.Resource
case "op", "ops", "operation", "operations":
if gBucket != r.Operation {
if i != 0 {
cmd.Println(hr)
}
cmd.Printf("%s %7s %s to %s\n", r.Operation, r.Access, role, r.Resource)
} else {
cmd.Printf("%s %7s %s to %s\n", bOp, r.Access, role, r.Resource)
}
gBucket = r.Operation
default:
cmd.Printf("%7s %s to %s on %s\n", r.Access, role, r.Operation, r.Resource)
}
}
},
}
cmd.Flags().StringArrayVarP(&resources, "resource", "r", nil, "Filter by resource (by prefix)")
cmd.Flags().StringArrayVarP(&roles, "role", "", nil, "Filter by role (handle or ID)")
cmd.Flags().StringArrayVarP(&operations, "operation", "o", nil, "Filter by operation")
cmd.Flags().BoolVarP(&deny, "deny", "d", false, "Show only deny")
cmd.Flags().BoolVarP(&allow, "allow", "a", false, "Show only allows")
cmd.Flags().StringVarP(&groupBy, "group", "g", "", "Group rules on output")
return
}

View File

@@ -0,0 +1,84 @@
package commands
import (
"context"
"fmt"
"github.com/cortezaproject/corteza/server/pkg/auth"
"github.com/cortezaproject/corteza/server/pkg/cli"
"github.com/cortezaproject/corteza/server/store"
"github.com/cortezaproject/corteza/server/system/service"
"github.com/cortezaproject/corteza/server/system/types"
"github.com/spf13/cobra"
)
func Roles(ctx context.Context, app serviceInitializer) *cobra.Command {
cmd := &cobra.Command{
Use: "roles",
Aliases: []string{"role"},
Short: "Role management",
}
cmd.AddCommand(rolesAddUser(ctx, app))
cmd.AddCommand(rolesList(ctx, app))
return cmd
}
func rolesAddUser(ctx context.Context, app serviceInitializer) *cobra.Command {
return &cobra.Command{
Use: "useradd [role-ID-or-name-or-handle] [user-ID-or-email]",
Aliases: []string{"assign", "add-user", "adduser", "user-add", "user"},
Short: "Add user to role",
Args: cobra.ExactArgs(2),
PreRunE: commandPreRunInitService(app),
Run: func(cmd *cobra.Command, args []string) {
ctx = auth.SetIdentityToContext(ctx, auth.ServiceUser())
var (
roleStr, userStr = args[0], args[1]
role *types.Role
user *types.User
err error
)
role, err = service.DefaultRole.FindByAny(ctx, roleStr)
cli.HandleError(err)
user, err = service.DefaultUser.FindByAny(ctx, userStr)
cli.HandleError(err)
cli.HandleError(store.CreateRoleMember(ctx, service.DefaultStore, &types.RoleMember{
RoleID: role.ID,
Resource: fmt.Sprintf("corteza::system:user/%d", user.ID),
}))
cmd.Printf("Added user [%d] %q to [%d] %q role\n", user.ID, user.Email, role.ID, role.Name)
},
}
}
func rolesList(ctx context.Context, app serviceInitializer) *cobra.Command {
return &cobra.Command{
Use: "list",
Short: "List all roles",
PreRunE: commandPreRunInitService(app),
Run: func(cmd *cobra.Command, args []string) {
ctx = auth.SetIdentityToContext(ctx, auth.ServiceUser())
f := types.RoleFilter{Query: ""}
if len(args) > 0 {
f.Query = args[0]
}
rr, _, err := store.SearchRoles(ctx, service.DefaultStore, f)
cli.HandleError(err)
for _, r := range rr {
cmd.Printf("%-20d %-30s %s\n", r.ID, r.Handle, r.Name)
}
},
}
}

View File

@@ -0,0 +1,228 @@
package commands
import (
"context"
"encoding/json"
"fmt"
"os"
"strings"
"github.com/cortezaproject/corteza/server/pkg/auth"
"github.com/cortezaproject/corteza/server/system/types"
"github.com/spf13/cobra"
"github.com/cortezaproject/corteza/server/pkg/cli"
"github.com/cortezaproject/corteza/server/system/service"
)
func Settings(ctx context.Context, app serviceInitializer) *cobra.Command {
var (
cmd = &cobra.Command{
Use: "settings",
Short: "Settings management",
}
restartWarning = func(cmd *cobra.Command, args []string) {
fmt.Print("\nYour server needs to be restarted in order to apply changes\n")
}
)
list := &cobra.Command{
Use: "list",
Short: "List all",
PreRunE: commandPreRunInitService(app),
Run: func(cmd *cobra.Command, args []string) {
ctx = auth.SetIdentityToContext(ctx, auth.ServiceUser())
prefix := cmd.Flags().Lookup("prefix").Value.String()
if kv, err := service.DefaultSettings.FindByPrefix(ctx, prefix); err != nil {
cli.HandleError(err)
} else {
var maxlen int
for _, v := range kv {
if l := len(v.Name); l > maxlen {
maxlen = l
}
}
for _, v := range kv {
cmd.Printf("%s%s\t%v\n", v.Name, strings.Repeat(" ", maxlen-len(v.Name)), v.Value)
}
}
},
}
list.Flags().String("prefix", "", "SettingsFilter settings by prefix")
get := &cobra.Command{
Use: "get [key to get, ...]",
Short: "Get value (raw JSON) for a specific key",
Args: cobra.ExactArgs(1),
PreRunE: commandPreRunInitService(app),
Run: func(cmd *cobra.Command, args []string) {
ctx = auth.SetIdentityToContext(ctx, auth.ServiceUser())
if v, err := service.DefaultSettings.Get(ctx, args[0], 0); err != nil {
cli.HandleError(err)
} else if v != nil {
cmd.Printf("%v\n", v.Value)
}
},
}
set := &cobra.Command{
Use: "set [key to set] [value]",
Short: "Set value (raw JSON or string) for a specific key",
Args: cobra.ExactArgs(2),
PreRunE: commandPreRunInitService(app),
Run: func(cmd *cobra.Command, args []string) {
ctx = auth.SetIdentityToContext(ctx, auth.ServiceUser())
value := args[1]
v := &types.SettingValue{
Name: args[0],
}
if cmd.Flags().Lookup("as-string").Changed {
cli.HandleError(v.SetSetting(value))
} else {
err := v.SetRawSetting(value)
if _, is := err.(*json.SyntaxError); is {
// Quote the raw value and re-parse
err = v.SetRawSetting(`"` + value + `"`)
}
cli.HandleError(err)
}
cli.HandleError(service.DefaultSettings.Set(ctx, v))
},
PostRun: restartWarning,
}
set.Flags().BoolP("as-string", "s", false, "Treat input value as string (to avoid wrapping in quotes)")
imp := &cobra.Command{
Use: "import [file]",
Short: "Import settings as JSON from stdin or file",
Args: cobra.MaximumNArgs(1),
PreRunE: commandPreRunInitService(app),
Run: func(cmd *cobra.Command, args []string) {
ctx = auth.SetIdentityToContext(ctx, auth.ServiceUser())
var (
fh *os.File
err error
)
if len(args) > 0 {
fh, err = os.Open(args[0])
cli.HandleError(err)
} else {
fh = os.Stdin
}
var (
decoder = json.NewDecoder(fh)
input = map[string]interface{}{}
vv types.SettingValueSet
)
cli.HandleError(decoder.Decode(&input))
for k, v := range input {
val := &types.SettingValue{Name: k}
cli.HandleError(val.SetSetting(v))
vv = append(vv, val)
}
if len(vv) > 0 {
cli.HandleError(service.DefaultSettings.BulkSet(ctx, vv))
}
},
PostRun: restartWarning,
}
exp := &cobra.Command{
Use: "export [file]",
Short: "Export settings as JSON to stdout or file",
Args: cobra.MaximumNArgs(1),
PreRunE: commandPreRunInitService(app),
Run: func(cmd *cobra.Command, args []string) {
ctx = auth.SetIdentityToContext(ctx, auth.ServiceUser())
var (
fh *os.File
err error
)
if len(args) > 0 {
fh, err = os.Create(args[0])
cli.HandleError(err)
} else {
fh = os.Stdin
}
var (
encoder = json.NewEncoder(fh)
)
encoder.SetIndent("", " ")
if vv, err := service.DefaultSettings.FindByPrefix(ctx); err != nil {
cli.HandleError(err)
} else {
cli.HandleError(encoder.Encode(vv.KV()))
}
},
}
del := &cobra.Command{
Use: "delete [keys, ...]",
Short: "Set value (raw JSON) for a specific key (or by prefix)",
Args: cobra.MinimumNArgs(0),
PreRunE: commandPreRunInitService(app),
Run: func(cmd *cobra.Command, args []string) {
ctx = auth.SetIdentityToContext(ctx, auth.ServiceUser())
var (
names = []string{}
)
if prefix := cmd.Flags().Lookup("prefix").Value.String(); len(prefix) > 0 {
if vv, err := service.DefaultSettings.FindByPrefix(ctx); err != nil {
cli.HandleError(err)
} else {
_ = vv.Walk(func(v *types.SettingValue) error {
names = append(names, v.Name)
return nil
})
}
} else if len(args) > 0 {
names = args
}
for a := 0; a < len(names); a++ {
cli.HandleError(service.DefaultSettings.Delete(ctx, names[a], 0))
}
},
PostRun: restartWarning,
}
del.Flags().String("prefix", "", "SettingsFilter settings by prefix")
cmd.AddCommand(
list,
get,
set,
del,
imp,
exp,
)
return cmd
}

View File

@@ -0,0 +1,125 @@
package commands
import (
"context"
"time"
"github.com/cortezaproject/corteza/server/system/service"
"github.com/spf13/cobra"
)
// Will perform OpenID connect auto-configuration
func Sink(ctx context.Context, app serviceInitializer) *cobra.Command {
var (
expires string
srup = service.SinkRequestUrlParams{}
)
cmd := &cobra.Command{
Use: "sink",
Short: "Sink",
}
signatureCmd := &cobra.Command{
Use: "signature",
Short: "Creates signature for sink HTTP endpoint",
PreRunE: commandPreRunInitService(app),
RunE: func(cmd *cobra.Command, args []string) error {
if expires != "" {
// validate expiration date if set
if exp, err := time.Parse("2006-01-02", expires); err != nil {
return err
} else {
srup.Expires = &exp
}
}
if su, srup, err := service.DefaultSink.SignURL(srup); err != nil {
return err
} else {
cmd.Println(su)
cmd.Println("Sink request constraints:")
if srup.SignatureInPath {
cmd.Println(" - signature should be part of path")
} else {
cmd.Println(" - signature should be part of query-string")
}
if srup.Method != "" {
cmd.Printf(" - expecting request method %q\n", srup.Method)
}
if srup.Expires != nil {
cmd.Printf(" - signature expires at: %s\n", srup.Expires)
}
if srup.MaxBodySize > 0 {
cmd.Printf(" - max request body size is %d Kb\n", srup.MaxBodySize/1024)
} else {
cmd.Println(" - body size is not limited")
}
if srup.ContentType != "" {
cmd.Printf(" - expecting content type to be %q\n", srup.ContentType)
}
if srup.Path != "" {
cmd.Printf(" - valid path under /sink: %q\n", srup.Path)
}
}
return nil
},
}
signatureCmd.Flags().StringVar(
&srup.Origin,
"origin",
"",
"Origin of the request (arbitrary string, optional)")
signatureCmd.Flags().StringVar(
&srup.ContentType,
"content-type",
"",
"Content type (optional)")
signatureCmd.Flags().StringVar(
&srup.Path,
"path",
"",
"Full sink request path (do not include /sink prefix, add / for just root)")
signatureCmd.Flags().StringVar(
&expires,
"expires",
"",
"Date of expiration (YYYY-MM-DD, optional)")
signatureCmd.Flags().StringVar(
&srup.Method,
"method",
"",
"HTTP method that will be used (optional)")
signatureCmd.Flags().Int64Var(
&srup.MaxBodySize,
"max-body-size",
0,
"Max allowed body size")
signatureCmd.Flags().BoolVar(
&srup.SignatureInPath,
"signature-in-path",
false,
"Include signature in a path instead of query string")
cmd.AddCommand(
signatureCmd,
)
return cmd
}

View File

@@ -0,0 +1,311 @@
package commands
import (
"context"
"fmt"
"strconv"
"syscall"
"time"
"github.com/brianvoe/gofakeit/v6"
"github.com/cortezaproject/corteza/server/pkg/auth"
"github.com/cortezaproject/corteza/server/pkg/cli"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/store"
"github.com/cortezaproject/corteza/server/system/service"
"github.com/cortezaproject/corteza/server/system/types"
"github.com/spf13/cobra"
"golang.org/x/crypto/ssh/terminal"
)
func Users(ctx context.Context, app serviceInitializer) *cobra.Command {
var (
flagNoPassword bool
flagPassword string
flagMakePasswordLink bool
flagRoles []string
flagSendEmail bool
)
// User management commands.
cmd := &cobra.Command{
Use: "users",
Aliases: []string{"user"},
Short: "User management",
}
// List users.
listCmd := &cobra.Command{
Use: "list",
Short: "List users",
PreRunE: commandPreRunInitService(app),
Run: func(cmd *cobra.Command, args []string) {
ctx = auth.SetIdentityToContext(ctx, auth.ServiceUser())
var (
queryFlag = cmd.Flags().Lookup("query").Value.String()
limitFlag = cmd.Flags().Lookup("limit").Value.String()
limit int
err error
)
limit, err = strconv.Atoi(limitFlag)
cli.HandleError(err)
uf := types.UserFilter{Query: queryFlag}
uf.Sort = filter.SortExprSet{&filter.SortExpr{Column: "updated_at"}}
uf.Limit = uint(limit)
users, _, err := service.DefaultStore.SearchUsers(ctx, uf)
cli.HandleError(err)
fmt.Fprintf(
cmd.OutOrStdout(),
" Created Updated EmailAddress\n",
)
for _, u := range users {
upd := "---- -- --"
if u.UpdatedAt != nil {
upd = u.UpdatedAt.Format("2006-01-02")
}
fmt.Fprintf(
cmd.OutOrStdout(),
"%20d %s %s %-100s %s\n",
u.ID,
u.CreatedAt.Format("2006-01-02"),
upd,
u.Email,
u.Name,
)
}
},
}
listCmd.Flags().IntP("limit", "l", 20, "How many entry to display")
listCmd.Flags().StringP("query", "q", "", "Query and filter by handle, email, name")
addCmd := &cobra.Command{
Use: "add [email]",
Short: "Add new user",
Args: cobra.MinimumNArgs(1),
PreRunE: commandPreRunInitActivate(app),
Run: func(cmd *cobra.Command, args []string) {
ctx = auth.SetIdentityToContext(ctx, auth.ServiceUser())
var (
authSvc = service.Auth(service.AuthOptions{})
// @todo email validation
user = &types.User{Email: args[0]}
err error
// url for to create password(make-password-link)
url string
// Use provided password...
password = []byte(flagPassword)
role *types.Role
mm types.RoleMemberSet
)
if !flagNoPassword && !flagMakePasswordLink && len(password) == 0 {
cmd.Print("Set password: ")
if password, err = terminal.ReadPassword(int(syscall.Stdin)); err != nil {
cli.HandleError(err)
}
}
if len(password) > 0 && !authSvc.CheckPasswordStrength(string(password)) {
cli.HandleError(service.AuthErrPasswordNotSecure())
}
for _, ri := range flagRoles {
role, err = service.DefaultRole.FindByAny(ctx, ri)
cli.HandleError(err)
mm = append(mm, &types.RoleMember{RoleID: role.ID})
}
// Update current settings to be sure we do not have outdated values
cli.HandleError(service.DefaultSettings.UpdateCurrent(ctx))
if user, err = service.DefaultUser.Create(ctx, user); err != nil {
cli.HandleError(err)
}
// generate the create password link of user
if flagNoPassword && len(password) == 0 && flagMakePasswordLink {
url, err = authSvc.GeneratePasswordCreateToken(ctx, user.Email)
if err != nil {
cli.HandleError(err)
}
}
if flagSendEmail {
if err = authSvc.SendInviteEmail(ctx, user.Email); err != nil {
cli.HandleError(err)
}
}
fmt.Fprintf(cmd.OutOrStdout(), "User created [%d].\n", user.ID)
if flagMakePasswordLink && len(url) > 0 {
fmt.Fprintf(cmd.OutOrStdout(), "Set password by clicking here:\n%s\n", url)
}
if len(mm) > 0 {
_ = mm.Walk(func(m *types.RoleMember) error {
m.Resource = fmt.Sprintf("corteza::system:user/%d", user.ID)
return nil
})
cli.HandleError(store.CreateRoleMember(ctx, service.DefaultStore, mm...))
}
if len(password) > 0 {
if err = authSvc.SetPassword(ctx, user.ID, string(password)); err != nil {
cli.HandleError(err)
}
}
},
}
addCmd.Flags().BoolVar(
&flagNoPassword,
"no-password",
false,
"Do not ask for password")
addCmd.Flags().StringVar(
&flagPassword,
"password",
"",
"Provide password (as alternative to interactive way)")
addCmd.Flags().BoolVar(
&flagMakePasswordLink,
"make-password-link",
false,
"Provide link to create password")
addCmd.Flags().StringSliceVar(
&flagRoles,
"role",
nil,
"Add user to roles (use ID or handle, repeat for multiple roles)")
addCmd.Flags().BoolVar(
&flagSendEmail,
"send-invite",
false,
"Send invite email to user with accept invite link")
pwdCmd := &cobra.Command{
Use: "password [email]",
Short: "Change password for user",
Args: cobra.MinimumNArgs(1),
PreRunE: commandPreRunInitService(app),
Run: func(cmd *cobra.Command, args []string) {
ctx = auth.SetIdentityToContext(ctx, auth.ServiceUser())
var (
user *types.User
err error
password []byte
)
// Update current settings to be sure that we do not have outdated values
cli.HandleError(service.DefaultSettings.UpdateCurrent(ctx))
if user, err = service.DefaultUser.FindByEmail(ctx, args[0]); err != nil {
cli.HandleError(err)
}
cmd.Print("Set password: ")
if password, err = terminal.ReadPassword(int(syscall.Stdin)); err != nil {
cli.HandleError(err)
}
if len(password) == 0 {
// Password not set, that's ok too.
return
}
if err = service.DefaultAuth.SetPassword(ctx, user.ID, string(password)); err != nil {
cli.HandleError(err)
}
},
}
cmd.AddCommand(
listCmd,
addCmd,
pwdCmd,
UsersSynthetic(ctx, app),
)
return cmd
}
func UsersSynthetic(ctx context.Context, app serviceInitializer) *cobra.Command {
var (
total uint
faker = gofakeit.NewCrypto()
synth = &cobra.Command{
Use: "synthetic",
Aliases: []string{"synth"},
Short: "Generate or remove synthetic users",
PersistentPreRunE: func(cmd *cobra.Command, args []string) (err error) {
return app.InitServices(ctx)
},
}
gen = &cobra.Command{
Use: "generate",
Aliases: []string{"gen"},
Short: "Generate synthetic users",
Run: func(cmd *cobra.Command, args []string) {
cmd.Printf("Generating %d users ...", total)
bm := time.Now()
ctx = auth.SetIdentityToContext(ctx, auth.ServiceUser())
cli.HandleError(service.DefaultUser.CreateSynthetic(ctx, faker, total))
cmd.Printf("done in %s", time.Since(bm).Round(time.Millisecond))
cmd.Println()
},
}
rem = &cobra.Command{
Use: "remove",
Aliases: []string{"rm", "d", "delete", "del"},
Short: "Remove synthetic users",
Run: func(cmd *cobra.Command, args []string) {
cmd.Printf("Removing all synthetic users ...")
bm := time.Now()
ctx = auth.SetIdentityToContext(ctx, auth.ServiceUser())
cli.HandleError(service.DefaultUser.RemoveSynthetic(ctx))
cmd.Printf("done in %s", time.Since(bm).Round(time.Millisecond))
cmd.Println()
},
}
)
gen.Flags().UintVarP(&total, "total", "t", 1, "Number of synthetic users generated")
synth.AddCommand(gen, rem)
return synth
}

View File

@@ -0,0 +1,89 @@
package system
import (
"github.com/cortezaproject/corteza/server/codegen/schema"
)
component: schema.#component & {
handle: "system"
resources: {
"attachment": attachment
"application": application
"apigw-route": apigw_route
"apigw-filter": apigw_filter
"auth-client": auth_client
"auth-confirmed-client": auth_confirmed_client
"auth-session": auth_session
"auth-oa2token": auth_oa2token
"credential": credential
"data-privacy-request": data_privacy_request
"data-privacy-request-comment": data_privacy_request_comment
"queue": queue
"queue-message": queue_message
"reminder": reminder
"notification": notification
"report": report
"resource-translation": resource_translation
"role": role
"role-member": role_member
"user-group": user_group
"settings": settings
"template": template
"user": user
"dal-connection": dal_connection
"dal-sensitivity-level": dal_sensitivity_level
"dal-schema-alteration": dal_schema_alteration
}
rbac: operations: {
"action-log.read": description: "Access to action log"
"settings.read": description: "Read system settings"
"settings.manage": description: "Manage system settings"
"auth-client.create": description: "Create auth clients"
"auth-clients.search": description: "List, search or filter auth clients"
"role.create": description: "Create roles"
"roles.search": description: "List, search or filter roles"
"user-group.create": description: "Create user groups"
"user-groups.search": description: "List, search or filter user groups"
"user.create": description: "Create users"
"users.search": description: "List, search or filter users"
"dal-connection.create": description: "Create DAL connections"
"dal-connections.search": description: "List, search or filter DAL connections"
"dal-sensitivity-level.manage": description: "Can manage DAL sensitivity levels"
"application.create": description: "Create applications"
"applications.search": description: "List, search or filter auth clients"
"application.flag.self": description: "Manage private flags for applications"
"application.flag.global": description: "Manage global flags for applications"
"template.create": description: "Create template"
"templates.search": description: "List, search or filter templates"
"report.create": description: "Create report"
"reports.search": description: "List, search or filter reports"
"reminder.assign": description: " Assign reminders"
"queue.create": description: "Create messagebus queues"
"queues.search": description: "List, search or filter messagebus queues"
"apigw-route.create": description: "Create API gateway route"
"apigw-routes.search": description: "List search or filter API gateway routes"
"resource-translations.manage": description: "List, search, create, or update resource translations"
"dal-schema-alterations.manage": description: "List, search, apply, or dismiss DAL alterations"
"data-privacy-request.create": description: "Create data privacy requests"
"data-privacy-requests.search": description: "List, search or filter data privacy requests"
"notification.assign": description: "Assign notifications to other users"
}
}

View File

@@ -0,0 +1,81 @@
package system
import (
"github.com/cortezaproject/corteza/server/codegen/schema"
)
credential: {
model: {
omitGetterSetter: true
attributes: {
id: schema.IdField
owner_id: { schema.AttributeUserRef, storeIdent: "rel_owner", ident: "ownerID" }
label: {
dal: {}
}
kind: {
dal: { type: "Text", length: 128 }
}
credentials: {
dal: {}
}
meta: {
goType: "rawJson"
dal: { type: "JSON", defaultEmptyObject: true }
}
created_at: schema.SortableTimestampNowField
updated_at: schema.SortableTimestampNilField
deleted_at: schema.SortableTimestampNilField
last_used_at: schema.SortableTimestampNilField
expires_at: schema.SortableTimestampNilField
}
indexes: {
"primary": { attribute: "id" }
"owner_kind": {
attributes: [ "owner_id", "kind" ]
predicate: "deleted_at IS NULL"
}
}
}
filter: {
struct: {
owner_id: {goType: "uint64", ident: "ownerID", storeIdent: "rel_owner"}
kind: {goType: "string"}
credentials: {goType: "string"}
deleted: {goType: "filter.State", storeIdent: "deleted_at"}
}
byValue: ["owner_id", "kind", "credentials"]
byNilState: ["deleted"]
}
features: {
labels: false
paging: false
sorting: false
checkFn: false
}
envoy: {
omit: true
}
store: {
api: {
lookups: [
{
fields: ["id"]
description: """
searches for credentials by ID
It returns credentials even if deleted
"""
},
]
}
}
}

View File

@@ -0,0 +1,103 @@
package system
import (
"github.com/cortezaproject/corteza/server/codegen/schema"
)
dal_connection: {
model: {
attributes: {
id: schema.IdField
handle: schema.HandleField
type: {
sortable: true
dal: {}
}
config: {
goType: "types.ConnectionConfig"
dal: { type: "JSON", defaultEmptyObject: true }
omitSetter: true
omitGetter: true
}
meta: {
goType: "types.ConnectionMeta"
dal: { type: "JSON", defaultEmptyObject: true }
omitSetter: true
omitGetter: true
}
created_at: schema.SortableTimestampNowField
updated_at: schema.SortableTimestampNilField
deleted_at: schema.SortableTimestampNilField
created_by: schema.AttributeUserRef
updated_by: schema.AttributeUserRef
deleted_by: schema.AttributeUserRef
}
indexes: {
"primary": { attribute: "id" }
}
}
filter: {
struct: {
dal_connection_id: {goType: "[]uint64", ident: "dalConnectionID", storeIdent: "id"}
handle: {goType: "string"}
type: {goType: "string"}
deleted: {goType: "filter.State", storeIdent: "deleted_at"}
}
byValue: ["dal_connection_id", "handle", "type"]
byNilState: ["deleted"]
}
features: {
labels: false
}
envoy: {
yaml: {
supportMappedInput: true
mappedField: "Handle"
identKeyAlias: ["connection", "connections"]
}
store: {
extendedRefDecoder: true
}
}
rbac: {
operations: {
"read": description: "Read connection"
"update": description: "Update connection"
"delete": description: "Delete connection"
"dal-config.manage": description: "Manage DAL configuration"
}
}
store: {
api: {
lookups: [
{
fields: ["id"]
description: """
searches for connection by ID
It returns connection even if deleted or suspended
"""
}, {
fields: ["handle"]
nullConstraint: ["deleted_at"]
constraintCheck: true
description: """
searches for connection by handle
It returns only valid connection (not deleted)
"""
},
]
}
}
}

View File

@@ -0,0 +1,113 @@
package system
import (
"github.com/cortezaproject/corteza/server/codegen/schema"
)
dal_schema_alteration: {
features: {
labels: false
checkFn: false
}
model: {
// lengths for the lang, resource fields are now a bit shorter
// Reason for that is supported index length in MySQL
attributes: {
id: schema.IdField
batchID: {
goType: "uint64"
storeIdent: "batch_id"
dal: { type: "ID" }
}
dependsOn: {
goType: "uint64"
storeIdent: "depends_on"
dal: { type: "Ref", refModelResType: "corteza::system:dal-schema-alteration" }
}
resource: {
storeIdent: "resource"
dal: { type: "Text", length: 256 }
}
resourceType: {
storeIdent: "resource_type"
dal: { type: "Text", length: 256 }
}
connectionID: {
goType: "uint64"
storeIdent: "connection_id"
dal: { type: "Ref", refModelResType: "corteza::system:dal-connection" }
}
kind: {
dal: { type: "Text", length: 256 }
}
params: {
goType: "*types.DalSchemaAlterationParams"
dal: { type: "JSON", defaultEmptyObject: true }
omitSetter: true
omitGetter: true
}
error: {
dal: { type: "Text" }
}
created_at: schema.SortableTimestampNowField
updated_at: schema.SortableTimestampNilField
deleted_at: schema.SortableTimestampNilField
completed_at: schema.SortableTimestampNilField
dismissed_at: schema.SortableTimestampNilField
created_by: schema.AttributeUserRef
updated_by: schema.AttributeUserRef
deleted_by: schema.AttributeUserRef
completed_by: schema.AttributeUserRef
dismissed_by: schema.AttributeUserRef
}
indexes: {
"primary": { attribute: "id" }
"unique_alteration": {
fields: [
{ attribute: "id" },
{ attribute: "batchID" },
]
}
}
}
envoy: {
// Not needed for this resource (yet)
omit: true
}
filter: {
struct: {
resource: {goType: "[]string", ident: "resource" }
resourceType: {goType: "string", ident: "resourceType", storeIdent: "resource_type" }
alteration_id: {goType: "[]uint64", ident: "alterationID", storeIdent: "id" }
batch_id: {goType: "[]uint64", ident: "batchID" }
kind: {}
deleted: {goType: "filter.State", storeIdent: "deleted_at"}
completed: {goType: "filter.State", storeIdent: "completed_at"}
dismissed: {goType: "filter.State", storeIdent: "dismissed_at"}
}
byValue: ["kind", "resource", "resourceType", "alteration_id", "batch_id"]
byNilState: ["deleted", "completed", "dismissed"]
}
store: {
api: {
lookups: [
{
fields: ["id"]
description: """
searches for resource translation by ID
It also returns deleted resource translations.
"""
},
]
}
}
}

View File

@@ -0,0 +1,76 @@
package system
import (
"github.com/cortezaproject/corteza/server/codegen/schema"
)
dal_sensitivity_level: {
model: {
attributes: {
id: schema.IdField
handle: schema.HandleField
level: {
sortable: true,
goType: "int"
dal: { type: "Number", meta: { "rdbms:type": "integer" } }
}
meta: {
goType: "types.DalSensitivityLevelMeta"
dal: { type: "JSON", defaultEmptyObject: true }
omitSetter: true
omitGetter: true
}
created_at: schema.SortableTimestampNowField
updated_at: schema.SortableTimestampNilField
deleted_at: schema.SortableTimestampNilField
created_by: schema.AttributeUserRef
updated_by: schema.AttributeUserRef
deleted_by: schema.AttributeUserRef
}
indexes: {
"primary": { attribute: "id" }
}
}
filter: {
struct: {
dal_sensitivity_level_id: {goType: "[]uint64", ident: "dalSensitivityLevelID", storeIdent: "id"}
handle: { goType: "string" }
deleted: {goType: "filter.State", storeIdent: "deleted_at"}
}
byValue: ["dal_sensitivity_level_id", "handle"]
byNilState: ["deleted"]
}
envoy: {
yaml: {
supportMappedInput: true
mappedField: "Handle"
identKeyAlias: ["sensitivity_level"]
}
store: {}
}
features: {
labels: false
}
store: {
api: {
lookups: [
{
fields: ["id"]
description: """
searches for user by ID
It returns user even if deleted or suspended
"""
}
]
}
}
}

View File

@@ -0,0 +1,99 @@
package system
import (
"github.com/cortezaproject/corteza/server/codegen/schema"
)
data_privacy_request: {
features: {
labels: false
}
model: {
omitGetterSetter: true
attributes: {
id: schema.IdField
kind: {
goType: "types.RequestKind",
sortable: true
dal: {}
}
status: {
goType: "types.RequestStatus",
sortable: true
dal: { type: "Text", length: 64 }
}
payload: {
goType: "types.DataPrivacyRequestPayloadSet"
dal: { type: "JSON" }
}
requested_at: schema.SortableTimestampField
requested_by: {
goType: "uint64"
dal: { type: "Ref", refModelResType: "corteza::system:user" }
}
completed_at: schema.SortableTimestampNilField
completed_by: {
goType: "uint64"
dal: { type: "Ref", refModelResType: "corteza::system:user" }
}
created_at: schema.SortableTimestampNowField
updated_at: schema.SortableTimestampNilField
deleted_at: schema.SortableTimestampNilField
created_by: schema.AttributeUserRef
updated_by: schema.AttributeUserRef
deleted_by: schema.AttributeUserRef
}
indexes: {
"primary": { attribute: "id" }
}
}
filter: {
struct: {
request_id: {goType: "[]uint64", ident: "requestID", storeIdent: "id" }
requested_by: {goType: "[]uint64", ident: "requestedBy" }
kind: {goType: "[]types.RequestKind"}
status: {goType: "[]types.RequestStatus"}
}
query: ["kind", "status"]
byValue: ["kind", "status", "requested_by"]
}
rbac: {
operations: {
read:
description: "Read data privacy request"
approve:
description: "Approve/Reject data privacy request"
}
}
envoy: {
omit: true
}
store: {
api: {
lookups: [
{
fields: ["id"]
description: """
searches for data privacy request by ID
It returns data privacy request even if deleted
"""
}
]
functions: []
}
}
}

View File

@@ -0,0 +1,57 @@
package system
import (
"github.com/cortezaproject/corteza/server/codegen/schema"
)
data_privacy_request_comment: {
features: {
labels: false
}
model: {
attributes: {
id: schema.IdField
request_id: {
ident: "requestID",
goType: "uint64",
storeIdent: "rel_request"
dal: { type: "Ref", refModelResType: "corteza::system:user" }
}
comment: {
goType: "string"
dal: {}
}
created_at: schema.SortableTimestampNowField
updated_at: schema.SortableTimestampNilField
deleted_at: schema.SortableTimestampNilField
created_by: schema.AttributeUserRef
updated_by: schema.AttributeUserRef
deleted_by: schema.AttributeUserRef
}
indexes: {
"primary": { attribute: "id" }
}
}
filter: {
struct: {
request_id: {goType: "[]uint64", ident: "requestID", storeIdent: "rel_request" }
}
query: []
byValue: ["request_id"]
}
envoy: {
omit: true
}
store: {
api: {
functions: []
}
}
}

1294
server/system/envoy/store_decode.gen.go generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,110 @@
package envoy
import (
"context"
"fmt"
"github.com/cortezaproject/corteza/server/pkg/dal"
"github.com/cortezaproject/corteza/server/pkg/envoyx"
"github.com/cortezaproject/corteza/server/store"
"github.com/cortezaproject/corteza/server/system/types"
)
func (d StoreDecoder) extendDecoder(ctx context.Context, s store.Storer, dl dal.FullService, _ envoyx.DecodeParams, rt string, nodes map[string]*envoyx.Node, f envoyx.ResourceFilter) (out envoyx.NodeSet, err error) {
switch rt {
case types.SettingValueResourceType:
return d.decodeSettingValue(ctx, s, dl, types.SettingsFilter{})
}
return
}
func (d StoreDecoder) decodeSettingValue(ctx context.Context, s store.Storer, dl dal.FullService, f types.SettingsFilter) (out envoyx.NodeSet, err error) {
// @todo this might need to be improved.
// Currently, no resource is vast enough to pose a problem.
rr, _, err := store.SearchSettingValues(ctx, s, f)
if err != nil {
return
}
for _, r := range rr {
var n *envoyx.Node
n, err = SettingValueToEnvoyNode(r)
if err != nil {
return
}
out = append(out, n)
}
return
}
func SettingValueToEnvoyNode(r *types.SettingValue) (node *envoyx.Node, err error) {
// SettingValues don't have references so it can be omitted
node = &envoyx.Node{
Resource: r,
ResourceType: types.SettingValueResourceType,
Identifiers: envoyx.MakeIdentifiers(
r.Name,
),
}
return
}
func (d StoreDecoder) extendedApigwRouteDecoder(ctx context.Context, s store.Storer, dl dal.FullService, f types.ApigwRouteFilter, base envoyx.NodeSet) (out envoyx.NodeSet, err error) {
for _, b := range base {
route := b.Resource.(*types.ApigwRoute)
filters, err := d.decodeApigwFilter(ctx, s, dl, types.ApigwFilterFilter{
RouteID: route.ID,
})
if err != nil {
return nil, err
}
out = append(out, filters...)
}
return
}
func decodeAuthClientRefs(c *types.AuthClient) (refs map[string]envoyx.Ref) {
refs = make(map[string]envoyx.Ref, 4)
if c.Security.ImpersonateUser > 0 {
refs["Security.ImpersonateUser"] = envoyx.Ref{
ResourceType: types.UserResourceType,
Identifiers: envoyx.MakeIdentifiers(c.Security.ImpersonateUser),
}
}
roleSliceToRefs(refs, "Security.PermittedRoles", c.Security.PermittedRoles)
roleSliceToRefs(refs, "Security.ProhibitedRoles", c.Security.ProhibitedRoles)
roleSliceToRefs(refs, "Security.ForcedRoles", c.Security.ForcedRoles)
return
}
func decodeDalConnectionRefs(c *types.DalConnection) (refs map[string]envoyx.Ref) {
if c.Config.Privacy.SensitivityLevelID == 0 {
return
}
refs = map[string]envoyx.Ref{
"Config.Privacy.SensitivityLevelID": {
ResourceType: types.DalSensitivityLevelResourceType,
Identifiers: envoyx.MakeIdentifiers(c.Config.Privacy.SensitivityLevelID),
},
}
return
}
func roleSliceToRefs(refs map[string]envoyx.Ref, k string, rr []string) {
for i, r := range rr {
refs[fmt.Sprintf("%s.%d.RoleID", k, i)] = envoyx.Ref{
ResourceType: types.RoleResourceType,
Identifiers: envoyx.MakeIdentifiers(r),
}
}
}

2458
server/system/envoy/store_encode.gen.go generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,173 @@
package envoy
import (
"context"
"time"
"github.com/cortezaproject/corteza/server/pkg/envoyx"
"github.com/cortezaproject/corteza/server/pkg/rbac"
"github.com/cortezaproject/corteza/server/store"
"github.com/cortezaproject/corteza/server/system/types"
)
func (e StoreEncoder) prepare(ctx context.Context, p envoyx.EncodeParams, s store.Storer, rt string, nn envoyx.NodeSet) (err error) {
switch rt {
case rbac.RuleResourceType:
return e.prepareRbacRule(ctx, p, s, nn)
case types.ResourceTranslationResourceType:
return e.prepareResourceTranslation(ctx, p, s, nn)
case types.SettingValueResourceType:
return e.prepareSetting(ctx, p, s, nn)
}
return
}
func (e StoreEncoder) encode(ctx context.Context, p envoyx.EncodeParams, s store.Storer, rt string, nn envoyx.NodeSet, tree envoyx.Traverser) (err error) {
switch rt {
case rbac.RuleResourceType:
return e.encodeRbacRules(ctx, p, s, nn, tree)
case types.ResourceTranslationResourceType:
return e.encodeResourceTranslations(ctx, p, s, nn, tree)
case types.SettingValueResourceType:
return e.encodeSettings(ctx, p, s, nn, tree)
}
return
}
func (e StoreEncoder) setApplicationDefaults(res *types.Application) (err error) {
if res.CreatedAt.IsZero() {
res.CreatedAt = time.Now()
}
if res.Unify == nil {
res.Unify = &types.ApplicationUnify{}
}
return
}
func (e StoreEncoder) validateApplication(res *types.Application) (err error) {
return
}
func (e StoreEncoder) setApigwRouteDefaults(res *types.ApigwRoute) (err error) {
if res.CreatedAt.IsZero() {
res.CreatedAt = time.Now()
}
return
}
func (e StoreEncoder) validateApigwRoute(res *types.ApigwRoute) (err error) {
return
}
func (e StoreEncoder) setApigwFilterDefaults(res *types.ApigwFilter) (err error) {
if res.CreatedAt.IsZero() {
res.CreatedAt = time.Now()
}
return
}
func (e StoreEncoder) validateApigwFilter(res *types.ApigwFilter) (err error) {
return
}
func (e StoreEncoder) setAuthClientDefaults(res *types.AuthClient) (err error) {
if res.CreatedAt.IsZero() {
res.CreatedAt = time.Now()
}
return
}
func (e StoreEncoder) validateAuthClient(res *types.AuthClient) (err error) {
return
}
func (e StoreEncoder) setQueueDefaults(res *types.Queue) (err error) {
if res.CreatedAt.IsZero() {
res.CreatedAt = time.Now()
}
return
}
func (e StoreEncoder) validateQueue(res *types.Queue) (err error) {
return
}
func (e StoreEncoder) setReportDefaults(res *types.Report) (err error) {
if res.CreatedAt.IsZero() {
res.CreatedAt = time.Now()
}
return
}
func (e StoreEncoder) validateReport(res *types.Report) (err error) {
return
}
func (e StoreEncoder) setRoleDefaults(res *types.Role) (err error) {
if res.CreatedAt.IsZero() {
res.CreatedAt = time.Now()
}
return
}
func (e StoreEncoder) validateRole(res *types.Role) (err error) {
return
}
func (e StoreEncoder) setTemplateDefaults(res *types.Template) (err error) {
if res.CreatedAt.IsZero() {
res.CreatedAt = time.Now()
}
return
}
func (e StoreEncoder) validateTemplate(res *types.Template) (err error) {
return
}
func (e StoreEncoder) setUserDefaults(res *types.User) (err error) {
if res.CreatedAt.IsZero() {
res.CreatedAt = time.Now()
}
return
}
func (e StoreEncoder) validateUser(res *types.User) (err error) {
return
}
func (e StoreEncoder) setDalConnectionDefaults(res *types.DalConnection) (err error) {
if res.CreatedAt.IsZero() {
res.CreatedAt = time.Now()
}
return
}
func (e StoreEncoder) validateDalConnection(res *types.DalConnection) (err error) {
return
}
func (e StoreEncoder) setDalSensitivityLevelDefaults(res *types.DalSensitivityLevel) (err error) {
if res.CreatedAt.IsZero() {
res.CreatedAt = time.Now()
}
return
}
func (e StoreEncoder) validateDalSensitivityLevel(res *types.DalSensitivityLevel) (err error) {
return
}

View File

@@ -0,0 +1,123 @@
package envoy
import (
"context"
"fmt"
"strings"
"github.com/cortezaproject/corteza/server/pkg/envoyx"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/pkg/id"
"github.com/cortezaproject/corteza/server/store"
"github.com/cortezaproject/corteza/server/system/types"
"github.com/pkg/errors"
)
func (e StoreEncoder) prepareResourceTranslation(ctx context.Context, p envoyx.EncodeParams, s store.Storer, nn envoyx.NodeSet) (err error) {
// @todo existing resource translations?
for _, n := range nn {
if n.Resource == nil {
panic("unexpected state: cannot call prepareResourceTranslation with nodes without a defined Resource")
}
res, ok := n.Resource.(*types.ResourceTranslation)
if !ok {
panic("unexpected resource type: node expecting type of ResourceTranslation")
}
// Run expressions on the nodes
err = e.runEvals(ctx, false, n)
if err != nil {
return
}
res.ID = id.Next()
// @todo merge conflicts if we do existing assertion
n.Resource = res
}
return
}
// encodeResourceTranslations encodes a set of resource into the database
func (e StoreEncoder) encodeResourceTranslations(ctx context.Context, p envoyx.EncodeParams, s store.Storer, nn envoyx.NodeSet, tree envoyx.Traverser) (err error) {
seen := make(map[string]uint64)
for _, n := range nn {
err = e.encodeResourceTranslation(ctx, p, s, n, tree, seen)
if err != nil {
return
}
}
return
}
// encodeResourceTranslation encodes the resource into the database
func (e StoreEncoder) encodeResourceTranslation(ctx context.Context, p envoyx.EncodeParams, s store.Storer, n *envoyx.Node, tree envoyx.Traverser, seen map[string]uint64) (err error) {
// Grab dependency references
var auxID uint64
err = func() (err error) {
for fieldLabel, ref := range n.References {
auxID = safeParentID(tree, n, ref)
err = n.Resource.SetValue(fieldLabel, 0, auxID)
if err != nil {
return
}
}
return
}()
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("failed to set dependency references for %s %s", n.ResourceType, n.Resource))
return
}
res := n.Resource.(*types.ResourceTranslation)
// Reuse the ID of the matching translation so the upsert updates it instead
// of tripping the unique (lang, resource, key) constraint; translations can
// repeat inside the import batch or already exist in the database
key := strings.ToLower(res.Lang.String() + "|" + res.Resource + "|" + res.K)
if ID, ok := seen[key]; ok {
res.ID = ID
} else {
if err = e.setExistingResourceTranslationID(ctx, s, res); err != nil {
return
}
seen[key] = res.ID
}
// Flush to the DB
err = store.UpsertResourceTranslation(ctx, s, res)
if err != nil {
return
}
return
}
// setExistingResourceTranslationID looks up the stored translation with the
// same (lang, resource, key) and reuses its ID; deleted translations are
// matched too as the unique constraint covers them as well
func (e StoreEncoder) setExistingResourceTranslationID(ctx context.Context, s store.Storer, res *types.ResourceTranslation) (err error) {
ee, _, err := store.SearchResourceTranslations(ctx, s, types.ResourceTranslationFilter{
Lang: res.Lang.String(),
Resource: res.Resource,
Deleted: filter.StateInclusive,
})
if err != nil && err != store.ErrNotFound {
return err
}
for _, ex := range ee {
if strings.EqualFold(ex.K, res.K) {
res.ID = ex.ID
break
}
}
return nil
}

View File

@@ -0,0 +1,77 @@
package envoy
import (
"context"
"fmt"
"github.com/cortezaproject/corteza/server/pkg/envoyx"
"github.com/cortezaproject/corteza/server/pkg/rbac"
"github.com/cortezaproject/corteza/server/store"
"github.com/pkg/errors"
)
func (e StoreEncoder) prepareRbacRule(ctx context.Context, p envoyx.EncodeParams, s store.Storer, nn envoyx.NodeSet) (err error) {
// @todo existing RBAC rules?
for _, n := range nn {
if n.Resource == nil {
panic("unexpected state: cannot call prepareRbacRule with nodes without a defined Resource")
}
res, ok := n.Resource.(*rbac.Rule)
if !ok {
panic("unexpected resource type: node expecting type of RbacRule")
}
// Run expressions on the nodes
err = e.runEvals(ctx, false, n)
if err != nil {
return
}
// @todo merge conflicts if we do existing assertion
n.Resource = res
}
return
}
// encodeRbacRules encodes a set of resource into the database
func (e StoreEncoder) encodeRbacRules(ctx context.Context, p envoyx.EncodeParams, s store.Storer, nn envoyx.NodeSet, tree envoyx.Traverser) (err error) {
for _, n := range nn {
err = e.encodeRbacRule(ctx, p, s, n, tree)
if err != nil {
return
}
}
return
}
// encodeRbacRule encodes the resource into the database
func (e StoreEncoder) encodeRbacRule(ctx context.Context, p envoyx.EncodeParams, s store.Storer, n *envoyx.Node, tree envoyx.Traverser) (err error) {
// Grab dependency references
var auxID uint64
err = func() (err error) {
for fieldLabel, ref := range n.References {
auxID = safeParentID(tree, n, ref)
err = n.Resource.SetValue(fieldLabel, 0, auxID)
if err != nil {
return
}
}
return
}()
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("failed to set dependency references for %s %s", n.ResourceType, n.Resource))
return
}
// Flush to the DB
err = store.UpsertRbacRule(ctx, s, n.Resource.(*rbac.Rule))
if err != nil {
return
}
return
}

View File

@@ -0,0 +1,68 @@
package envoy
import (
"context"
"time"
"github.com/cortezaproject/corteza/server/pkg/envoyx"
"github.com/cortezaproject/corteza/server/store"
"github.com/cortezaproject/corteza/server/system/types"
systemTypes "github.com/cortezaproject/corteza/server/system/types"
)
func (e StoreEncoder) prepareSetting(ctx context.Context, p envoyx.EncodeParams, s store.Storer, nn envoyx.NodeSet) (err error) {
// @todo existing settings?
for _, n := range nn {
if n.Resource == nil {
panic("unexpected state: cannot call prepareSetting with nodes without a defined Resource")
}
res, ok := n.Resource.(*types.SettingValue)
if !ok {
panic("unexpected resource type: node expecting type of SettingValue")
}
// Run expressions on the nodes
err = e.runEvals(ctx, false, n)
if err != nil {
return
}
// @todo merge conflicts if we do existing assertion
if res.UpdatedAt.IsZero() {
res.UpdatedAt = time.Now()
}
n.Resource = res
}
return
}
// encodeSettings encodes a set of resource into the database
func (e StoreEncoder) encodeSettings(ctx context.Context, p envoyx.EncodeParams, s store.Storer, nn envoyx.NodeSet, tree envoyx.Traverser) (err error) {
for _, n := range nn {
err = e.encodeSetting(ctx, p, s, n, tree)
if err != nil {
return
}
}
return
}
// encodeSetting encodes the resource into the database
func (e StoreEncoder) encodeSetting(ctx context.Context, p envoyx.EncodeParams, s store.Storer, n *envoyx.Node, tree envoyx.Traverser) (err error) {
// SettingValues don't have any references so there is no need to handle them
// Flush to the DB
if !n.Evaluated.Skip {
err = store.UpsertSettingValue(ctx, s, n.Resource.(*systemTypes.SettingValue))
if err != nil {
return
}
}
return
}

View File

@@ -0,0 +1,119 @@
package envoy
import (
"context"
"fmt"
"strings"
"github.com/cortezaproject/corteza/server/pkg/envoyx"
"github.com/cortezaproject/corteza/server/store"
"github.com/cortezaproject/corteza/server/system/types"
"github.com/spf13/cast"
)
type (
// UserGetter is a utility struct to resolve user references from
// different parts of the system such as the dep graph and the database
UserGetter struct {
depGraph *envoyx.DepGraph
store store.Storer
baseFilter types.UserFilter
// resolved refs, including misses; imports resolve the same handful
// of refs over and over so this saves a query per value
cache map[string]uint64
}
)
func MakeUserGetter(s store.Storer, tt envoyx.Traverser) (g *UserGetter) {
g = &UserGetter{
store: s,
cache: make(map[string]uint64),
}
g.baseFilter = types.UserFilter{}
return
}
// Resolve returns the user ID for the provided reference
//
// If the user can not be resolved, 0 is returned.
func (g *UserGetter) Resolve(ctx context.Context, ref any) (out uint64, err error) {
// Try to get from datasource
if g.depGraph != nil {
out, err = g.getDS(ref)
if err != nil {
return
}
}
if out > 0 {
return
}
// Try to get from the database
out, err = g.getDB(ctx, ref)
return
}
func (g *UserGetter) getDS(ref any) (out uint64, err error) {
n := g.depGraph.NodeForRef(envoyx.Ref{
ResourceType: types.UserResourceType,
Identifiers: envoyx.MakeIdentifiers(ref),
})
if n == nil {
return
}
return n.Resource.GetID(), nil
}
// @todo this can be improved by prefetching and indexing refs
func (g *UserGetter) getDB(ctx context.Context, ref any) (out uint64, err error) {
aux := strings.TrimSpace(cast.ToString(ref))
// Nothing to resolve; without this the query below would match any user
if aux == "" {
return 0, nil
}
// Numeric references are user IDs (record exports write IDs); keep them
// as-is instead of reinterpreting them as a name/email query — the query
// could substring-match an unrelated user, and unresolvable IDs (deleted
// or purged users) round-tripped verbatim before user refs were resolved
// through this getter.
if id := cast.ToUint64(aux); id > 0 {
return id, nil
}
if out, ok := g.cache[aux]; ok {
return out, nil
}
defer func() {
if err == nil {
g.cache[aux] = out
}
}()
f := g.baseFilter
// @todo expand this
f.Query = aux
set, _, err := store.SearchUsers(ctx, g.store, f)
if err != nil {
return
}
if len(set) == 0 {
return
}
if len(set) > 1 {
err = fmt.Errorf("ambiguous reference %v: matches more then one user", ref)
return
}
return set[0].ID, nil
}

3779
server/system/envoy/yaml_decode.gen.go generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,211 @@
package envoy
import (
"encoding/json"
"fmt"
"strings"
"github.com/cortezaproject/corteza/server/pkg/envoyx"
"github.com/cortezaproject/corteza/server/pkg/y7s"
"github.com/cortezaproject/corteza/server/system/types"
systemTypes "github.com/cortezaproject/corteza/server/system/types"
sqlt "github.com/jmoiron/sqlx/types"
"golang.org/x/text/language"
"gopkg.in/yaml.v3"
)
func (d *auxYamlDoc) unmarshalYAML(k string, n *yaml.Node) (out envoyx.NodeSet, err error) {
switch k {
case "settings", "setting":
out, err = d.unmarshalSettingsNode(n)
}
return
}
func (d *auxYamlDoc) unmarshalSettingsNode(n *yaml.Node) (out envoyx.NodeSet, err error) {
out = make(envoyx.NodeSet, 0, len(n.Content))
err = y7s.Each(n, func(k, v *yaml.Node) (err error) {
if v == nil {
return y7s.NodeErr(n, "malformed setting definition")
}
s := &types.SettingValue{}
switch v.Kind {
case yaml.MappingNode:
if err = v.Decode(&s); err != nil {
return
}
default:
if err = y7s.DecodeScalar(k, "setting name", &s.Name); err != nil {
return err
}
if y7s.IsKind(v, yaml.SequenceNode) {
aux := make([]interface{}, 0, 10)
y7s.EachSeq(v, func(n *yaml.Node) error {
var vx interface{}
err := y7s.DecodeScalar(n, "setting value", &vx)
if err != nil {
return err
}
aux = append(aux, vx)
return nil
})
m, err := json.Marshal(aux)
if err != nil {
return err
}
s.Value = sqlt.JSONText(m)
} else if y7s.IsKind(v, yaml.MappingNode) {
aux := make(map[string]interface{})
y7s.EachMap(v, func(k, v *yaml.Node) error {
var vx interface{}
err := y7s.DecodeScalar(v, "setting value", &vx)
if err != nil {
return err
}
aux[k.Value] = vx
return nil
})
m, err := json.Marshal(aux)
if err != nil {
return err
}
s.Value = sqlt.JSONText(m)
} else {
var aux interface{}
err = y7s.DecodeScalar(v, "setting value", &aux)
if err != nil {
return err
}
m, err := json.Marshal(aux)
if err != nil {
return err
}
s.Value = sqlt.JSONText(m)
}
}
out = append(out, &envoyx.Node{
Resource: s,
ResourceType: types.SettingValueResourceType,
})
return
})
err = y7s.EachMap(n, func(lang, loc *yaml.Node) error {
langTag := systemTypes.Lang{Tag: language.Make(lang.Value)}
return y7s.EachMap(loc, func(res, kv *yaml.Node) error {
return y7s.EachMap(kv, func(k, msg *yaml.Node) error {
out = append(out, &envoyx.Node{
Resource: &systemTypes.ResourceTranslation{
Resource: res.Value,
Lang: langTag,
K: k.Value,
Message: msg.Value,
},
// Providing resource type as plain text to reduce cross component references
ResourceType: "corteza::system:resource-translation",
References: envoyx.SplitResourceIdentifier(res.Value),
})
return nil
})
})
})
if err != nil {
return
}
for _, o := range out {
for _, r := range o.References {
if r.Scope.IsEmpty() {
continue
}
o.Scope = r.Scope
break
}
}
return
}
func (d *auxYamlDoc) unmarshalFiltersExtendedNode(dctx documentContext, n *yaml.Node, meta ...*yaml.Node) (out envoyx.NodeSet, err error) {
return d.unmarshalApigwFilterNode(dctx, n, meta...)
}
func (d *auxYamlDoc) unmarshalUserRolesNode(r *types.User, n *yaml.Node) (refs map[string]envoyx.Ref, idents envoyx.Identifiers, err error) {
refs = make(map[string]envoyx.Ref, len(n.Content))
i := 0
err = y7s.EachSeq(n, func(n *yaml.Node) error {
refs[fmt.Sprintf("Roles.%d", i)] = envoyx.Ref{
ResourceType: types.RoleResourceType,
Identifiers: envoyx.MakeIdentifiers(n.Value),
}
return nil
})
return
}
func (d *auxYamlDoc) unmarshalAuthClientSecurityNode(r *types.AuthClient, n *yaml.Node) (refs map[string]envoyx.Ref, idents envoyx.Identifiers, err error) {
refs = make(map[string]envoyx.Ref)
err = y7s.EachMap(n, func(k, v *yaml.Node) (err error) {
switch strings.ToLower(k.Value) {
case "impersonateuser":
var av string
err = y7s.DecodeScalar(v, "Impersonate user", &av)
refs["Security.ImpersonateUser"] = envoyx.Ref{
ResourceType: types.UserResourceType,
Identifiers: envoyx.MakeIdentifiers(av),
}
break
case "permittedroles":
refs = envoyx.MergeRefs(refs, roleNodeSliceToRefs("Security.PermittedRoles", v))
break
case "prohibitedroles":
refs = envoyx.MergeRefs(refs, roleNodeSliceToRefs("Security.ProhibitedRoles", v))
break
case "forcedroles":
refs = envoyx.MergeRefs(refs, roleNodeSliceToRefs("Security.ForcedRoles", v))
break
}
return nil
})
return
}
func roleNodeSliceToRefs(k string, n *yaml.Node) (refs map[string]envoyx.Ref) {
refs = make(map[string]envoyx.Ref, len(n.Content))
i := 0
y7s.EachSeq(n, func(n *yaml.Node) error {
refs[fmt.Sprintf("%s.%d", k, i)] = envoyx.Ref{
ResourceType: types.RoleResourceType,
Identifiers: envoyx.MakeIdentifiers(n.Value),
}
i++
return nil
})
return
}

1121
server/system/envoy/yaml_encode.gen.go generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,339 @@
package envoy
import (
"context"
"fmt"
"strings"
"github.com/cortezaproject/corteza/server/pkg/envoyx"
"github.com/cortezaproject/corteza/server/pkg/rbac"
"github.com/cortezaproject/corteza/server/pkg/y7s"
"github.com/cortezaproject/corteza/server/system/types"
"gopkg.in/yaml.v3"
)
type (
// Wrapper to avoid constant type casting for role nodes
rbacRuleWrap struct {
node *envoyx.Node
rule *rbac.Rule
}
// Wrapper to keep role and rules together
rbacRuleRoleWrap struct {
rules []rbacRuleWrap
roleNode *envoyx.Node
role *types.Role
}
)
func (e YamlEncoder) encode(ctx context.Context, base *yaml.Node, p envoyx.EncodeParams, rt string, nodes envoyx.NodeSet, tt envoyx.Traverser) (out *yaml.Node, err error) {
switch rt {
case rbac.RuleResourceType:
return e.encodeRbacRules(ctx, base, p, nodes, tt)
case types.ResourceTranslationResourceType:
return e.encodeResourceTranslations(ctx, base, p, nodes, tt)
case types.SettingValueResourceType:
return e.encodeSettingValues(ctx, base, p, nodes, tt)
}
return
}
func (e YamlEncoder) encodeResourceTranslations(ctx context.Context, base *yaml.Node, p envoyx.EncodeParams, nodes envoyx.NodeSet, tt envoyx.Traverser) (out *yaml.Node, err error) {
err = e.resolveRulePathDeps(ctx, tt, nodes)
if err != nil {
return
}
byLang := make(map[string][]*envoyx.Node)
for _, n := range nodes {
byLang[n.Resource.(*types.ResourceTranslation).Lang.String()] = append(byLang[n.Resource.(*types.ResourceTranslation).Lang.String()], n)
}
out = base
var aux *yaml.Node
for lang, nodes := range byLang {
aux, err = e.encodeResourceTranslationsByResource(p, nodes, tt)
if err != nil {
return
}
out, err = y7s.AddMap(out, lang, aux)
if err != nil {
return
}
}
return y7s.MakeMap("locale", out)
}
func (e YamlEncoder) encodeResourceTranslationsByResource(p envoyx.EncodeParams, nodes envoyx.NodeSet, tt envoyx.Traverser) (out *yaml.Node, err error) {
byResource := make(map[string][]*envoyx.Node)
for _, n := range nodes {
byResource[n.Resource.(*types.ResourceTranslation).Resource] = append(byResource[n.Resource.(*types.ResourceTranslation).Resource], n)
}
var aux *yaml.Node
for resource, nodes := range byResource {
aux, err = e.encodeResourceTranslationsKv(p, nodes, tt)
if err != nil {
return
}
out, err = y7s.AddMap(out, resource, aux)
if err != nil {
return
}
}
return
}
func (e YamlEncoder) encodeResourceTranslationsKv(p envoyx.EncodeParams, nodes envoyx.NodeSet, tt envoyx.Traverser) (out *yaml.Node, err error) {
out, _ = y7s.MakeMap()
for _, n := range nodes {
rt := n.Resource.(*types.ResourceTranslation)
out, err = y7s.AddMap(out, rt.K, rt.Message)
if err != nil {
return
}
}
return
}
func (e YamlEncoder) encodeRbacRules(ctx context.Context, base *yaml.Node, p envoyx.EncodeParams, nodes envoyx.NodeSet, tt envoyx.Traverser) (out *yaml.Node, err error) {
err = e.resolveRulePathDeps(ctx, tt, nodes)
if err != nil {
return
}
// Batch by access (allow, deny)
allows := make([]rbacRuleWrap, 0, len(nodes))
denies := make([]rbacRuleWrap, 0, len(nodes))
for _, n := range nodes {
r := n.Resource.(*rbac.Rule)
if r.Access == rbac.Allow {
allows = append(allows, rbacRuleWrap{
node: n,
rule: r,
})
} else {
denies = append(denies, rbacRuleWrap{
node: n,
rule: r,
})
}
}
// Encode allows
var aux *yaml.Node
aux, err = e.encodeRbacRulesByRole(p, allows, tt)
if err != nil {
return
}
out = base
out, err = y7s.AddMap(out, "allow", aux)
if err != nil {
return
}
// Encode denies
aux, err = e.encodeRbacRulesByRole(p, denies, tt)
if err != nil {
return
}
out, err = y7s.AddMap(out, "deny", aux)
if err != nil {
return
}
return
}
func (e YamlEncoder) encodeSettingValues(ctx context.Context, base *yaml.Node, p envoyx.EncodeParams, nodes envoyx.NodeSet, tt envoyx.Traverser) (out *yaml.Node, err error) {
// Setting values don't have any refs
for _, n := range nodes {
sv := n.Resource.(*types.SettingValue)
base, err = y7s.AddMap(base, sv.Name, sv.Value)
if err != nil {
return
}
}
return y7s.MakeMap("settings", base)
}
func (e YamlEncoder) encodeRbacRulesByRole(p envoyx.EncodeParams, rules []rbacRuleWrap, tt envoyx.Traverser) (out *yaml.Node, err error) {
// Batch by role; make sure to keep the multiple identifier thing in mind
byRole := make(map[string]rbacRuleRoleWrap)
for _, r := range rules {
role := r.node.References["RoleID"]
aux := rbacRuleRoleWrap{}
ok := false
// Check if an identifier already exists
for _, ident := range role.Identifiers.Slice {
aux, ok = byRole[ident]
if ok {
break
}
}
// If not, create a new entry and resolve the role
if !ok {
aux.roleNode = tt.ParentForRef(r.node, role)
if aux.roleNode != nil {
aux.role = aux.roleNode.Resource.(*types.Role)
}
}
// Add the rule to the batch and update potentially missing identifiers
aux.rules = append(aux.rules, r)
for _, ident := range role.Identifiers.Slice {
byRole[ident] = aux
}
}
// Encode
var aux *yaml.Node
for _, r := range byRole {
aux, err = e.encodeRbacRulesByResource(p, r.rules)
if err != nil {
return
}
out, err = y7s.AddMap(out,
r.roleNode.Identifiers.FriendlyIdentifier(), aux,
)
if err != nil {
return
}
}
return
}
func (e YamlEncoder) encodeRbacRulesByResource(p envoyx.EncodeParams, rules []rbacRuleWrap) (out *yaml.Node, err error) {
byResource := make(map[string][]rbacRuleWrap)
// Batch
for _, r := range rules {
byResource[r.rule.Resource] = append(byResource[r.rule.Resource], r)
}
// Encode
var aux *yaml.Node
for resource, rules := range byResource {
aux, err = e.encodeRbacRuleOperations(p, rules)
if err != nil {
return
}
out, err = y7s.AddMap(out, resource, aux)
if err != nil {
return
}
}
return
}
func (e YamlEncoder) encodeRbacRuleOperations(p envoyx.EncodeParams, rules []rbacRuleWrap) (out *yaml.Node, err error) {
ops, _ := y7s.MakeSeq()
for _, r := range rules {
ops, err = y7s.AddSeq(ops, r.rule.Operation)
if err != nil {
return
}
}
return ops, nil
}
func (e YamlEncoder) resolveRulePathDeps(ctx context.Context, tt envoyx.Traverser, nodes envoyx.NodeSet) (err error) {
for _, n := range nodes {
var auxIdent string
for fieldLabel, ref := range n.References {
// Only resolve path refs; others are done later on
if !strings.Contains(fieldLabel, "Path.") {
continue
}
auxIdent = safeParentIdentifier(tt, n, ref)
if auxIdent == "" || auxIdent == "0" {
continue
}
err = n.Resource.SetValue(fieldLabel, 0, auxIdent)
if err != nil {
return
}
}
}
return
}
func (e YamlEncoder) encodeAuthClientSecurityC(ctx context.Context, p envoyx.EncodeParams, tt envoyx.Traverser, n *envoyx.Node, ac *types.AuthClient, sec *types.AuthClientSecurity) (_ any, err error) {
sqPermittedRoles, err := e.encodeRoleSlice(n, tt, "Security.PermittedRoles", sec.PermittedRoles)
if err != nil {
return
}
sqProhibitedRoles, err := e.encodeRoleSlice(n, tt, "Security.ProhibitedRoles", sec.ProhibitedRoles)
if err != nil {
return
}
sqForcedRoles, err := e.encodeRoleSlice(n, tt, "Security.ForcedRoles", sec.ForcedRoles)
if err != nil {
return
}
var impersonateUser string
if _, ok := n.References["Security.ImpersonateUser.UserID"]; ok {
usrRef := n.References["Security.ImpersonateUser.UserID"]
node := tt.ParentForRef(n, usrRef)
if node == nil {
err = fmt.Errorf("invalid user reference %v: user does not exist", usrRef)
return
}
impersonateUser = n.Identifiers.FriendlyIdentifier()
}
return y7s.MakeMap(
"impersonateUser", impersonateUser,
"permittedRoles", sqPermittedRoles,
"prohibitedRoles", sqProhibitedRoles,
"forcedRoles", sqForcedRoles,
)
}
func (e YamlEncoder) encodeRoleSlice(n *envoyx.Node, tt envoyx.Traverser, k string, rr []string) (out *yaml.Node, err error) {
sq, _ := y7s.MakeSeq()
for i := range rr {
roleRef := n.References[fmt.Sprintf("%s.%d.RoleID", k, i)]
node := tt.ParentForRef(n, roleRef)
if node == nil {
err = fmt.Errorf("invalid user reference %v: user does not exist", roleRef)
return
}
sq, err = y7s.AddSeq(sq, node.Identifiers.FriendlyIdentifier())
if err != nil {
return
}
}
return sq, nil
}

466
server/system/model/corteza.gen.go generated Normal file
View File

@@ -0,0 +1,466 @@
package model
// This file is auto-generated.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
//
import (
discoverytype "github.com/cortezaproject/corteza/server/discovery/types"
actionlogtype "github.com/cortezaproject/corteza/server/pkg/actionlog"
"github.com/cortezaproject/corteza/server/pkg/dal"
flagtype "github.com/cortezaproject/corteza/server/pkg/flag/types"
labelstype "github.com/cortezaproject/corteza/server/pkg/label/types"
rbactype "github.com/cortezaproject/corteza/server/pkg/rbac"
)
var Action = &dal.Model{
Ident: "actionlog",
ResourceType: actionlogtype.ActionResourceType,
Attributes: dal.AttributeSet{
&dal.Attribute{
Ident: "ID",
Type: &dal.TypeID{},
Store: &dal.CodecAlias{Ident: "id"},
},
&dal.Attribute{
Ident: "Timestamp", Sortable: true,
Type: &dal.TypeTimestamp{Timezone: true, Precision: -1},
Store: &dal.CodecAlias{Ident: "ts"},
},
&dal.Attribute{
Ident: "ActorIPAddr",
Type: &dal.TypeText{Length: 64},
Store: &dal.CodecAlias{Ident: "actor_ip_addr"},
},
&dal.Attribute{
Ident: "ActorID",
Type: &dal.TypeRef{
RefAttribute: "id",
RefModel: &dal.ModelRef{
ResourceType: "corteza::system:user",
},
},
Store: &dal.CodecAlias{Ident: "actor_id"},
},
&dal.Attribute{
Ident: "RequestOrigin",
Type: &dal.TypeText{Length: 32},
Store: &dal.CodecAlias{Ident: "request_origin"},
},
&dal.Attribute{
Ident: "RequestID",
Type: &dal.TypeText{Length: 256},
Store: &dal.CodecAlias{Ident: "request_id"},
},
&dal.Attribute{
Ident: "Resource",
Type: &dal.TypeText{Length: 512},
Store: &dal.CodecAlias{Ident: "resource"},
},
&dal.Attribute{
Ident: "Action",
Type: &dal.TypeText{Length: 64},
Store: &dal.CodecAlias{Ident: "action"},
},
&dal.Attribute{
Ident: "Error",
Type: &dal.TypeText{},
Store: &dal.CodecAlias{Ident: "error"},
},
&dal.Attribute{
Ident: "Severity",
Type: &dal.TypeNumber{HasDefault: true,
DefaultValue: 0,
Precision: -1, Scale: -1, Meta: map[string]interface{}{"rdbms:type": "integer"},
},
Store: &dal.CodecAlias{Ident: "severity"},
},
&dal.Attribute{
Ident: "Description",
Type: &dal.TypeText{},
Store: &dal.CodecAlias{Ident: "description"},
},
&dal.Attribute{
Ident: "Meta",
Type: &dal.TypeJSON{
DefaultValue: "{}",
},
Store: &dal.CodecAlias{Ident: "meta"},
},
},
Indexes: dal.IndexSet{
&dal.Index{
Ident: "actionlog_action",
Type: "BTREE",
Fields: []*dal.IndexField{
{
AttributeIdent: "Action",
},
},
},
&dal.Index{
Ident: "actionlog_actorId",
Type: "BTREE",
Fields: []*dal.IndexField{
{
AttributeIdent: "ActorID",
},
},
},
&dal.Index{
Ident: "PRIMARY",
Type: "BTREE",
Fields: []*dal.IndexField{
{
AttributeIdent: "ID",
},
},
},
&dal.Index{
Ident: "actionlog_relResource",
Type: "BTREE",
Fields: []*dal.IndexField{
{
AttributeIdent: "Resource",
},
},
},
&dal.Index{
Ident: "actionlog_ts",
Type: "BTREE",
Fields: []*dal.IndexField{
{
AttributeIdent: "Timestamp",
},
},
},
},
}
var Flag = &dal.Model{
Ident: "flags",
ResourceType: flagtype.FlagResourceType,
Attributes: dal.AttributeSet{
&dal.Attribute{
Ident: "Kind",
Type: &dal.TypeText{Length: 64},
Store: &dal.CodecAlias{Ident: "kind"},
},
&dal.Attribute{
Ident: "ResourceID",
Type: &dal.TypeID{},
Store: &dal.CodecAlias{Ident: "rel_resource"},
},
&dal.Attribute{
Ident: "OwnedBy",
Type: &dal.TypeRef{HasDefault: true,
DefaultValue: 0,
RefAttribute: "id",
RefModel: &dal.ModelRef{
ResourceType: "corteza::system:user",
},
},
Store: &dal.CodecAlias{Ident: "owned_by"},
},
&dal.Attribute{
Ident: "Name",
Type: &dal.TypeText{Length: 512},
Store: &dal.CodecAlias{Ident: "name"},
},
&dal.Attribute{
Ident: "Active",
Type: &dal.TypeBoolean{},
Store: &dal.CodecAlias{Ident: "active"},
},
},
Indexes: dal.IndexSet{
&dal.Index{
Ident: "PRIMARY",
Type: "BTREE",
Fields: []*dal.IndexField{
{
AttributeIdent: "Kind",
},
{
AttributeIdent: "ResourceID",
},
{
AttributeIdent: "OwnedBy",
},
{
AttributeIdent: "Name",
},
},
},
&dal.Index{
Ident: "flags_uniqueKindResOwnerName",
Type: "BTREE",
Unique: true,
Fields: []*dal.IndexField{
{
AttributeIdent: "Kind",
},
{
AttributeIdent: "ResourceID",
},
{
AttributeIdent: "OwnedBy",
},
{
AttributeIdent: "Name",
Modifiers: []dal.IndexFieldModifier{"LOWERCASE"},
},
},
},
},
}
var Label = &dal.Model{
Ident: "labels",
ResourceType: labelstype.LabelResourceType,
Attributes: dal.AttributeSet{
&dal.Attribute{
Ident: "Kind",
Type: &dal.TypeText{Length: 64},
Store: &dal.CodecAlias{Ident: "kind"},
},
&dal.Attribute{
Ident: "ResourceID",
Type: &dal.TypeID{},
Store: &dal.CodecAlias{Ident: "rel_resource"},
},
&dal.Attribute{
Ident: "Name",
Type: &dal.TypeText{Length: 512},
Store: &dal.CodecAlias{Ident: "name"},
},
&dal.Attribute{
Ident: "Value",
Type: &dal.TypeJSON{},
Store: &dal.CodecAlias{Ident: "value"},
},
},
Indexes: dal.IndexSet{
&dal.Index{
Ident: "PRIMARY",
Type: "BTREE",
Fields: []*dal.IndexField{
{
AttributeIdent: "Kind",
},
{
AttributeIdent: "ResourceID",
},
{
AttributeIdent: "Name",
},
},
},
&dal.Index{
Ident: "labels_uniqueKindResName",
Type: "BTREE",
Unique: true,
Fields: []*dal.IndexField{
{
AttributeIdent: "Kind",
},
{
AttributeIdent: "ResourceID",
},
{
AttributeIdent: "Name",
Modifiers: []dal.IndexFieldModifier{"LOWERCASE"},
},
},
},
},
}
var ResourceActivity = &dal.Model{
Ident: "resource_activity_log",
ResourceType: discoverytype.ResourceActivityResourceType,
Attributes: dal.AttributeSet{
&dal.Attribute{
Ident: "ID",
Type: &dal.TypeID{},
Store: &dal.CodecAlias{Ident: "id"},
},
&dal.Attribute{
Ident: "Timestamp", Sortable: true,
Type: &dal.TypeTimestamp{Timezone: true, Precision: -1},
Store: &dal.CodecAlias{Ident: "ts"},
},
&dal.Attribute{
Ident: "ResourceType",
Type: &dal.TypeText{},
Store: &dal.CodecAlias{Ident: "resource_type"},
},
&dal.Attribute{
Ident: "ResourceAction",
Type: &dal.TypeText{},
Store: &dal.CodecAlias{Ident: "resource_action"},
},
&dal.Attribute{
Ident: "ResourceID",
Type: &dal.TypeID{},
Store: &dal.CodecAlias{Ident: "rel_resource"},
},
&dal.Attribute{
Ident: "Meta",
Type: &dal.TypeJSON{
DefaultValue: "{}",
},
Store: &dal.CodecAlias{Ident: "meta"},
},
},
Indexes: dal.IndexSet{
&dal.Index{
Ident: "PRIMARY",
Type: "BTREE",
Fields: []*dal.IndexField{
{
AttributeIdent: "ID",
},
},
},
&dal.Index{
Ident: "resource_activity_log_resource",
Type: "BTREE",
Fields: []*dal.IndexField{
{
AttributeIdent: "ResourceID",
},
},
},
},
}
var Rule = &dal.Model{
Ident: "rbac_rules",
ResourceType: rbactype.RuleResourceType,
Attributes: dal.AttributeSet{
&dal.Attribute{
Ident: "RoleID",
Type: &dal.TypeRef{
RefAttribute: "id",
RefModel: &dal.ModelRef{
ResourceType: "corteza::system:role",
},
},
Store: &dal.CodecAlias{Ident: "rel_role"},
},
&dal.Attribute{
Ident: "Resource",
Type: &dal.TypeText{Length: 512},
Store: &dal.CodecAlias{Ident: "resource"},
},
&dal.Attribute{
Ident: "Operation",
Type: &dal.TypeText{Length: 50},
Store: &dal.CodecAlias{Ident: "operation"},
},
&dal.Attribute{
Ident: "Access",
Type: &dal.TypeNumber{Precision: -1, Scale: -1, Meta: map[string]interface{}{"rdbms:type": "integer"}},
Store: &dal.CodecAlias{Ident: "access"},
},
},
Indexes: dal.IndexSet{
&dal.Index{
Ident: "PRIMARY",
Type: "BTREE",
Fields: []*dal.IndexField{
{
AttributeIdent: "RoleID",
},
{
AttributeIdent: "Resource",
},
{
AttributeIdent: "Operation",
},
},
},
},
}
func init() {
models = append(
models,
Action,
Flag,
Label,
ResourceActivity,
Rule,
)
}

26
server/system/model/init.gen.go generated Normal file
View File

@@ -0,0 +1,26 @@
package model
// This file is auto-generated.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
//
import (
"context"
"github.com/cortezaproject/corteza/server/pkg/dal"
)
type (
modelReplacer interface {
ReplaceModel(ctx context.Context, model *dal.Model) (err error)
}
)
var (
models []*dal.Model
)
func Models() dal.ModelSet {
return models
}

2908
server/system/model/models.gen.go generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,65 @@
package system
import (
"github.com/cortezaproject/corteza/server/codegen/schema"
)
notification: {
features: {
labels: false
}
model: {
omitGetterSetter: true
attributes: {
id: schema.IdField
kind: {
sortable: true
goType: "types.NotificationKind"
dal: { type: "Text", length: 32 }
}
config: {
goType: "types.NotificationConfig"
dal: { type: "JSON", defaultEmptyObject: true }
}
recipient: schema.AttributeUserRef
created_by: schema.AttributeUserRef
read_at: schema.SortableTimestampNilField
created_at: schema.SortableTimestampNowField
updated_at: schema.SortableTimestampNilField
deleted_at: schema.SortableTimestampNilField
}
indexes: {
"primary": { attribute: "id" }
"recipient": { attribute: "recipient" }
"kind": { attribute: "kind" }
}
}
envoy: {
omit: true
}
filter: {
struct: {
notification_id: {goType: "[]uint64", ident: "notificationID", storeIdent: "id"}
kind: {goType: "[]types.NotificationKind"}
recipient: {goType: "uint64"}
read: {goType: "filter.State", storeIdent: "read_at"}
deleted: {goType: "filter.State", storeIdent: "deleted_at"}
}
byValue: ["notification_id", "recipient"]
byNilState: ["read", "deleted"]
}
store: {
api: {
lookups: [
{ fields: ["id"] }
]
}
}
}

100
server/system/queue.cue Normal file
View File

@@ -0,0 +1,100 @@
package system
import (
"github.com/cortezaproject/corteza/server/codegen/schema"
)
queue: {
features: {
labels: false
}
model: {
ident: "queue_settings"
attributes: {
id: schema.IdField
consumer: {
sortable: true,
goType: "string"
dal: {}
}
queue: {
sortable: true,
goType: "string"
dal: {}
envoy: {
identifier: true
}
}
meta: {
goType: "types.QueueMeta"
dal: { type: "JSON", defaultEmptyObject: true }
omitSetter: true
omitGetter: true
}
created_at: schema.SortableTimestampNowField
updated_at: schema.SortableTimestampNilField
deleted_at: schema.SortableTimestampNilField
created_by: schema.AttributeUserRef
updated_by: schema.AttributeUserRef
deleted_by: schema.AttributeUserRef
}
indexes: {
"primary": { attribute: "id" }
}
}
filter: {
struct: {
queue_id: {goType: "[]uint64", ident: "queueID", storeIdent: "id"}
query: {goType: "string"}
deleted: {goType: "filter.State", storeIdent: "deleted_at"}
}
query: ["queue", "consumer"]
byValue: ["queue_id"]
byNilState: ["deleted"]
}
envoy: {
yaml: {
supportMappedInput: true
mappedField: "Queue"
identKeyAlias: []
}
store: {
handleField: ""
}
}
rbac: {
operations: {
"read": description: "Read queue"
"update": description: "Update queue"
"delete": description: "Delete queue"
"queue.read": description: "Read from queue"
"queue.write": description: "Write to queue"
}
}
store: {
api: {
lookups: [
{
fields: ["id"]
description: """
searches for queue by ID
"""
}, {
fields: ["queue"]
description: """
searches for queue by queue name
"""
},
]
}
}
}

View File

@@ -0,0 +1,54 @@
package system
import (
"github.com/cortezaproject/corteza/server/codegen/schema"
)
queue_message: {
features: {
labels: false
checkFn: false
}
model: {
omitGetterSetter: true
attributes: {
id: schema.IdField
queue: {
sortable: true
dal: {}
}
payload: {
goType: "[]byte"
dal: { type: "Blob" }
}
created: schema.SortableTimestampNilField
processed: schema.SortableTimestampNilField
}
indexes: {
"primary": { attribute: "id" }
}
}
envoy: {
omit: true
}
filter: {
struct: {
queue: {}
processed: {goType: "filter.State", storeIdent: "processed"}
}
byValue: ["queue"]
byNilState: ["processed"]
}
store: {
api: {
lookups: []
}
}
}

View File

@@ -0,0 +1,74 @@
package system
import (
"github.com/cortezaproject/corteza/server/codegen/schema"
)
reminder: {
features: {
labels: false
}
model: {
omitGetterSetter: true
attributes: {
id: schema.IdField
resource: {
sortable: true
dal: { type: "Text", length: 512 }
}
payload: {
goType: "rawJson"
dal: { type: "JSON", defaultEmptyObject: true }
}
snooze_count: {
goType: "uint"
dal: { type: "Number", meta: { "rdbms:type": "integer" } }
}
assigned_to: schema.AttributeUserRef
assigned_by: schema.AttributeUserRef
assigned_at: schema.SortableTimestampField
dismissed_by: schema.AttributeUserRef
dismissed_at: schema.SortableTimestampNilField
remind_at: schema.SortableTimestampNilField
created_at: schema.SortableTimestampNowField
updated_at: schema.SortableTimestampNilField
deleted_at: schema.SortableTimestampNilField
}
indexes: {
"primary": { attribute: "id" }
"assigned_to": { attribute: "assigned_to" }
"resource": { attribute: "resource" }
}
}
envoy: {
omit: true
}
filter: {
struct: {
reminder_id: {goType: "[]uint64", ident: "reminderID", storeIdent: "id"}
resource: {}
assigned_to: {goType: "uint64"}
scheduled_from: {goType: "uint64"}
scheduled_until: {goType: "uint64"}
exclude_dismissed: { goType: "bool" }
include_deleted: { goType: "bool" }
scheduled_only: { goType: "bool" }
deleted: {goType: "filter.State", storeIdent: "deleted_at"}
}
byValue: ["reminder_id", "assigned_to"]
}
store: {
api: {
lookups: [
{ fields: ["id"] }
]
}
}
}

View File

@@ -0,0 +1,9 @@
package renderer
import "github.com/cortezaproject/corteza/server/pkg/valuestore"
func envGetter() func(k string) any {
return func(k string) any {
return valuestore.Global().Env(k)
}
}

View File

@@ -0,0 +1,108 @@
package renderer
import (
"bytes"
"context"
"io"
"github.com/cortezaproject/corteza/server/system/types"
)
type (
genericHTML struct {
def DriverDefinition
}
genericHTMLDriver struct{}
)
func newGenericHTML() driverFactory {
return &genericHTML{
def: DriverDefinition{
Name: "genericHTML",
InputTypes: []types.DocumentType{
types.DocumentTypePlain,
types.DocumentTypeHTML,
},
OutputTypes: []types.DocumentType{
types.DocumentTypeHTML,
},
},
}
}
func (d *genericHTML) Define() DriverDefinition {
return d.def
}
func (d *genericHTML) CanRender(t types.DocumentType) bool {
for _, i := range d.def.InputTypes {
if i == t {
return true
}
}
return false
}
func (d *genericHTML) CanProduce(t types.DocumentType) bool {
for _, o := range d.def.OutputTypes {
if o == t {
return true
}
}
return false
}
func (d *genericHTML) Driver() driver {
return &genericHTMLDriver{}
}
func (d *genericHTMLDriver) Render(ctx context.Context, pl *driverPayload) (io.ReadSeeker, error) {
if pl.Header == nil && pl.Footer == nil {
t, err := preprocHTMLTemplate(pl)
if err != nil {
return nil, err
}
dd := &bytes.Buffer{}
err = t.Execute(dd, pl.Variables)
return bytes.NewReader(dd.Bytes()), err
}
// Header and footer are rendered with the same variables and joined with
// the content into a single document; partials are one-shot readers, so
// they are buffered to let each of the three templates parse its own copy
partials := make(map[string][]byte, len(pl.Partials))
for h, r := range pl.Partials {
bb, err := io.ReadAll(r)
if err != nil {
return nil, err
}
partials[h] = bb
}
dd := &bytes.Buffer{}
for _, src := range []io.Reader{pl.Header, pl.Template, pl.Footer} {
if src == nil {
continue
}
aux := *pl
aux.Template = src
aux.Partials = make(map[string]io.Reader, len(partials))
for h, bb := range partials {
aux.Partials[h] = bytes.NewReader(bb)
}
t, err := preprocHTMLTemplate(&aux)
if err != nil {
return nil, err
}
if err = t.Execute(dd, aux.Variables); err != nil {
return nil, err
}
}
return bytes.NewReader(dd.Bytes()), nil
}

View File

@@ -0,0 +1,103 @@
package renderer
import (
"bytes"
"context"
"io"
"strings"
"testing"
)
func TestGenericHTMLHeaderFooter(t *testing.T) {
d := &genericHTMLDriver{}
pl := &driverPayload{
Template: bytes.NewBufferString(`<main>{{template "pp" .}}</main>`),
Header: bytes.NewBufferString(`<header>{{.name}} {{template "pp" .}}</header>`),
Footer: bytes.NewBufferString(`<footer>{{.name}}</footer>`),
Variables: map[string]interface{}{"name": "Human"},
Partials: map[string]io.Reader{
"pp": bytes.NewBufferString(`{{define "pp"}}partial:{{.name}}{{end}}`),
},
}
out, err := d.Render(context.Background(), pl)
if err != nil {
t.Fatal(err)
}
bb, _ := io.ReadAll(out)
got := string(bb)
want := `<header>Human partial:Human</header><main>partial:Human</main><footer>Human</footer>`
if got != want {
t.Fatalf("got %q, want %q", got, want)
}
// no header/footer keeps the single-pass path working
pl2 := &driverPayload{
Template: bytes.NewBufferString(`<main>ok</main>`),
Variables: map[string]interface{}{},
}
out2, err := d.Render(context.Background(), pl2)
if err != nil {
t.Fatal(err)
}
bb2, _ := io.ReadAll(out2)
if !strings.Contains(string(bb2), "<main>ok</main>") {
t.Fatalf("plain render broken: %q", string(bb2))
}
}
func TestGenericHTMLSafeHTML(t *testing.T) {
d := &genericHTMLDriver{}
rte := `<h1>Title 1</h1>`
pl := &driverPayload{
Template: bytes.NewBufferString(`<div>{{.body}}</div><div>{{.body | safeHTML}}</div>`),
Variables: map[string]interface{}{"body": rte},
}
out, err := d.Render(context.Background(), pl)
if err != nil {
t.Fatal(err)
}
bb, _ := io.ReadAll(out)
got := string(bb)
want := `<div>&lt;h1&gt;Title 1&lt;/h1&gt;</div><div><h1>Title 1</h1></div>`
if got != want {
t.Fatalf("got %q, want %q", got, want)
}
// the case the partial workaround could not cover: rich text in a list
pl2 := &driverPayload{
Template: bytes.NewBufferString(`{{range .items}}<td>{{.desc | safeHTML}}</td>{{end}}`),
Variables: map[string]interface{}{"items": []map[string]any{{"desc": rte}, {"desc": rte}}},
}
out2, err := d.Render(context.Background(), pl2)
if err != nil {
t.Fatal(err)
}
bb2, _ := io.ReadAll(out2)
if got, want := string(bb2), `<td><h1>Title 1</h1></td><td><h1>Title 1</h1></td>`; got != want {
t.Fatalf("got %q, want %q", got, want)
}
// an unset or empty value must not break the render of the whole document
pl3 := &driverPayload{
Template: bytes.NewBufferString(`<td>{{.unset | safeHTML}}</td><td>{{.blank | safeHTML}}</td><td>{{.count | safeHTML}}</td>`),
Variables: map[string]interface{}{"blank": nil, "count": 3},
}
out3, err := d.Render(context.Background(), pl3)
if err != nil {
t.Fatal(err)
}
bb3, _ := io.ReadAll(out3)
if got, want := string(bb3), `<td></td><td></td><td>3</td>`; got != want {
t.Fatalf("got %q, want %q", got, want)
}
}

View File

@@ -0,0 +1,74 @@
package renderer
import (
"bytes"
"context"
"io"
"regexp"
"github.com/cortezaproject/corteza/server/system/types"
)
type (
genericText struct {
def DriverDefinition
}
genericTextDriver struct{}
)
var (
plainTextRegex = regexp.MustCompile("text(/)?.*")
)
func newGenericText() driverFactory {
return &genericText{
def: DriverDefinition{
Name: "genericText",
InputTypes: []types.DocumentType{
types.DocumentTypePlain,
types.DocumentTypeHTML,
},
OutputTypes: []types.DocumentType{
types.DocumentTypePlain,
},
},
}
}
func (d *genericText) Define() DriverDefinition {
return d.def
}
func (d *genericText) CanRender(t types.DocumentType) bool {
for _, i := range d.def.InputTypes {
if i == t {
return true
}
}
return false
}
func (d *genericText) CanProduce(t types.DocumentType) bool {
for _, o := range d.def.OutputTypes {
if o == t {
return true
}
}
return false
}
func (d *genericText) Driver() driver {
return &genericTextDriver{}
}
func (d *genericTextDriver) Render(ctx context.Context, pl *driverPayload) (io.ReadSeeker, error) {
t, err := preprocPlainTemplate(pl.Template, pl.Partials)
if err != nil {
return nil, err
}
dd := &bytes.Buffer{}
err = t.Execute(dd, pl.Variables)
return bytes.NewReader(dd.Bytes()), err
}

View File

@@ -0,0 +1,387 @@
package renderer
import (
"bytes"
"context"
"fmt"
"io"
"mime/multipart"
"net/http"
"strings"
"sync"
"github.com/cortezaproject/corteza/server/system/types"
)
type (
gotenbergPDF struct {
url string
def DriverDefinition
}
gotenbergPDFDriver struct {
url string
}
)
// Gotenberg 7+ serves HTML conversion on the chromium module route, older (v6)
// deployments only expose /convert/html; the working route is detected on
// first render and cached per base URL
var gotenbergEndpoints = sync.Map{}
const (
gotenbergModernEndpoint = "/forms/chromium/convert/html"
gotenbergLegacyEndpoint = "/convert/html"
)
// @todo healthcheck, different input data formats
func newGotenbergPDF(url string) driverFactory {
return &gotenbergPDF{
url: url,
def: DriverDefinition{
Name: "gotenbergPDF",
InputTypes: []types.DocumentType{
types.DocumentTypePlain,
types.DocumentTypeHTML,
},
OutputTypes: []types.DocumentType{
types.DocumentTypePDF,
},
},
}
}
func (d *gotenbergPDF) Define() DriverDefinition {
return d.def
}
func (d *gotenbergPDF) CanRender(t types.DocumentType) bool {
for _, i := range d.def.InputTypes {
if i == t {
return true
}
}
return false
}
func (d *gotenbergPDF) CanProduce(t types.DocumentType) bool {
for _, o := range d.def.OutputTypes {
if o == t {
return true
}
}
return false
}
func (d *gotenbergPDF) Driver() driver {
return &gotenbergPDFDriver{
url: d.url,
}
}
func (d *gotenbergPDFDriver) Render(ctx context.Context, pl *driverPayload) (io.ReadSeeker, error) {
// HTTP request body stuff
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
// index.html is required by the rendering container; Gotenberg 7+ requires
// the "files" form field name, v6 accepts any name
part, err := writer.CreateFormFile("files", "index.html")
if err != nil {
return nil, err
}
err = d.prepareContent(part, pl)
if err != nil {
return nil, err
}
// Optional header/footer documents rendered with the same variables/partials
if pl.Header != nil {
hPart, err := writer.CreateFormFile("files", "header.html")
if err != nil {
return nil, err
}
if err = d.prepareAux(hPart, pl, pl.Header); err != nil {
return nil, err
}
}
if pl.Footer != nil {
fPart, err := writer.CreateFormFile("files", "footer.html")
if err != nil {
return nil, err
}
if err = d.prepareAux(fPart, pl, pl.Footer); err != nil {
return nil, err
}
}
// Document configurations
err = d.applyOptions(writer, pl.Options)
if err != nil {
return nil, err
}
err = writer.Close()
if err != nil {
return nil, err
}
// HTTP request header stuff
// @todo make sure to use the propper endpoint when you add support
// for different inputs.
url := d.url
if n, has := pl.Options["url"]; has {
url = n
}
endpoints := []string{gotenbergModernEndpoint, gotenbergLegacyEndpoint}
if e, has := gotenbergEndpoints.Load(url); has && e == gotenbergLegacyEndpoint {
endpoints = []string{gotenbergLegacyEndpoint, gotenbergModernEndpoint}
}
var resp *http.Response
for _, e := range endpoints {
req, err := http.NewRequestWithContext(ctx, "POST", url+e, bytes.NewReader(body.Bytes()))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", writer.FormDataContentType())
resp, err = http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
// 404 means the route doesn't exist on this Gotenberg version; try the other one
if resp.StatusCode == http.StatusNotFound {
resp.Body.Close()
resp = nil
continue
}
gotenbergEndpoints.Store(url, e)
break
}
if resp == nil {
return nil, fmt.Errorf("gotenberg render failed: no compatible endpoint found on %s", url)
}
defer resp.Body.Close()
ss, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("gotenberg render failed: %s: %s", resp.Status, strings.TrimSpace(string(ss)))
}
return bytes.NewReader(ss), nil
}
func (d *gotenbergPDFDriver) prepareContent(w io.Writer, pl *driverPayload) error {
t, err := preprocHTMLTemplate(pl)
if err != nil {
return err
}
return t.Execute(w, pl.Variables)
}
// prepareAux renders an auxiliary document (header/footer) with pl's variables
func (d *gotenbergPDFDriver) prepareAux(w io.Writer, pl *driverPayload, src io.Reader) error {
aux := *pl
aux.Template = src
return d.prepareContent(w, &aux)
}
func (d *gotenbergPDFDriver) applyOptions(mw *multipart.Writer, opts map[string]string) (err error) {
if opts == nil {
return nil
}
for k, v := range opts {
switch k {
case "marginTop",
"marginBottom",
"marginLeft",
"marginRight":
err = d.addFormField(mw, k, v)
case "marginY":
err = d.addFormField(mw, "marginTop", v)
if err != nil {
return err
}
err = d.addFormField(mw, "marginBottom", v)
case "marginX":
err = d.addFormField(mw, "marginLeft", v)
if err != nil {
return err
}
err = d.addFormField(mw, "marginRight", v)
case "margin":
err = d.addFormField(mw, "marginTop", v)
if err != nil {
return err
}
err = d.addFormField(mw, "marginBottom", v)
if err != nil {
return err
}
err = d.addFormField(mw, "marginLeft", v)
if err != nil {
return err
}
err = d.addFormField(mw, "marginRight", v)
case "documentSize":
w, h := d.documentDimensions(v)
if w+h != "" {
err = d.addFormField(mw, "paperWidth", w)
if err != nil {
return err
}
err = d.addFormField(mw, "paperHeight", h)
}
case "documentWidth":
err = d.addFormField(mw, "paperWidth", v)
case "documentHeight":
err = d.addFormField(mw, "paperHeight", v)
case "contentScale":
err = d.addFormField(mw, "scale", v)
case "orientation":
if v == "landscape" {
err = d.addFormField(mw, "landscape", "true")
} else {
err = d.addFormField(mw, "landscape", "false")
}
}
if err != nil {
return err
}
}
return nil
}
func (d *gotenbergPDFDriver) addFormField(mw *multipart.Writer, k, v string) error {
w, err := mw.CreateFormField(k)
if err != nil {
return err
}
_, err = w.Write([]byte(v))
return err
}
// documentDimensions returns the ISO216 standard document dimensions in inches (Gotenberg uses inches)
func (d *gotenbergPDFDriver) documentDimensions(isoDoc string) (string, string) {
switch strings.ToLower(isoDoc) {
// A series
case "a0":
return "33.1", "46.8"
case "a1":
return "23.4", "33.1"
case "a2":
return "16.5", "23.4"
case "a3":
return "11.7", "16.5"
case "a4":
return "8.3", "11.7"
case "a5":
return "5.8", "8.3"
case "a6":
return "4.1", "5.8"
case "a7":
return "2.9", "4.1"
case "a8":
return "2.0", "2.9"
case "a9":
return "1.5", "2.0"
case "a10":
return "1.0", "1.5"
// B series
case "b0":
return "39.4", "55.7"
case "b1":
return "27.8", "39.4"
case "b2":
return "19.7", "27.8"
case "b3":
return "13.9", "19.7"
case "b4":
return "9.8", "13.9"
case "b5":
return "6.9", "9.8"
case "b6":
return "4.9", "6.9"
case "b7":
return "3.5", "4.9"
case "b8":
return "2.4", "3.5"
case "b9":
return "1.7", "2.4"
case "b10":
return "1.2", "1.7"
// C series
case "c0":
return "36.1", "51.1"
case "c1":
return "25.5", "36.1"
case "c2":
return "18.0", "25.5"
case "c3":
return "12.8", "18.0"
case "c4":
return "9.0", "12.8"
case "c5":
return "6.4", "9.0"
case "c6":
return "4.5", "6.4"
case "c7":
return "3.2", "4.5"
case "c8":
return "2.2", "3.2"
case "c9":
return "1.6", "2.2"
case "c10":
return "1.1", "1.6"
// ANSI
case "ansi a":
return "8.5", "11"
case "ansi b":
return "11", "17"
case "ansi c":
return "17", "22"
case "ansi d":
return "22", "34"
case "ansi e":
return "34", "44"
// Proprietary NA
case "junior legal":
return "8", "5"
case "letter":
return "8.5", "11"
case "legal":
return "8.5", "14"
case "tabloid":
return "11", "17"
}
// This will fallback to the default (A4)
return "", ""
}

View File

@@ -0,0 +1,87 @@
package renderer
import (
"encoding/base64"
"fmt"
"html/template"
"io/ioutil"
"net/http"
"github.com/Masterminds/sprig"
)
func preprocHTMLTemplate(pl *driverPayload) (*template.Template, error) {
bb, err := ioutil.ReadAll(pl.Template)
if err != nil {
return nil, err
}
gtpl := template.New("text/html_render").
Funcs(sprig.FuncMap()).
Funcs(template.FuncMap{
// "attachDataURL": func(name string) template.URL {
// // Find the attachment
// att, has := pl.Attachments[name]
// if !has {
// return template.URL(fmt.Sprintf("error: attachment not found: %s", name))
// }
// // Process source
// bb, err := ioutil.ReadAll(att.Source)
// if err != nil {
// return template.URL(fmt.Sprintf("error: %s", err.Error()))
// }
// return template.URL("data:" + att.Mime + ";base64," + base64.RawStdEncoding.EncodeToString(bb))
// },
"inlineRemote": func(url string) (template.URL, error) {
rsp, err := http.Get(url)
if err != nil {
return "", err
}
defer rsp.Body.Close()
bb, err := ioutil.ReadAll(rsp.Body)
if err != nil {
return "", err
}
raw := base64.RawStdEncoding.EncodeToString(bb)
return template.URL("data:" + rsp.Header.Get("Content-Type") + ";base64," + raw), nil
},
// emits the value as-is, without the contextual escaping
// html/template applies to every other interpolated value;
// unset values render empty, the same as plain interpolation
"safeHTML": func(v any) template.HTML {
if v == nil {
return ""
}
return template.HTML(fmt.Sprintf("%v", v))
},
"env": envGetter(),
})
// Prep the original template
t, err := gtpl.Parse(string(bb))
if err != nil {
return nil, err
}
// Prep partials
for _, p := range pl.Partials {
bb, err = ioutil.ReadAll(p)
if err != nil {
return nil, err
}
t, err = gtpl.Parse(string(bb))
if err != nil {
return nil, err
}
}
return t, nil
}

View File

@@ -0,0 +1,59 @@
package renderer
import (
"context"
"fmt"
"io"
"github.com/cortezaproject/corteza/server/pkg/options"
)
type (
renderer struct {
factories []driverFactory
}
)
func Renderer(cfg options.TemplateOpt) *renderer {
ff := make([]driverFactory, 0, 3)
ff = append(ff, newGenericText(), newGenericHTML())
if cfg.RendererGotenbergEnabled {
ff = append(ff, newGotenbergPDF(cfg.RendererGotenbergAddress))
}
return &renderer{
factories: ff,
}
}
func (r *renderer) Render(ctx context.Context, pl *RendererPayload) (io.ReadSeeker, error) {
for _, f := range r.factories {
if f.CanRender(pl.TemplateType) && f.CanProduce(pl.TargetType) {
pp := make(map[string]io.Reader)
for _, prt := range pl.Partials {
pp[prt.Handle] = prt.Template
}
dpl := &driverPayload{
Template: pl.Template,
Variables: pl.Variables,
Options: pl.Options,
Partials: pp,
Attachments: pl.Attachments,
Header: pl.Header,
Footer: pl.Footer,
}
return f.Driver().Render(ctx, dpl)
}
}
return nil, fmt.Errorf("rendering failed: driver not found")
}
func (r *renderer) Drivers() []DriverDefinition {
dd := make([]DriverDefinition, len(r.factories))
for i, f := range r.factories {
dd[i] = f.Define()
}
return dd
}

View File

@@ -0,0 +1,43 @@
package renderer
import (
"io"
"io/ioutil"
"text/template"
"github.com/Masterminds/sprig"
)
func preprocPlainTemplate(tpl io.Reader, pp map[string]io.Reader) (*template.Template, error) {
bb, err := ioutil.ReadAll(tpl)
if err != nil {
return nil, err
}
gtpl := template.New("text/plain_render").
Funcs(sprig.TxtFuncMap()).
Funcs(template.FuncMap{
"env": envGetter(),
})
// Prep the original template
t, err := gtpl.Parse(string(bb))
if err != nil {
return nil, err
}
// Prep partials
for _, p := range pp {
bb, err = ioutil.ReadAll(p)
if err != nil {
return nil, err
}
t, err = gtpl.Parse(string(bb))
if err != nil {
return nil, err
}
}
return t, nil
}

View File

@@ -0,0 +1,65 @@
package renderer
import (
"context"
"io"
"github.com/cortezaproject/corteza/server/system/types"
)
type (
RendererPayload struct {
Template io.Reader
TemplateType types.DocumentType
TargetType types.DocumentType
Variables map[string]interface{}
Options map[string]string
Partials []*TemplatePartial
Attachments AttachmentIndex
// Optional header/footer documents, attached by drivers that support them
Header io.Reader
Footer io.Reader
}
TemplatePartial struct {
Handle string
Template io.Reader
TemplateType types.DocumentType
}
driverPayload struct {
Template io.Reader
Variables map[string]interface{}
Options map[string]string
Partials map[string]io.Reader
Attachments AttachmentIndex
Header io.Reader
Footer io.Reader
}
AttachmentIndex map[string]*Attachment
Attachment struct {
Source io.Reader
Mime string
Name string
}
DriverDefinition struct {
Name string `json:"name"`
InputTypes []types.DocumentType `json:"inputTypes"`
OutputTypes []types.DocumentType `json:"outputTypes"`
}
driverFactory interface {
Define() DriverDefinition
CanRender(t types.DocumentType) bool
CanProduce(t types.DocumentType) bool
Driver() driver
}
driver interface {
Render(ctx context.Context, pl *driverPayload) (io.ReadSeeker, error)
}
)

111
server/system/report.cue Normal file
View File

@@ -0,0 +1,111 @@
package system
import (
"github.com/cortezaproject/corteza/server/codegen/schema"
)
report: {
model: {
attributes: {
id: schema.IdField
handle: schema.HandleField
meta: {
goType: "*types.ReportMeta"
dal: { type: "JSON", defaultEmptyObject: true }
omitSetter: true
omitGetter: true
}
scenarios: {
goType: "types.ReportScenarioSet"
dal: { type: "JSON", defaultEmptyObject: true }
omitSetter: true
omitGetter: true
}
sources: {
goType: "types.ReportDataSourceSet"
dal: { type: "JSON", defaultEmptyObject: true }
omitSetter: true
omitGetter: true
}
blocks: {
goType: "types.ReportBlockSet"
dal: { type: "JSON", defaultEmptyObject: true }
omitSetter: true
omitGetter: true
}
owned_by: schema.AttributeUserRef
created_at: schema.SortableTimestampNowField
updated_at: schema.SortableTimestampNilField
deleted_at: schema.SortableTimestampNilField
created_by: schema.AttributeUserRef
updated_by: schema.AttributeUserRef
deleted_by: schema.AttributeUserRef
}
indexes: {
"primary": { attribute: "id" }
}
}
envoy: {
yaml: {
supportMappedInput: true
mappedField: "Handle"
identKeyAlias: ["reports"]
}
store: {}
}
filter: {
struct: {
report_id: {goType: "[]uint64", storeIdent: "id", ident: "reportID" }
handle: {goType: "string"}
deleted: {goType: "filter.State", storeIdent: "deleted_at"}
}
query: ["handle"]
byValue: ["handle", "report_id"]
byNilState: ["deleted"]
}
rbac: {
operations: {
read: description: "Read report"
update: description: "Update report"
delete: description: "Delete report"
run: description: "Run report"
}
}
store: {
api: {
lookups: [
{
fields: ["id"]
description: """
searches for report by ID
It returns report even if deleted
"""
}, {
fields: ["handle"]
nullConstraint: ["deleted_at"]
constraintCheck: true
description: """
searches for report by handle
It returns report if deleted
"""
},
]
}
}
// locale:
// extended: true
// keys:
// - { path: name, field: "Meta.Name" }
// - { path: description, field: "Meta.Description" }
// - { name: block title, path: "block.{{blockID}}.title", custom: true }
// - { name: block description, path: "block.{{blockID}}.description", custom: true }
}

View File

@@ -0,0 +1,139 @@
package reporting
import (
"fmt"
"strings"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/system/types"
)
type (
Frame struct {
Name string `json:"name"`
Source string `json:"source"`
Ref string `json:"ref,omitempty"`
// RefValue is the common value between the two datasources
RefValue string `json:"refValue,omitempty"`
// RelColumn is what column in the local ds was used
RelColumn string `json:"relColumn,omitempty"`
// RelSource is the ds that this frame is related to
RelSource string `json:"relSource,omitempty"`
Columns FrameColumnSet `json:"columns"`
Rows []FrameRow `json:"rows"`
Paging *filter.Paging `json:"paging"`
Sort filter.SortExprSet `json:"sort"`
Filter *types.ReportFilterExpr `json:"filter"`
}
FrameDescription struct {
Source string `json:"source"`
Ref string `json:"ref,omitempty"`
Columns FrameColumnSet `json:"columns"`
}
FrameRow []string
FrameColumnSet []FrameColumn
FrameColumn struct {
Name string `json:"name"`
Label string `json:"label"`
Kind string `json:"kind"`
Primary bool `json:"primary"`
Unique bool `json:"unique"`
System bool `json:"system"`
Multivalue bool `json:"multivalue"`
MultivalueDelimiter string `json:"multivalueDelimiter"`
}
FrameDefinitionSet []*FrameDefinition
FrameDefinition struct {
Name string `json:"name"`
Source string `json:"source"`
Ref string `json:"ref"`
Columns FrameColumnSet `json:"columns"`
Filter *types.ReportFilterExpr `json:"filter"`
Paging *filter.Paging `json:"paging"`
Sort filter.SortExprSet `json:"sort"`
}
)
// @todo nicer formatting and alignment
func (f Frame) String() string {
out := fmt.Sprintf("n: %10s; src: %10s\n", f.Name, f.Source)
if f.Ref != "" {
out += fmt.Sprintf("ref: %10s; col: %10s\n; key: %10s\n", f.Ref, f.RelColumn, f.RefValue)
}
if f.RelSource != "" {
out += fmt.Sprintf("Rel source: %10s;\n", f.RelSource)
}
for _, c := range f.Columns {
out += fmt.Sprintf("%s<%s>, ", c.Name, c.Kind)
}
out = strings.TrimRight(out, " ,")
out += "\n"
for i, r := range f.Rows {
out += fmt.Sprintf("%d| %s", i+1, strings.Join(r, ", "))
out += "\n"
}
if f.Paging != nil {
out += "\n"
out += fmt.Sprintf("< %s; =%s; > %s", f.Paging.PrevPage.String(), f.Paging.PageCursor.String(), f.Paging.NextPage.String())
}
if len(f.Sort) > 0 {
out += "\n"
out += f.Sort.String()
}
out += "\n"
out += fmt.Sprintf("ix %d; len %d", 0, len(f.Rows))
return out
}
func (cc FrameColumnSet) String() string {
out := ""
for _, c := range cc {
out += fmt.Sprintf("%s<%s>, ", c.Name, c.Kind)
}
return strings.TrimRight(out, " ,")
}
// OmitSys returns the columns that are not system defined
func (cc FrameColumnSet) OmitSys() FrameColumnSet {
out := make(FrameColumnSet, 0, len(cc))
for _, c := range cc {
if !c.System {
out = append(out, c)
}
}
return out
}
func (r FrameRow) String() string {
return strings.Join(r, ", ")
}
// FilterBySource returns a set of definitions for the requested identifier
func (dd FrameDefinitionSet) FilterBySource(ident string) FrameDefinitionSet {
out := make(FrameDefinitionSet, 0, len(dd))
for _, d := range dd {
if d.Source == ident {
out = append(out, d)
}
}
return out
}

View File

@@ -0,0 +1,141 @@
package reporting
import (
"fmt"
"strings"
"github.com/cortezaproject/corteza/server/pkg/dal"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/modern-go/reflect2"
)
type (
// reportFrameBuilder assist in frame construction from iterators
//
// Primarily simplifies mapping the correct iterator attributes to correct
// frame row columns and having them encoded properly.
reportFrameBuilder struct {
def *FrameDefinition
frame *Frame
ownCol string
attrMapping map[string]int
attrMvDel map[string]string
}
)
// newReportFrameBuilder initializes a new report frame builder
func newReportFrameBuilder(def *FrameDefinition) *reportFrameBuilder {
// Index cols for easier lookups
attrMap := make(map[string]int)
mvDelMap := make(map[string]string)
for i, c := range def.Columns {
attrMap[c.Name] = i
if c.Multivalue {
mvDelMap[c.Name] = c.MultivalueDelimiter
if mvDelMap[c.Name] == "" {
mvDelMap[c.Name] = "\n"
}
}
}
out := &reportFrameBuilder{
def: def,
attrMapping: attrMap,
}
// Init output frame
out.freshFrame()
return out
}
// linked includes additional metadata required by the link step
func (b *reportFrameBuilder) linked(ownCol, relCol, relSrc string) {
b.ownCol = ownCol
b.frame.RelColumn = relCol
b.frame.RelSource = relSrc
}
// addRow adds a new dal.Row to the frame
func (b *reportFrameBuilder) addRow(r *dal.Row) {
rrow := make(FrameRow, len(b.def.Columns))
for k, cc := range r.CountValues() {
ix, ok := b.attrMapping[k]
if !ok {
continue
}
auxRow := make(FrameRow, cc)
for i := uint(0); i < cc; i++ {
val, _ := r.GetValue(k, i)
auxRow[i] = b.stringifyVal(k, val)
}
rrow[ix] = b.joinMultiVal(k, auxRow)
}
b.frame.Rows = append(b.frame.Rows, rrow)
if b.ownCol != "" {
v, _ := r.GetValue(b.ownCol, 0)
b.frame.RefValue = b.stringifyVal(b.ownCol, v)
}
}
// done returns the constructed frame and prepares a new frame with the same
// metadata as the original one
func (b *reportFrameBuilder) done() *Frame {
out := b.frame
b.freshFrame()
return out
}
func (b *reportFrameBuilder) stringifyVal(col string, val any) string {
// Edgecase for when values are nil since the stringer uses <nil> for those
if reflect2.IsNil(val) {
return ""
}
// @todo nicer formatting and such? V1 didn't do much different
return fmt.Sprintf("%v", val)
}
func (b *reportFrameBuilder) joinMultiVal(col string, vals []string) string {
// @todo b.attrMvDel[col] is never actually set so it should be ALONG with this default
del, ok := b.attrMvDel[col]
if !ok {
del = "\n"
}
return strings.Join(vals, del)
}
func (b *reportFrameBuilder) freshFrame() {
// reuse the old frame metadata and clears out the rows
if b.frame != nil {
aux := *b.frame
b.frame = &aux
b.frame.Rows = []FrameRow{}
return
}
b.frame = &Frame{
Name: b.def.Name,
Source: b.def.Source,
Ref: b.def.Ref,
Rows: []FrameRow{},
Columns: b.def.Columns,
Sort: b.def.Sort,
Filter: b.def.Filter,
}
if b.def.Paging != nil {
b.frame.Paging = &filter.Paging{
Limit: b.def.Paging.Limit,
PageCursor: b.def.Paging.PageCursor,
}
}
}

View File

@@ -0,0 +1,741 @@
// Package reporting implements utilities for working with system reports and
// DAL pipelines
//
// The package exists primarily for making the system service codebase smaller
// and prepare for reusability with compose record reports.
package reporting
import (
"context"
"fmt"
"github.com/cortezaproject/corteza/server/pkg/dal"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/system/types"
"github.com/spf13/cast"
)
type (
// run is a wrapper around the pipeline and corresponding
// output frame definitions
run struct {
Pipeline dal.Pipeline
Defs FrameDefinitionSet
}
modelFinder interface {
FindModel(dal.ModelRef) *dal.Model
}
// @todo not a fan of this anymore; consider reworking pkg/dal/service to
// provide a more sequential approach we can use.
dryRunner interface {
modelFinder
Dryrun(context.Context, dal.Pipeline) error
}
)
// Runs create a set of report runs based on step and frame definitions
func Runs(pr modelFinder, steps types.ReportStepSet, defs FrameDefinitionSet) (out []run, err error) {
// Prepare runs based on the provided definitions
//
// - If consecutive definitions point to the same source with the same name
// consider them to fall under the same workload (the link step)
// - else, one def per workload
auxDefs := make(FrameDefinitionSet, 0)
var aux run
for i, def := range defs {
if i == 0 {
auxDefs = append(auxDefs, def)
continue
}
// Definitions fall together
if def.Name == defs[i-1].Name && def.Source == defs[i-1].Source {
auxDefs = append(auxDefs, def)
continue
}
// Make run for the previous definition (exclude current!!)
aux, err = makeRun(pr, steps, auxDefs)
if err != nil {
return
}
out = append(out, aux)
// Prepare next definition batch including the current one
auxDefs = make(FrameDefinitionSet, 0)
auxDefs = append(auxDefs, def)
}
// Handle the ones (potentially) not covered by the above loop
if len(auxDefs) > 0 {
aux, err = makeRun(pr, steps, auxDefs)
if err != nil {
return
}
out = append(out, aux)
}
return
}
// Frames returns a set of Frame for the given workload & iterator combo
func Frames(ctx context.Context, iter dal.Iterator, r run) (ff []*Frame, err error) {
// Preprocessing on the workload's frame definitions; assure all
// columns/metdata are there to avoid nonesense later down the line
updateDefinitionColumns(r)
// @todo perhaps need to change; for now only this scenario introduces multiple
// frame defs per workload
if len(r.Defs) > 1 {
return stepLinkFrames(ctx, iter, r)
}
return stepFrames(ctx, iter, r)
}
// Describe returns a set of frame descriptions based on the given pipeline
func Describe(ctx context.Context, rr dryRunner, ss types.ReportStepSet, sources []string) (out []FrameDescription, err error) {
// Make a run for the whole thing
pp, err := makePipeline(rr, ss, nil)
if err != nil {
return
}
var aux []FrameDescription
for _, src := range sources {
// Use the requested source as root
sub := pp.Slice(src)
err = rr.Dryrun(ctx, sub)
if err != nil {
return
}
s := sub[0]
aux, err = describePipeline(s, src)
if err != nil {
return
}
out = append(out, aux...)
}
return
}
// stepLinkFrames is dedicated for the link step due to it's unique output
func stepLinkFrames(ctx context.Context, iter dal.Iterator, r run) (ff []*Frame, err error) {
defs := r.Defs
// @note this will only be called for the link step so it can freely panic if violated
defLink := r.Pipeline[0].(*dal.Link)
// Unpack frame definitions for the link
defLeft, defRight := unpackLinkDefs(defs, r.Pipeline)
// Init vars to keep track of the progress
// @note true is left, false is right
counters := make(map[bool]uint)
builders := make(map[bool]*reportFrameBuilder)
builders[true] = newReportFrameBuilder(defLeft)
builders[false] = newReportFrameBuilder(defRight)
builders[false].linked(defLink.On.Right, defLink.On.Left, defLink.RelLeft)
limits := make(map[bool]uint)
if defLeft.Paging != nil {
limits[true] = defLeft.Paging.Limit
}
if defRight.Paging != nil {
limits[false] = defRight.Paging.Limit
}
// Helper to determine if we need a next cursor
nextCursor := false
// Helpers for reading iterators
var (
ref string
row = &dal.Row{}
doingF = false
)
for iter.Next(ctx) {
if limits[true] > 0 && counters[true] >= limits[true] {
nextCursor = true
break
}
row.Reset()
_ = iter.Scan(row)
// Determine ref and which vars to use
aux, _ := row.GetValue(dal.LinkRefIdent, 0)
ref = cast.ToString(aux)
if ref == "" {
ref = defLeft.Ref
}
left := ref == defLeft.Ref
// When needed, flush the finished frames to the output
if left && doingF {
ff = append(ff, builders[false].done())
doingF = false
} else if !left {
doingF = true
}
builders[left].addRow(row)
counters[left]++
}
if err = iter.Err(); err != nil {
return
}
// If the loop ended before the limit cut it off, we need to finish the
// last right frame as it wasn't yet in the above loop
if !nextCursor {
if doingF {
ff = append(ff, builders[false].done())
}
}
// Apply paging cursor to the left frame
// @todo consider applying them to the right as well, for now, no
if nextCursor {
if builders[true].frame.Paging == nil {
builders[true].frame.Paging = &filter.Paging{}
}
builders[true].frame.Paging.NextPage, err = iter.ForwardCursor(row)
if err != nil {
return
}
}
// Complete the output with the left frame
// @note the left frame goes to the start and the right frames are in the same order
// as the related rows from the left frame.
return append([]*Frame{builders[true].done()}, ff...), nil
}
// unpackLinkDefs returns the left and the right frame definition disregarding
// the order of the definitions in the input
func unpackLinkDefs(defs FrameDefinitionSet, pp dal.Pipeline) (left, right *FrameDefinition) {
r := pp[0]
l := r.(*dal.Link)
find := func(defs FrameDefinitionSet, ref string) *FrameDefinition {
for _, def := range defs {
if def.Ref == ref {
return def
}
}
return nil
}
return find(defs, l.RelLeft), find(defs, l.RelRight)
}
// stepFrames is a generic iter to Frame converter
func stepFrames(ctx context.Context, iter dal.Iterator, r run) (ff []*Frame, err error) {
defs := r.Defs
// @note only the link step takes multiple defs and that one is not covered
// by this function
if len(defs) != 1 {
panic(fmt.Sprintf("impossible state: expecting one frame definition, got %d", len(defs)))
}
def := defs[0]
// Init vars to keep track of the progress
limit := uint(0)
counter := uint(0)
builder := newReportFrameBuilder(def)
if def.Paging != nil {
limit = def.Paging.Limit
}
// Helper to determine if we need a next cursor
nextCursor := false
// Helpers for reading iterators
row := &dal.Row{}
for iter.Next(ctx) {
if limit > 0 && counter >= limit {
nextCursor = true
break
}
row.Reset()
_ = iter.Scan(row)
builder.addRow(row)
counter++
}
if err = iter.Err(); err != nil {
return
}
// Apply paging cursor to the frame
if nextCursor {
if builder.frame.Paging == nil {
builder.frame.Paging = &filter.Paging{}
}
builder.frame.Paging.NextPage, err = iter.ForwardCursor(row)
if err != nil {
return
}
}
return append(ff, builder.done()), nil
}
func describePipeline(s dal.PipelineStep, src string) (out []FrameDescription, err error) {
aa := s.Attributes()
out = make([]FrameDescription, len(aa))
for i, a := range aa {
out[i].Source = src
out[i].Columns = mappingToFrameCols(a)
}
// @note this case is only possible for the link step; expand when/if needed
if len(out) == 2 {
l := s.(*dal.Link)
out[0].Ref = l.RelLeft
out[1].Ref = l.RelRight
}
return
}
func makeRun(pr modelFinder, ss types.ReportStepSet, defs FrameDefinitionSet) (out run, err error) {
var pp dal.Pipeline
pp, err = makePipeline(pr, ss, defs)
if err != nil {
return
}
out.Defs = defs
out.Pipeline = pp.Slice(defs[0].Source)
return
}
// makePipeline converts the given report steps into the DAL pipeline
func makePipeline(mf modelFinder, ss types.ReportStepSet, defs FrameDefinitionSet) (pp dal.Pipeline, err error) {
for _, step := range ss {
switch {
case step.Load != nil:
aux, err := convStepLoad(mf, *step.Load, defs.FilterBySource(step.Load.Name))
if err != nil {
return nil, err
}
pp = append(pp, aux)
case step.Aggregate != nil:
aux, err := convStepAggregate(*step.Aggregate, defs.FilterBySource(step.Aggregate.Name))
if err != nil {
return nil, err
}
pp = append(pp, aux)
case step.Join != nil:
aux, err := convStepJoin(*step.Join, defs.FilterBySource(step.Join.Name))
if err != nil {
return nil, err
}
pp = append(pp, aux)
case step.Link != nil:
aux, err := convStepLink(*step.Link, defs.FilterBySource(step.Link.Name))
if err != nil {
return nil, err
}
pp = append(pp, aux)
default:
// this should never happen
panic(fmt.Errorf("unknown step type: %v", step.Kind))
}
}
return pp, pp.LinkSteps()
}
// mappingToFrameCols converts pipeline AttributeMapping to FrameColumnSet
func mappingToFrameCols(mm []dal.AttributeMapping) FrameColumnSet {
out := make(FrameColumnSet, 0, len(mm))
for _, m := range mm {
out = append(out, mappingToFrameCol(m))
}
return out
}
// @note current implementation a bit _rushed_ since I'll probably rethink
// how the pipeline handles attributes -- will revisit then.
func mappingToFrameCol(m dal.AttributeMapping) FrameColumn {
p := m.Properties()
const (
// Coppied around to reduce imports
emailLength = 254
urlLength = 2048
attachmentResType = "corteza::system:attachment"
userResType = "corteza::system:user"
moduleResType = "corteza::compose:module"
)
l := m.Properties().Label
if l == "" {
l = m.Identifier()
}
out := FrameColumn{
Name: m.Identifier(),
Label: l,
Kind: "String",
Primary: p.IsPrimary,
System: p.IsSystem,
Multivalue: p.IsMultivalue,
MultivalueDelimiter: p.MultivalueDelimiter,
}
if out.MultivalueDelimiter == "" {
out.MultivalueDelimiter = "\n"
}
switch t := p.Type.(type) {
case *dal.TypeBoolean:
out.Kind = "Boolean"
case *dal.TypeDate, *dal.TypeTime, *dal.TypeTimestamp:
out.Kind = "DateTime"
case *dal.TypeNumber:
out.Kind = "Number"
case *dal.TypeEnum:
out.Kind = "Select"
case *dal.TypeText:
// @note temporary solution; we should push some meta along with it
if t.Length == emailLength {
out.Kind = "Email"
} else if t.Length == urlLength {
out.Kind = "URL"
} else {
out.Kind = "String"
}
case *dal.TypeRef:
switch t.RefModel.ResourceType {
case moduleResType:
out.Kind = "Record"
case userResType:
out.Kind = "User"
case attachmentResType:
out.Kind = "File"
}
}
return out
}
// Report step -> DAL step conversion
// convStepLoad converts ReportStepLoad to dal.Datasource
func convStepLoad(pr modelFinder, step types.ReportStepLoad, defs FrameDefinitionSet) (out *dal.Datasource, err error) {
// Validation
if len(defs) > 1 {
err = fmt.Errorf("cannot convert load step: expecting at most one definition, got %d", len(defs))
return
}
// Get additional filtering
var extf filter.Filter
if len(defs) == 1 {
extf = filterFromDef(defs[0])
}
// Prepare model ref
mfr, err := makeModelRef(step)
if err != nil {
return
}
// @todo refactor this out after we support other resources with potentially missing soft delete fields
f, err := dal.FilterFromExpr(step.Filter.Node()).
MergeFilters(filter.Generic(filter.WithStateConstraint("deletedAt", filter.StateExcluded)))
if err != nil {
return
}
f, err = f.MergeFilters(extf)
if err != nil {
return
}
// Make pipeline step
return &dal.Datasource{
Ident: step.Name,
Filter: f,
ModelRef: mfr,
OutAttributes: modelAttributes(pr, step, mfr),
}, nil
}
// convStepAggregate converts ReportStepAggregate to dal.Aggregate
func convStepAggregate(step types.ReportStepAggregate, defs FrameDefinitionSet) (out *dal.Aggregate, err error) {
// Validation
if len(defs) > 1 {
err = fmt.Errorf("cannot convert aggregate step: expecting at most one definition, got %d", len(defs))
return
}
// Get additional filtering
var extf filter.Filter
if len(defs) == 1 {
extf = filterFromDef(defs[0])
}
f, err := dal.FilterFromExpr(step.Filter.Node()).
MergeFilters(extf)
if err != nil {
return
}
ggs := make([]dal.AggregateAttr, 0, len(step.Keys))
for _, c := range step.Keys {
ggs = append(ggs, dal.AggregateAttr{
Key: true,
Identifier: c.Name,
Label: c.Label,
Expression: c.Def.Node(),
})
}
vvs := make([]dal.AggregateAttr, 0, len(step.Columns))
for _, c := range step.Columns {
vvs = append(vvs, dal.AggregateAttr{
Identifier: c.Name,
Label: c.Label,
Expression: c.Def.Node(),
})
}
// Make pipeline step
out = &dal.Aggregate{
Ident: step.Name,
RelSource: step.Source,
Filter: f,
Group: ggs,
OutAttributes: vvs,
}
return
}
// convStepJoin converts ReportStepJoin to dal.Join
func convStepJoin(step types.ReportStepJoin, defs FrameDefinitionSet) (out *dal.Join, err error) {
// Validation
if len(defs) > 1 {
err = fmt.Errorf("cannot convert join step: expecting at most one definition, got %d", len(defs))
return
}
// Get additional filtering
var extf filter.Filter
if len(defs) == 1 {
extf = filterFromDef(defs[0])
}
f, err := dal.FilterFromExpr(step.Filter.Node()).
MergeFilters(extf)
if err != nil {
return
}
// Make pipeline step
out = &dal.Join{
Ident: step.Name,
RelLeft: step.LocalSource,
RelRight: step.ForeignSource,
Filter: f,
On: dal.JoinPredicate{
Left: step.LocalColumn,
Right: step.ForeignColumn,
},
}
return
}
// convStepLink converts ReportStepLink to dal.Link
func convStepLink(step types.ReportStepLink, defs FrameDefinitionSet) (out *dal.Link, err error) {
// Validation
if len(defs) > 2 {
err = fmt.Errorf("cannot convert join step: expecting at most two definitions, got %d", len(defs))
return
}
// @todo additional filtering; will need to split the dal.Link filter into
// left and right for more control and clarity
var extf filter.Filter
if len(defs) > 0 {
extf = filterFromDef(defs[0])
}
// Get additional filtering
f, err := dal.FilterFromExpr(step.Filter.Node()).
MergeFilters(extf)
if err != nil {
return
}
// @todo temporary constraint
if len(defs) == 2 {
d := defs[1]
if d.Filter != nil && d.Filter.Node() != nil || len(d.Sort) > 0 {
return nil, fmt.Errorf("temporary constraint: cannot apply sorting/filtering to foreign frame definition")
}
}
// Make pipeline step
out = &dal.Link{
Ident: step.Name,
RelLeft: step.LocalSource,
RelRight: step.ForeignSource,
On: dal.LinkPredicate{
Left: step.LocalColumn,
Right: step.ForeignColumn,
},
Filter: f,
}
return
}
// updateDefinitionColumns assures run's frame column completeness
func updateDefinitionColumns(r run) {
ppAttrs := r.Pipeline[0].Attributes()
for i, def := range r.Defs {
if len(def.Columns) > 0 {
continue
}
def.Columns = mappingToFrameCols(ppAttrs[i])
}
}
// makeModelRef returns the model ref based on the step load definition
// @todo should be expanded when we support models that are not compose modules
func makeModelRef(step types.ReportStepLoad) (out dal.ModelRef, err error) {
var (
connectionID uint64
moduleID, namespaceID uint64
module, namespace string
aux any
ok bool
)
if aux, ok = step.Definition["moduleID"]; ok {
moduleID = cast.ToUint64(aux)
} else if aux, ok = step.Definition["module"]; ok {
module = cast.ToString(aux)
} else {
err = fmt.Errorf("step definition is missing moduleID or module")
return
}
if aux, ok = step.Definition["namespaceID"]; ok {
namespaceID = cast.ToUint64(aux)
} else if aux, ok = step.Definition["namespace"]; ok {
namespace = cast.ToString(aux)
} else {
err = fmt.Errorf("step definition is missing namespaceID or namespace")
return
}
// Connection is optional, default is primary connection
if aux, ok = step.Definition["connectionID"]; ok {
connectionID = cast.ToUint64(aux)
}
out.ConnectionID = connectionID
out.Refs = make(map[string]any)
// Use only one of the two identifier variations with priority to ID
if moduleID > 0 {
out.Refs["moduleID"] = moduleID
} else {
out.Refs["module"] = module
}
if namespaceID > 0 {
out.Refs["namespaceID"] = namespaceID
} else {
out.Refs["namespace"] = namespace
}
return
}
// modelAttributes returns the attributes defined on the referenced model
func modelAttributes(pr modelFinder, step types.ReportStepLoad, mfr dal.ModelRef) []dal.AttributeMapping {
// All of the attributes
attrs, err := getModelAttrs(pr, mfr)
if err != nil {
return nil
}
return attrToMapping(attrs...)
}
func getModelAttrs(pr modelFinder, mfr dal.ModelRef) (attrs dal.AttributeSet, err error) {
m := pr.FindModel(mfr)
if m == nil {
return nil, fmt.Errorf("model not found: %v", mfr)
}
return m.Attributes, nil
}
func attrToMapping(aa ...*dal.Attribute) (out []dal.AttributeMapping) {
for _, a := range aa {
out = append(out, dal.SimpleAttr{
Ident: a.Ident,
Src: a.Ident,
Props: dal.MapProperties{
Label: a.Label,
Type: a.Type,
IsSystem: a.System,
Nullable: a.Type.IsNullable(),
IsPrimary: a.PrimaryKey,
// @todo add multi value delimiter; it's currently not set on the
// attribute so we might need to rethink this a bit
IsMultivalue: a.MultiValue,
},
})
}
return
}
func filterFromDef(def *FrameDefinition) (out filter.Filter) {
aux := filter.Generic(
filter.WithExpressionParsed(def.Filter.Node()),
filter.WithOrderBy(def.Sort),
)
if def.Paging != nil {
aux = aux.With(
filter.WithCursor(def.Paging.PageCursor),
filter.WithLimit(def.Paging.Limit),
)
}
return aux
}

View File

@@ -0,0 +1,328 @@
package reporting
import (
"context"
"testing"
"github.com/cortezaproject/corteza/server/pkg/dal"
"github.com/cortezaproject/corteza/server/pkg/ql"
"github.com/cortezaproject/corteza/server/system/types"
"github.com/stretchr/testify/require"
)
type (
mockModelFinder struct{}
)
func (mockModelFinder) FindModel(dal.ModelRef) *dal.Model {
return &dal.Model{
Attributes: dal.AttributeSet{{
Ident: "attr1",
Label: "Attr 1",
Type: dal.TypeText{Length: 100},
}, {
Ident: "attr2",
Label: "Attr 2",
Type: dal.TypeNumber{},
}},
}
}
func TestRuns(t *testing.T) {
simpleSS := types.ReportStepSet{
{Load: &types.ReportStepLoad{
Name: "l1",
Definition: map[string]interface{}{
"module": "mod",
"namespace": "ns",
},
}},
}
t.Run("no definitions", func(t *testing.T) {
rr, err := Runs(mockModelFinder{}, simpleSS, nil)
require.NoError(t, err)
require.Empty(t, rr)
})
t.Run("one definition", func(t *testing.T) {
defs := FrameDefinitionSet{
{Name: "d1", Source: "l1"},
}
rr, err := Runs(mockModelFinder{}, simpleSS, defs)
require.NoError(t, err)
require.Len(t, rr, 1)
})
t.Run("two different definitions for same source", func(t *testing.T) {
defs := FrameDefinitionSet{
{Name: "d1", Source: "l1"},
{Name: "d2", Source: "l1"},
}
rr, err := Runs(mockModelFinder{}, simpleSS, defs)
require.NoError(t, err)
require.Len(t, rr, 2)
require.Equal(t, "d1", rr[0].Defs[0].Name)
require.Equal(t, "d2", rr[1].Defs[0].Name)
})
t.Run("two defs for link", func(t *testing.T) {
defs := FrameDefinitionSet{
{Name: "d1", Source: "lnk", Ref: "l1"},
{Name: "d1", Source: "lnk", Ref: "l2"},
}
linkSS := types.ReportStepSet{
{Load: &types.ReportStepLoad{
Name: "l1",
Definition: map[string]interface{}{
"module": "mod",
"namespace": "ns",
},
}},
{Load: &types.ReportStepLoad{
Name: "l2",
Definition: map[string]interface{}{
"module": "mod",
"namespace": "ns",
},
}},
{Link: &types.ReportStepLink{
Name: "lnk",
LocalSource: "l1",
ForeignSource: "l2",
LocalColumn: "lc",
ForeignColumn: "fc",
}},
}
rr, err := Runs(mockModelFinder{}, linkSS, defs)
require.NoError(t, err)
require.Len(t, rr, 1)
require.Equal(t, "d1", rr[0].Defs[0].Name)
require.Equal(t, "l1", rr[0].Defs[0].Ref)
require.Equal(t, "d1", rr[0].Defs[1].Name)
require.Equal(t, "l2", rr[0].Defs[1].Ref)
})
t.Run("link and regular", func(t *testing.T) {
defs := FrameDefinitionSet{
{Name: "d1", Source: "lnk", Ref: "l1"},
{Name: "d1", Source: "lnk", Ref: "l2"},
{Name: "d2", Source: "l1"},
}
linkSS := types.ReportStepSet{
{Load: &types.ReportStepLoad{
Name: "l1",
Definition: map[string]interface{}{
"module": "mod",
"namespace": "ns",
},
}},
{Load: &types.ReportStepLoad{
Name: "l2",
Definition: map[string]interface{}{
"module": "mod",
"namespace": "ns",
},
}},
{Link: &types.ReportStepLink{
Name: "lnk",
LocalSource: "l1",
ForeignSource: "l2",
LocalColumn: "lc",
ForeignColumn: "fc",
}},
}
rr, err := Runs(mockModelFinder{}, linkSS, defs)
require.NoError(t, err)
require.Len(t, rr, 2)
require.Equal(t, "d1", rr[0].Defs[0].Name)
require.Equal(t, "l1", rr[0].Defs[0].Ref)
require.Equal(t, "d1", rr[0].Defs[1].Name)
require.Equal(t, "l2", rr[0].Defs[1].Ref)
require.Equal(t, "d2", rr[1].Defs[0].Name)
})
// Uses all possible step definitions in one go
t.Run("full definitions", func(t *testing.T) {
defs := FrameDefinitionSet{
// Load
{Name: "d1", Source: "l1"},
// Aggregate
{Name: "d2", Source: "agg1"},
// Link
{Name: "d3", Source: "lnk", Ref: "l1"},
{Name: "d3", Source: "lnk", Ref: "l2"},
// join
{Name: "d4", Source: "jn"},
}
rr, err := Runs(
mockModelFinder{},
types.ReportStepSet{
// Loads
{Load: &types.ReportStepLoad{
Name: "l1",
Definition: map[string]interface{}{
"module": "mod",
"namespace": "ns",
},
}},
{Load: &types.ReportStepLoad{
Name: "l2",
Definition: map[string]interface{}{
"module": "mod",
"namespace": "ns",
},
}},
// Aggregate
{Aggregate: &types.ReportStepAggregate{
Name: "agg1",
Source: "l1",
Keys: types.ReportAggregateColumnSet{{
Name: "k1",
Def: &types.ReportFilterExpr{ASTNode: &ql.ASTNode{Raw: "f1"}},
}},
Columns: types.ReportAggregateColumnSet{{
Name: "c1",
Def: &types.ReportFilterExpr{ASTNode: &ql.ASTNode{Raw: "count(f1)"}},
}},
}},
// Link
{Link: &types.ReportStepLink{
Name: "lnk",
LocalSource: "l1",
ForeignSource: "l2",
LocalColumn: "lc",
ForeignColumn: "fc",
}},
// Join
{Join: &types.ReportStepJoin{
Name: "jn",
LocalSource: "l1",
ForeignSource: "l2",
LocalColumn: "lc",
ForeignColumn: "fc",
}},
},
defs,
)
require.NoError(t, err)
require.Len(t, rr, 4)
})
}
func TestStepLoadConv(t *testing.T) {
t.Run("ref from ID", func(t *testing.T) {
c, err := convStepLoad(
mockModelFinder{},
types.ReportStepLoad{
Name: "l1",
Definition: map[string]interface{}{
"moduleID": 42,
"namespaceID": 43,
"connectionID": 44,
},
},
nil,
)
require.NoError(t, err)
require.NotNil(t, c)
})
t.Run("ref from handles", func(t *testing.T) {
c, err := convStepLoad(
mockModelFinder{},
types.ReportStepLoad{
Name: "l1",
Definition: map[string]interface{}{
"module": "mod1",
"namespace": "ns1",
"connectionID": 44,
},
},
nil,
)
require.NoError(t, err)
require.NotNil(t, c)
})
t.Run("missing module ref", func(t *testing.T) {
_, err := convStepLoad(
mockModelFinder{},
types.ReportStepLoad{
Name: "l1",
Definition: map[string]interface{}{
"namespace": "ns1",
"connectionID": 44,
},
},
nil,
)
require.Error(t, err)
})
t.Run("missing namespace ref", func(t *testing.T) {
_, err := convStepLoad(
mockModelFinder{},
types.ReportStepLoad{
Name: "l1",
Definition: map[string]interface{}{
"module": "mod1",
"connectionID": 44,
},
},
nil,
)
require.Error(t, err)
})
}
func TestFrames_regular(t *testing.T) {
ctx := context.Background()
buff := dal.InMemoryBuffer()
buff.Add(ctx, (&dal.Row{}).WithValue("id", 0, 1).WithValue("f1", 0, "f1 v1").WithValue("f2", 0, "f2 v1"))
buff.Add(ctx, (&dal.Row{}).WithValue("id", 0, 2).WithValue("f1", 0, "f1 v2").WithValue("f2", 0, "f2 v2"))
buff.Add(ctx, (&dal.Row{}).WithValue("id", 0, 3).WithValue("f1", 0, "f1 v3").WithValue("f2", 0, "f2 v3"))
r := run{
Pipeline: dal.Pipeline{&dal.Datasource{Ident: "l1"}},
Defs: FrameDefinitionSet{
{
Name: "d1",
Source: "l1",
Columns: FrameColumnSet{
{Name: "id", Label: "id", Kind: "Record"},
{Name: "f1", Label: "f1", Kind: "String"},
},
},
},
}
ff, err := Frames(ctx, buff, r)
require.NoError(t, err)
require.Len(t, ff, 1)
f := ff[0]
require.Len(t, f.Rows, 3)
require.Equal(t, FrameRow{"1", "f1 v1"}, f.Rows[0])
require.Equal(t, FrameRow{"2", "f1 v2"}, f.Rows[1])
require.Equal(t, FrameRow{"3", "f1 v3"}, f.Rows[2])
}

View File

@@ -0,0 +1,99 @@
package system
import (
"github.com/cortezaproject/corteza/server/codegen/schema"
)
resource_translation: {
features: {
labels: false
checkFn: false
}
model: {
defaultSetter: true
// lengths for the lang, resource fields are now a bit shorter
// Reason for that is supported index length in MySQL
attributes: {
id: schema.IdField
lang: {
goType: "types.Lang"
dal: { type: "Text", length: 32 }
omitSetter: true
omitGetter: true
}
resource: {
dal: { type: "Text", length: 256 }
}
k: {
dal: { type: "Text", length: 256 }
}
message: {
dal: {}
}
created_at: schema.SortableTimestampNowField
updated_at: schema.SortableTimestampNilField
deleted_at: schema.SortableTimestampNilField
owned_by: schema.AttributeUserRef
created_by: schema.AttributeUserRef
updated_by: schema.AttributeUserRef
deleted_by: schema.AttributeUserRef
}
indexes: {
"primary": { attribute: "id" }
"unique_translation": {
fields: [
{ attribute: "lang", modifiers: [ "LOWERCASE" ] },
{ attribute: "resource", modifiers: [ "LOWERCASE" ] },
{ attribute: "k", modifiers: [ "LOWERCASE" ] },
]
}
}
}
envoy: {
// Special handling for i18n
omit: true
}
filter: {
struct: {
translation_id: {goType: "[]uint64", ident: "translationID" }
lang: {}
resource: {}
resourceType: {}
owner_id: {goType: "uint64", ident: "ownerID", storeIdent: "rel_owner"}
deleted: {goType: "filter.State", storeIdent: "deleted_at"}
}
byValue: ["resource", "lang", "translation_id"]
byNilState: ["deleted"]
}
store: {
api: {
lookups: [
{
fields: ["id"]
description: """
searches for resource translation by ID
It also returns deleted resource translations.
"""
},
]
functions: [
{
expIdent: "TransformResource"
args: [
{ident: "lang", goType: "language.Tag" },
]
return: [ "map[string]map[string]*locale.ResourceTranslation" ]
},
]
}
}
}

2779
server/system/rest.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,194 @@
package rest
import (
"context"
"strconv"
"strings"
"github.com/cortezaproject/corteza/server/pkg/actionlog"
"github.com/cortezaproject/corteza/server/pkg/filter"
pkgid "github.com/cortezaproject/corteza/server/pkg/id"
"github.com/cortezaproject/corteza/server/store"
"github.com/cortezaproject/corteza/server/system/rest/request"
"github.com/cortezaproject/corteza/server/system/service"
"github.com/cortezaproject/corteza/server/system/types"
"github.com/go-oauth2/oauth2/v4/errors"
)
type (
Actionlog struct {
actionSvc actionlog.Recorder
store store.Users
ac interface {
CanReadActionLog(ctx context.Context) bool
}
}
// Extend actionlog.Action so we can
// provide user's email
actionlogActionPayload struct {
*actionlog.Action
Actor string `json:"actor,omitempty"`
Meta map[string]interface{} `json:"meta,omitempty"`
}
actionlogPayload struct {
Filter actionlog.Filter `json:"filter"`
Set []*actionlogActionPayload `json:"set"`
}
)
func (Actionlog) New() *Actionlog {
return &Actionlog{
actionSvc: service.DefaultActionlog,
ac: service.DefaultAccessControl,
store: service.DefaultStore,
}
}
func (ctrl *Actionlog) List(ctx context.Context, r *request.ActionlogList) (interface{}, error) {
if !ctrl.ac.CanReadActionLog(ctx) {
return nil, errors.ErrAccessDenied
}
var (
err error
f = actionlog.Filter{
FromTimestamp: r.From,
ToTimestamp: r.To,
BeforeActionID: r.BeforeActionID,
ActorID: r.ActorID,
Resource: r.Resource,
Action: r.Action,
Limit: r.Limit,
}
)
ee, f, err := ctrl.actionSvc.Find(ctx, f)
return ctrl.makeFilterPayload(ctx, ee, f, err)
}
func (ctrl Actionlog) makeFilterPayload(ctx context.Context, ee []*actionlog.Action, f actionlog.Filter, err error) (*actionlogPayload, error) {
if err != nil {
return nil, err
}
var (
pp = make([]*actionlogActionPayload, len(ee))
)
// Remap events to payload structs
for e := range ee {
pp[e] = &actionlogActionPayload{Action: ee[e]}
pp[e].Meta = ee[e].Meta
sanitizeMapStringInterface(pp[e].Meta)
}
err = userPreloader(
ctx,
ctrl.store,
func(c chan uint64) {
for e := range ee {
c <- ee[e].ActorID
}
close(c)
},
types.UserFilter{
Deleted: filter.StateInclusive,
Suspended: filter.StateInclusive,
},
func(u *types.User) error {
for p := range pp {
if pp[p].ActorID == u.ID {
pp[p].Actor = u.Name
if pp[p].Actor == "" {
pp[p].Actor = u.Email
}
}
}
return nil
},
)
if err != nil {
return nil, err
}
return &actionlogPayload{Filter: f, Set: pp}, nil
}
// Making sure JS can read old entries that were encoded
// as numeric. When decoded from the JSON stored in the DB
// they are of type float64. We'll cast them and store as uint64
//
// We do not want to do that to any numeric values just for the keys ending with
// "ID" or "By"
func sanitizeMapStringInterface(m map[string]interface{}) {
for k := range m {
switch v := m[k].(type) {
case float64:
if strings.HasSuffix(k, "ID") || strings.HasSuffix(k, "By") {
// make sure uint64 values on fields ending with ID
// are properly encoded as strings
m[k] = strconv.FormatUint(uint64(v), 10)
}
case map[string]interface{}:
sanitizeMapStringInterface(v)
}
}
}
// Preloader collects all ids of users, loads them and sets them back
//
// We'll be accessing the store directly since this is protected with action-log.read operation check.
//
// @todo move action log collection and user merging to dedicated service
func userPreloader(ctx context.Context, s store.Users, g func(chan uint64), f types.UserFilter, collect func(*types.User) error) error {
var (
// channel that will collect the IDs in the getter
ch = make(chan uint64, 0)
// unique index for IDs
unq = make(map[uint64]bool)
)
// Reset the collection of the IDs
f.UserID = make([]string, 0)
// Call getter and collect the IDs
go g(ch)
rangeLoop:
for {
select {
case <-ctx.Done():
close(ch)
break rangeLoop
case id, ok := <-ch:
if !ok {
// Channel closed
break rangeLoop
}
if !unq[id] {
unq[id] = true
f.UserID = append(f.UserID, pkgid.String(id))
}
}
}
// Load all users (even if deleted, suspended) from the given list of IDs
uu, _, err := store.SearchUsers(ctx, s, f)
if err != nil {
return err
}
return uu.Walk(collect)
}

View File

@@ -0,0 +1,156 @@
package rest
import (
"context"
"github.com/cortezaproject/corteza/server/pkg/api"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/system/rest/request"
"github.com/cortezaproject/corteza/server/system/service"
"github.com/cortezaproject/corteza/server/system/types"
)
type (
ApigwFilter struct {
svc filterService
ac templateAccessController
}
functionPayload struct {
*types.ApigwFilter
}
functionSetPayload struct {
Filter types.ApigwFilterFilter `json:"filter"`
Set []*functionPayload `json:"set"`
}
filterService interface {
FindByID(ctx context.Context, ID uint64) (*types.ApigwFilter, error)
Search(ctx context.Context, filter types.ApigwFilterFilter) (types.ApigwFilterSet, types.ApigwFilterFilter, error)
Create(ctx context.Context, new *types.ApigwFilter) (*types.ApigwFilter, error)
Update(ctx context.Context, upd *types.ApigwFilter) (*types.ApigwFilter, error)
DeleteByID(ctx context.Context, ID uint64) error
UndeleteByID(ctx context.Context, ID uint64) error
DefFilter(context.Context, string) (interface{}, error)
DefProxyAuth(context.Context) (interface{}, error)
}
)
func (ApigwFilter) New() *ApigwFilter {
return &ApigwFilter{
svc: service.DefaultApigwFilter,
ac: service.DefaultAccessControl,
}
}
func (ctrl *ApigwFilter) List(ctx context.Context, r *request.ApigwFilterList) (interface{}, error) {
var (
err error
f = types.ApigwFilterFilter{
RouteID: r.RouteID,
Deleted: filter.State(r.Deleted),
// todo: this should dynamic as Delete
// but making it default to `1`, until UI is aligned with this
Disabled: filter.StateInclusive,
}
)
if f.Paging, err = filter.NewPaging(r.Limit, r.PageCursor); err != nil {
return nil, err
}
if f.Sorting, err = filter.NewSorting(r.Sort); err != nil {
return nil, err
}
set, f, err := ctrl.svc.Search(ctx, f)
return ctrl.makeFilterPayload(ctx, set, f, err)
}
func (ctrl *ApigwFilter) Create(ctx context.Context, r *request.ApigwFilterCreate) (interface{}, error) {
var (
err error
q = &types.ApigwFilter{
Route: r.RouteID,
Weight: r.Weight,
Kind: r.Kind,
Ref: r.Ref,
Enabled: r.Enabled,
Params: r.Params,
}
)
q, err = ctrl.svc.Create(ctx, q)
return ctrl.makePayload(ctx, q, err)
}
func (ctrl *ApigwFilter) Read(ctx context.Context, r *request.ApigwFilterRead) (interface{}, error) {
return ctrl.svc.FindByID(ctx, r.FilterID)
}
func (ctrl *ApigwFilter) Update(ctx context.Context, r *request.ApigwFilterUpdate) (interface{}, error) {
var (
err error
q = &types.ApigwFilter{
ID: r.FilterID,
Route: r.RouteID,
Weight: r.Weight,
Kind: r.Kind,
Ref: r.Ref,
Enabled: r.Enabled,
Params: r.Params,
UpdatedAt: r.UpdatedAt,
}
)
q, err = ctrl.svc.Update(ctx, q)
return ctrl.makePayload(ctx, q, err)
}
func (ctrl *ApigwFilter) Delete(ctx context.Context, r *request.ApigwFilterDelete) (interface{}, error) {
return api.OK(), ctrl.svc.DeleteByID(ctx, r.FilterID)
}
func (ctrl *ApigwFilter) DefFilter(ctx context.Context, r *request.ApigwFilterDefFilter) (interface{}, error) {
return ctrl.svc.DefFilter(ctx, r.Kind)
}
func (ctrl *ApigwFilter) DefProxyAuth(ctx context.Context, r *request.ApigwFilterDefProxyAuth) (interface{}, error) {
return ctrl.svc.DefProxyAuth(ctx)
}
func (ctrl *ApigwFilter) Undelete(ctx context.Context, r *request.ApigwFilterUndelete) (interface{}, error) {
return api.OK(), ctrl.svc.UndeleteByID(ctx, r.FilterID)
}
func (ctrl *ApigwFilter) makePayload(ctx context.Context, q *types.ApigwFilter, err error) (*functionPayload, error) {
if err != nil || q == nil {
return nil, err
}
qq := &functionPayload{
ApigwFilter: q,
}
return qq, nil
}
func (ctrl *ApigwFilter) makeFilterPayload(ctx context.Context, nn types.ApigwFilterSet, f types.ApigwFilterFilter, err error) (*functionSetPayload, error) {
if err != nil {
return nil, err
}
msp := &functionSetPayload{Filter: f, Set: make([]*functionPayload, len(nn))}
for i := range nn {
msp.Set[i], _ = ctrl.makePayload(ctx, nn[i], nil)
}
return msp, nil
}

View File

@@ -0,0 +1,180 @@
package rest
import (
"context"
"github.com/cortezaproject/corteza/server/pkg/apigw/profiler"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/system/rest/request"
"github.com/cortezaproject/corteza/server/system/service"
"github.com/cortezaproject/corteza/server/system/types"
)
type (
profilerService interface {
Hits(context.Context, types.ApigwProfilerFilter) (types.ApigwProfilerHitSet, types.ApigwProfilerFilter, error)
HitsAggregated(context.Context, types.ApigwProfilerFilter) (types.ApigwProfilerAggregationSet, types.ApigwProfilerFilter, error)
Purge(context.Context, *profiler.PurgeFilter)
}
ApigwProfiler struct {
svc profilerService
ac templateAccessController
}
profilerRoutePayload struct {
*types.ApigwProfilerHit
}
profilerHitPayload struct {
*types.ApigwProfilerAggregation
}
profilerRouteSetPayload struct {
Filter types.ApigwProfilerFilter `json:"filter"`
Set []*profilerRoutePayload `json:"set"`
}
profilerHitSetPayload struct {
Filter types.ApigwProfilerFilter `json:"filter"`
Set []*profilerHitPayload `json:"set"`
}
)
func (ApigwProfiler) New() *ApigwProfiler {
return &ApigwProfiler{
svc: service.DefaultApigwProfiler,
ac: service.DefaultAccessControl,
}
}
// List displays the the aggregated list of routes
func (ctrl *ApigwProfiler) Aggregation(ctx context.Context, r *request.ApigwProfilerAggregation) (interface{}, error) {
var (
err error
f = types.ApigwProfilerFilter{
Path: r.Path,
Before: r.Before,
}
)
if f.Sorting, err = filter.NewSorting(r.Sort); err != nil {
return nil, err
}
if f.Paging, err = filter.NewPaging(r.Limit, ""); err != nil {
return nil, err
}
set, f, err := ctrl.svc.HitsAggregated(ctx, f)
return ctrl.makeFilterPayload(ctx, set, f, err)
}
// Route displays the list of hits per-route
func (ctrl *ApigwProfiler) Route(ctx context.Context, r *request.ApigwProfilerRoute) (interface{}, error) {
var (
err error
f = types.ApigwProfilerFilter{
Path: r.RouteID,
Before: r.Before,
}
)
if f.Sorting, err = filter.NewSorting(r.Sort); err != nil {
return nil, err
}
if f.Paging, err = filter.NewPaging(r.Limit, ""); err != nil {
return nil, err
}
set, f, err := ctrl.svc.Hits(ctx, f)
return ctrl.makeRouteFilterPayload(ctx, set, f, err)
}
// Hit displays the details of a certain hit on a route
func (ctrl *ApigwProfiler) Hit(ctx context.Context, r *request.ApigwProfilerHit) (interface{}, error) {
var (
f = types.ApigwProfilerFilter{
Hit: r.HitID,
}
)
set, f, err := ctrl.svc.Hits(ctx, f)
if len(set) != 1 {
return nil, nil
}
return ctrl.makeRoutePayload(ctx, set[0], err)
}
func (ctrl *ApigwProfiler) PurgeAll(ctx context.Context, r *request.ApigwProfilerPurgeAll) (_ interface{}, _ error) {
var (
f = &profiler.PurgeFilter{}
)
ctrl.svc.Purge(ctx, f)
return
}
func (ctrl *ApigwProfiler) Purge(ctx context.Context, r *request.ApigwProfilerPurge) (_ interface{}, _ error) {
var (
f = &profiler.PurgeFilter{
RouteID: r.RouteID,
}
)
ctrl.svc.Purge(ctx, f)
return
}
func (ctrl *ApigwProfiler) makePayload(ctx context.Context, q *types.ApigwProfilerAggregation, err error) (*profilerHitPayload, error) {
if err != nil || q == nil {
return nil, err
}
qq := &profilerHitPayload{
ApigwProfilerAggregation: q,
}
return qq, nil
}
func (ctrl *ApigwProfiler) makeFilterPayload(ctx context.Context, nn types.ApigwProfilerAggregationSet, f types.ApigwProfilerFilter, err error) (*profilerHitSetPayload, error) {
if err != nil {
return nil, err
}
msp := &profilerHitSetPayload{Filter: f, Set: make([]*profilerHitPayload, len(nn))}
for i := range nn {
msp.Set[i], _ = ctrl.makePayload(ctx, nn[i], nil)
}
return msp, nil
}
func (ctrl *ApigwProfiler) makeRoutePayload(ctx context.Context, q *types.ApigwProfilerHit, err error) (*profilerRoutePayload, error) {
if err != nil || q == nil {
return nil, err
}
return &profilerRoutePayload{q}, nil
}
func (ctrl *ApigwProfiler) makeRouteFilterPayload(ctx context.Context, nn types.ApigwProfilerHitSet, f types.ApigwProfilerFilter, err error) (*profilerRouteSetPayload, error) {
if err != nil {
return nil, err
}
msp := &profilerRouteSetPayload{Filter: f, Set: make([]*profilerRoutePayload, len(nn))}
for i := range nn {
msp.Set[i], _ = ctrl.makeRoutePayload(ctx, nn[i], nil)
}
return msp, nil
}

View File

@@ -0,0 +1,162 @@
package rest
import (
"context"
"github.com/cortezaproject/corteza/server/pkg/api"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/system/rest/request"
"github.com/cortezaproject/corteza/server/system/service"
"github.com/cortezaproject/corteza/server/system/types"
)
type (
ApigwRoute struct {
svc routeService
ac apiGwRouteAccessController
}
routePayload struct {
*types.ApigwRoute
CanGrant bool `json:"canGrant"`
CanUpdateApigwRoute bool `json:"canUpdateApigwRoute"`
CanDeleteApigwRoute bool `json:"canDeleteApigwRoute"`
}
routeSetPayload struct {
Filter types.ApigwRouteFilter `json:"filter"`
Set []*routePayload `json:"set"`
}
routeService interface {
FindByID(ctx context.Context, ID uint64) (*types.ApigwRoute, error)
Create(ctx context.Context, new *types.ApigwRoute) (*types.ApigwRoute, error)
Update(ctx context.Context, upd *types.ApigwRoute) (*types.ApigwRoute, error)
DeleteByID(ctx context.Context, ID uint64) error
UndeleteByID(ctx context.Context, ID uint64) error
Search(ctx context.Context, filter types.ApigwRouteFilter) (types.ApigwRouteSet, types.ApigwRouteFilter, error)
}
apiGwRouteAccessController interface {
CanGrant(context.Context) bool
CanCreateApigwRoute(context.Context) bool
CanUpdateApigwRoute(context.Context, *types.ApigwRoute) bool
CanDeleteApigwRoute(context.Context, *types.ApigwRoute) bool
}
)
func (ApigwRoute) New() *ApigwRoute {
return &ApigwRoute{
svc: service.DefaultApigwRoute,
ac: service.DefaultAccessControl,
}
}
func (ctrl *ApigwRoute) List(ctx context.Context, r *request.ApigwRouteList) (interface{}, error) {
var (
err error
f = types.ApigwRouteFilter{
// todo: this should renamed to r.Endpoint after UI is aligned with this
Endpoint: r.Query,
Deleted: filter.State(r.Deleted),
// todo: this should dynamic as Delete
// but making it default to `1`, until UI is aligned with this
Disabled: filter.StateInclusive,
}
)
if f.Paging, err = filter.NewPaging(r.Limit, r.PageCursor); err != nil {
return nil, err
}
f.IncTotal = r.IncTotal
if f.Sorting, err = filter.NewSorting(r.Sort); err != nil {
return nil, err
}
set, f, err := ctrl.svc.Search(ctx, f)
return ctrl.makeFilterPayload(ctx, set, f, err)
}
func (ctrl *ApigwRoute) Create(ctx context.Context, r *request.ApigwRouteCreate) (interface{}, error) {
var (
err error
q = &types.ApigwRoute{
Endpoint: r.Endpoint,
Method: r.Method,
Enabled: r.Enabled,
}
)
q, err = ctrl.svc.Create(ctx, q)
return ctrl.makePayload(ctx, q, err)
}
func (ctrl *ApigwRoute) Read(ctx context.Context, r *request.ApigwRouteRead) (interface{}, error) {
res, err := ctrl.svc.FindByID(ctx, r.RouteID)
return ctrl.makePayload(ctx, res, err)
}
func (ctrl *ApigwRoute) Update(ctx context.Context, r *request.ApigwRouteUpdate) (interface{}, error) {
var (
err error
q = &types.ApigwRoute{
ID: r.RouteID,
Endpoint: r.Endpoint,
Method: r.Method,
Group: r.Group,
Enabled: r.Enabled,
Meta: r.Meta,
UpdatedAt: r.UpdatedAt,
}
)
q, err = ctrl.svc.Update(ctx, q)
return ctrl.makePayload(ctx, q, err)
}
func (ctrl *ApigwRoute) Delete(ctx context.Context, r *request.ApigwRouteDelete) (interface{}, error) {
return api.OK(), ctrl.svc.DeleteByID(ctx, r.RouteID)
}
func (ctrl *ApigwRoute) Undelete(ctx context.Context, r *request.ApigwRouteUndelete) (interface{}, error) {
return api.OK(), ctrl.svc.UndeleteByID(ctx, r.RouteID)
}
func (ctrl *ApigwRoute) makePayload(ctx context.Context, q *types.ApigwRoute, err error) (*routePayload, error) {
if err != nil || q == nil {
return nil, err
}
qq := &routePayload{
ApigwRoute: q,
CanGrant: ctrl.ac.CanGrant(ctx),
CanUpdateApigwRoute: ctrl.ac.CanUpdateApigwRoute(ctx, q),
CanDeleteApigwRoute: ctrl.ac.CanDeleteApigwRoute(ctx, q),
}
return qq, nil
}
func (ctrl *ApigwRoute) makeFilterPayload(ctx context.Context, nn types.ApigwRouteSet, f types.ApigwRouteFilter, err error) (*routeSetPayload, error) {
if err != nil {
return nil, err
}
msp := &routeSetPayload{Filter: f, Set: make([]*routePayload, len(nn))}
for i := range nn {
msp.Set[i], _ = ctrl.makePayload(ctx, nn[i], nil)
}
return msp, nil
}

View File

@@ -0,0 +1,254 @@
package rest
import (
"context"
"strconv"
"github.com/cortezaproject/corteza/server/pkg/api"
"github.com/cortezaproject/corteza/server/pkg/auth"
"github.com/cortezaproject/corteza/server/pkg/corredor"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/pkg/flag"
"github.com/cortezaproject/corteza/server/system/rest/request"
"github.com/cortezaproject/corteza/server/system/service"
"github.com/cortezaproject/corteza/server/system/service/event"
"github.com/cortezaproject/corteza/server/system/types"
)
type (
Application struct {
application applicationService
attachment service.AttachmentService
ac applicationAccessController
}
applicationService interface {
LookupByID(ctx context.Context, ID uint64) (app *types.Application, err error)
Search(ctx context.Context, filter types.ApplicationFilter) (aa types.ApplicationSet, f types.ApplicationFilter, err error)
Create(ctx context.Context, new *types.Application) (app *types.Application, err error)
Update(ctx context.Context, upd *types.Application) (app *types.Application, err error)
Delete(ctx context.Context, ID uint64) (err error)
Undelete(ctx context.Context, ID uint64) (err error)
Reorder(ctx context.Context, order []uint64) (err error)
Flag(ctx context.Context, app *types.Application, ownedBy uint64, f string) error
Unflag(ctx context.Context, app *types.Application, ownedBy uint64, f string) error
}
applicationAccessController interface {
CanGrant(context.Context) bool
CanUpdateApplication(context.Context, *types.Application) bool
CanDeleteApplication(context.Context, *types.Application) bool
}
applicationPayload struct {
*types.Application
CanGrant bool `json:"canGrant"`
CanUpdateApplication bool `json:"canUpdateApplication"`
CanDeleteApplication bool `json:"canDeleteApplication"`
}
applicationSetPayload struct {
Filter types.ApplicationFilter `json:"filter"`
Set []*applicationPayload `json:"set"`
}
)
func (Application) New() *Application {
return &Application{
application: service.DefaultApplication,
attachment: service.DefaultAttachment,
ac: service.DefaultAccessControl,
}
}
func (ctrl *Application) List(ctx context.Context, r *request.ApplicationList) (interface{}, error) {
var (
err error
f = types.ApplicationFilter{
Name: r.Name,
Query: r.Query,
Labels: r.Labels,
Flags: r.Flags,
IncFlags: r.IncFlags,
Deleted: filter.State(r.Deleted),
}
)
if f.Paging, err = filter.NewPaging(r.Limit, r.PageCursor); err != nil {
return nil, err
}
f.IncTotal = r.IncTotal
if f.Sorting, err = filter.NewSorting(r.Sort); err != nil {
return nil, err
}
set, filter, err := ctrl.application.Search(ctx, f)
return ctrl.makeFilterPayload(ctx, set, filter, err)
}
func (ctrl *Application) Create(ctx context.Context, r *request.ApplicationCreate) (interface{}, error) {
var (
err error
app = &types.Application{
Name: r.Name,
Enabled: r.Enabled,
Weight: r.Weight,
Labels: r.Labels,
}
)
if r.Unify != nil {
app.Unify = &types.ApplicationUnify{}
if err := r.Unify.Unmarshal(app.Unify); err != nil {
return nil, err
}
}
app, err = ctrl.application.Create(ctx, app)
return ctrl.makePayload(ctx, app, err)
}
func (ctrl *Application) Update(ctx context.Context, r *request.ApplicationUpdate) (interface{}, error) {
var (
err error
app = &types.Application{
ID: r.ApplicationID,
Name: r.Name,
Enabled: r.Enabled,
Weight: r.Weight,
Labels: r.Labels,
UpdatedAt: r.UpdatedAt,
}
)
if r.Unify != nil {
app.Unify = &types.ApplicationUnify{}
if err := r.Unify.Unmarshal(app.Unify); err != nil {
return nil, err
}
}
app, err = ctrl.application.Update(ctx, app)
return ctrl.makePayload(ctx, app, err)
}
func (ctrl *Application) Read(ctx context.Context, r *request.ApplicationRead) (interface{}, error) {
app, err := ctrl.application.LookupByID(ctx, r.ApplicationID)
if err != nil {
return ctrl.makePayload(ctx, app, err)
}
err = flag.Load(ctx, service.DefaultStore, r.IncFlags, auth.GetIdentityFromContext(ctx).Identity(), app)
return ctrl.makePayload(ctx, app, err)
}
func (ctrl *Application) Delete(ctx context.Context, r *request.ApplicationDelete) (interface{}, error) {
return api.OK(), ctrl.application.Delete(ctx, r.ApplicationID)
}
func (ctrl *Application) Undelete(ctx context.Context, r *request.ApplicationUndelete) (interface{}, error) {
return api.OK(), ctrl.application.Undelete(ctx, r.ApplicationID)
}
func (ctrl *Application) Upload(ctx context.Context, r *request.ApplicationUpload) (interface{}, error) {
file, err := r.Upload.Open()
if err != nil {
return nil, err
}
defer file.Close()
a, err := ctrl.attachment.CreateApplicationAttachment(
ctx,
r.Upload.Filename,
r.Upload.Size,
file,
nil,
)
if err != nil {
return nil, err
}
return makeAttachmentPayload(ctx, a, err)
}
func (ctrl *Application) TriggerScript(ctx context.Context, r *request.ApplicationTriggerScript) (rsp interface{}, err error) {
var (
application *types.Application
)
if application, err = ctrl.application.LookupByID(ctx, r.ApplicationID); err != nil {
return
}
// @todo implement same behaviour as we have on record - Application+oldApplication
err = corredor.Service().Exec(ctx, r.Script, corredor.ExtendScriptArgs(event.ApplicationOnManual(application, application), r.Args))
return application, err
}
func (ctrl *Application) Reorder(ctx context.Context, r *request.ApplicationReorder) (interface{}, error) {
order := make([]uint64, len(r.ApplicationIDs))
for i, aid := range r.ApplicationIDs {
parsed, err := strconv.ParseUint(aid, 10, 64)
if err != nil {
return nil, err
}
order[i] = parsed
}
return api.OK(), ctrl.application.Reorder(ctx, order)
}
func (ctrl *Application) FlagCreate(ctx context.Context, r *request.ApplicationFlagCreate) (interface{}, error) {
app, err := ctrl.application.LookupByID(ctx, r.ApplicationID)
if err != nil {
return nil, err
}
return api.OK(), ctrl.application.Flag(ctx, app, r.OwnedBy, r.Flag)
}
func (ctrl *Application) FlagDelete(ctx context.Context, r *request.ApplicationFlagDelete) (interface{}, error) {
app, err := ctrl.application.LookupByID(ctx, r.ApplicationID)
if err != nil {
return nil, err
}
return api.OK(), ctrl.application.Unflag(ctx, app, r.OwnedBy, r.Flag)
}
func (ctrl Application) makePayload(ctx context.Context, m *types.Application, err error) (*applicationPayload, error) {
if err != nil || m == nil {
return nil, err
}
return &applicationPayload{
Application: m,
CanGrant: ctrl.ac.CanGrant(ctx),
CanUpdateApplication: ctrl.ac.CanUpdateApplication(ctx, m),
CanDeleteApplication: ctrl.ac.CanDeleteApplication(ctx, m),
}, nil
}
func (ctrl Application) makeFilterPayload(ctx context.Context, nn types.ApplicationSet, f types.ApplicationFilter, err error) (*applicationSetPayload, error) {
if err != nil {
return nil, err
}
msp := &applicationSetPayload{Filter: f, Set: make([]*applicationPayload, len(nn))}
for i := range nn {
msp.Set[i], _ = ctrl.makePayload(ctx, nn[i], nil)
}
return msp, nil
}

View File

@@ -0,0 +1,187 @@
package rest
import (
"context"
"fmt"
"io"
"net/http"
"net/url"
"github.com/cortezaproject/corteza/server/pkg/api"
"github.com/cortezaproject/corteza/server/pkg/auth"
"github.com/cortezaproject/corteza/server/system/rest/request"
"github.com/cortezaproject/corteza/server/system/service"
"github.com/cortezaproject/corteza/server/system/types"
)
type (
attachmentPayload struct {
*types.Attachment
}
attachmentSetPayload struct {
Filter types.AttachmentFilter `json:"filter"`
Set []*attachmentPayload `json:"set"`
}
Attachment struct {
attachment service.AttachmentService
}
)
func (Attachment) New() *Attachment {
return &Attachment{
attachment: service.DefaultAttachment,
}
}
func (ctrl Attachment) Read(ctx context.Context, r *request.AttachmentRead) (interface{}, error) {
if !auth.GetIdentityFromContext(ctx).Valid() {
return nil, fmt.Errorf("Unauthorized")
}
a, err := ctrl.attachment.FindByID(ctx, r.AttachmentID)
return makeAttachmentPayload(ctx, a, err)
}
func (ctrl Attachment) Delete(ctx context.Context, r *request.AttachmentDelete) (interface{}, error) {
if !auth.GetIdentityFromContext(ctx).Valid() {
return nil, fmt.Errorf("Unauthorized")
}
_, err := ctrl.attachment.FindByID(ctx, r.AttachmentID)
if err != nil {
return nil, err
}
return api.OK(), ctrl.attachment.DeleteByID(ctx, r.AttachmentID)
}
func (ctrl Attachment) Original(ctx context.Context, r *request.AttachmentOriginal) (interface{}, error) {
if err := ctrl.isAccessible(r.Kind, r.AttachmentID, r.UserID, r.Sign); err != nil {
return nil, err
}
return ctrl.serve(ctx, r.AttachmentID, false, r.Download)
}
func (ctrl Attachment) Preview(ctx context.Context, r *request.AttachmentPreview) (interface{}, error) {
if err := ctrl.isAccessible(r.Kind, r.AttachmentID, r.UserID, r.Sign); err != nil {
return nil, err
}
return ctrl.serve(ctx, r.AttachmentID, true, false)
}
func (ctrl Attachment) isAccessible(kind string, attachmentID, userID uint64, signature string) error {
if kind == types.AttachmentKindSettings || kind == types.AttachmentKindAvatar {
// Attachments on settings are public
return nil
}
if signature == "" {
return fmt.Errorf("Unauthorized")
}
if userID == 0 {
return fmt.Errorf("missing or invalid user ID")
}
if attachmentID == 0 {
return fmt.Errorf("missing or invalid attachment ID")
}
if !auth.DefaultSigner.Verify(signature, userID, attachmentID) {
return fmt.Errorf("missing or invalid signature")
}
return nil
}
func (ctrl Attachment) serve(ctx context.Context, attachmentID uint64, preview, download bool) (interface{}, error) {
return func(w http.ResponseWriter, req *http.Request) {
att, err := ctrl.attachment.FindByID(ctx, attachmentID)
if err != nil {
// Simplify error handling for now
w.WriteHeader(http.StatusNotFound)
return
}
var fh io.ReadSeekCloser
if preview {
fh, err = ctrl.attachment.OpenPreview(att)
} else {
fh, err = ctrl.attachment.OpenOriginal(att)
}
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer fh.Close()
name := url.QueryEscape(att.Name)
if download {
w.Header().Add("Content-Disposition", "attachment; filename="+name)
} else {
w.Header().Add("Content-Disposition", "inline; filename="+name)
w.Header().Add("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; sandbox")
}
http.ServeContent(w, req, name, att.CreatedAt, fh)
}, nil
}
func (ctrl Attachment) makeFilterPayload(ctx context.Context, aa types.AttachmentSet, f types.AttachmentFilter, err error) (*attachmentSetPayload, error) {
if err != nil {
return nil, err
}
asp := &attachmentSetPayload{Filter: f, Set: make([]*attachmentPayload, len(aa))}
for i := range aa {
asp.Set[i], _ = makeAttachmentPayload(ctx, aa[i], nil)
}
return asp, nil
}
func makeAttachmentPayload(ctx context.Context, a *types.Attachment, err error) (*attachmentPayload, error) {
if err != nil || a == nil {
return nil, err
}
var (
userID = auth.GetIdentityFromContext(ctx).Identity()
signParams = ""
preview string
baseURL = fmt.Sprintf("/attachment/%s/%d/", a.Kind, a.ID)
)
if a.Meta.Preview != nil {
var ext = a.Meta.Preview.Extension
if ext == "" {
ext = "jpg"
}
preview = baseURL + fmt.Sprintf("preview.%s", ext)
}
switch a.Kind {
case types.AttachmentKindSettings:
// No URL signing, settings attachments are public.
default:
signParams = fmt.Sprintf("?sign=%s&userID=%d", auth.DefaultSigner.Sign(userID, a.ID), userID)
}
ap := &attachmentPayload{a}
ap.Url = baseURL + fmt.Sprintf("original/%s", url.PathEscape(a.Name)) + signParams
ap.PreviewUrl = preview + signParams
return ap, nil
}

View File

@@ -0,0 +1,81 @@
package rest
import (
"context"
"github.com/cortezaproject/corteza/server/pkg/auth"
"github.com/cortezaproject/corteza/server/pkg/payload"
"github.com/cortezaproject/corteza/server/system/rest/request"
"github.com/cortezaproject/corteza/server/system/service"
"github.com/cortezaproject/corteza/server/system/types"
)
type (
Auth struct {
settings *types.AppSettings
authSvc authUserService
}
authUserResponse struct {
JWT string `json:"jwt"`
User *authUserPayload `json:"user"`
}
authUserPayload struct {
ID uint64 `json:"userID,string"`
Name string `json:"name"`
Email string `json:"email"`
Username string `json:"username"`
Handle string `json:"handle"`
Roles []string `json:"roles"`
}
authUserService interface {
Impersonate(ctx context.Context, userID uint64) (*types.User, error)
LoadRoleMemberships(ctx context.Context, user *types.User) error
}
)
func (Auth) New() *Auth {
return &Auth{
settings: service.CurrentSettings,
authSvc: service.DefaultAuth,
}
}
// Impersonate implements impersonation functionality
//
// This is experimental and internals will most likely change in the future:
func (ctrl *Auth) Impersonate(ctx context.Context, r *request.AuthImpersonate) (interface{}, error) {
u, err := ctrl.authSvc.Impersonate(ctx, r.UserID)
if err != nil {
return nil, err
}
return ctrl.makePayload(ctx, u)
}
func (ctrl *Auth) makePayload(ctx context.Context, user *types.User) (*authUserResponse, error) {
if err := ctrl.authSvc.LoadRoleMemberships(ctx, user); err != nil {
return nil, err
}
// Generate and save the token
t, err := auth.TokenIssuer.Issue(ctx, auth.WithIdentity(user))
if err != nil {
return nil, nil
}
return &authUserResponse{
JWT: string(t),
User: &authUserPayload{
ID: user.ID,
Name: user.Name,
Handle: user.Handle,
Username: user.Username,
Email: user.Email,
Roles: payload.Uint64stoa(user.Roles()),
},
}, nil
}

View File

@@ -0,0 +1,183 @@
package rest
import (
"context"
"github.com/cortezaproject/corteza/server/pkg/api"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/pkg/options"
"github.com/cortezaproject/corteza/server/system/rest/request"
"github.com/cortezaproject/corteza/server/system/service"
"github.com/cortezaproject/corteza/server/system/types"
)
type (
AuthClient struct {
authClient authClientService
ac authClientAccessController
opt options.AuthOpt
}
authClientService interface {
LookupByID(ctx context.Context, ID uint64) (app *types.AuthClient, err error)
Search(ctx context.Context, filter types.AuthClientFilter) (aa types.AuthClientSet, f types.AuthClientFilter, err error)
Create(ctx context.Context, new *types.AuthClient) (app *types.AuthClient, err error)
Update(ctx context.Context, upd *types.AuthClient) (app *types.AuthClient, err error)
Delete(ctx context.Context, ID uint64) (err error)
Undelete(ctx context.Context, ID uint64) (err error)
ExposeSecret(ctx context.Context, ID uint64) (secret string, err error)
RegenerateSecret(ctx context.Context, ID uint64) (secret string, err error)
IsDefaultClient(c *types.AuthClient) bool
}
authClientAccessController interface {
CanGrant(context.Context) bool
CanUpdateAuthClient(context.Context, *types.AuthClient) bool
CanDeleteAuthClient(context.Context, *types.AuthClient) bool
}
authClientPayload struct {
*types.AuthClient
IsDefault bool `json:"isDefault"`
CanGrant bool `json:"canGrant"`
CanUpdateAuthClient bool `json:"canUpdateAuthClient"`
CanDeleteAuthClient bool `json:"canDeleteAuthClient"`
}
authClientSetPayload struct {
Filter types.AuthClientFilter `json:"filter"`
Set []*authClientPayload `json:"set"`
}
)
func (AuthClient) New() *AuthClient {
return &AuthClient{
authClient: service.DefaultAuthClient,
ac: service.DefaultAccessControl,
}
}
func (ctrl *AuthClient) List(ctx context.Context, r *request.AuthClientList) (interface{}, error) {
var (
err error
f = types.AuthClientFilter{
Handle: r.Handle,
Labels: r.Labels,
Deleted: filter.State(r.Deleted),
}
)
if f.Paging, err = filter.NewPaging(r.Limit, r.PageCursor); err != nil {
return nil, err
}
f.IncTotal = r.IncTotal
if f.Sorting, err = filter.NewSorting(r.Sort); err != nil {
return nil, err
}
set, filter, err := ctrl.authClient.Search(ctx, f)
return ctrl.makeFilterPayload(ctx, set, filter, err)
}
func (ctrl *AuthClient) Create(ctx context.Context, r *request.AuthClientCreate) (interface{}, error) {
var (
err error
app = &types.AuthClient{
Handle: r.Handle,
Meta: r.Meta,
ValidGrant: r.ValidGrant,
RedirectURI: r.RedirectURI,
Scope: r.Scope,
Trusted: r.Trusted,
Enabled: r.Enabled,
ValidFrom: r.ValidFrom,
ExpiresAt: r.ExpiresAt,
Security: r.Security,
Labels: r.Labels,
}
)
app, err = ctrl.authClient.Create(ctx, app)
return ctrl.makePayload(ctx, app, err)
}
func (ctrl *AuthClient) Update(ctx context.Context, r *request.AuthClientUpdate) (interface{}, error) {
var (
err error
app = &types.AuthClient{
ID: r.ClientID,
Handle: r.Handle,
Meta: r.Meta,
ValidGrant: r.ValidGrant,
RedirectURI: r.RedirectURI,
Scope: r.Scope,
Trusted: r.Trusted,
Enabled: r.Enabled,
ValidFrom: r.ValidFrom,
ExpiresAt: r.ExpiresAt,
Security: r.Security,
Labels: r.Labels,
UpdatedAt: r.UpdatedAt,
}
)
app, err = ctrl.authClient.Update(ctx, app)
return ctrl.makePayload(ctx, app, err)
}
func (ctrl *AuthClient) Read(ctx context.Context, r *request.AuthClientRead) (interface{}, error) {
app, err := ctrl.authClient.LookupByID(ctx, r.ClientID)
return ctrl.makePayload(ctx, app, err)
}
func (ctrl *AuthClient) ExposeSecret(ctx context.Context, r *request.AuthClientExposeSecret) (interface{}, error) {
return ctrl.authClient.ExposeSecret(ctx, r.ClientID)
}
func (ctrl *AuthClient) RegenerateSecret(ctx context.Context, r *request.AuthClientRegenerateSecret) (interface{}, error) {
return ctrl.authClient.RegenerateSecret(ctx, r.ClientID)
}
func (ctrl *AuthClient) Delete(ctx context.Context, r *request.AuthClientDelete) (interface{}, error) {
return api.OK(), ctrl.authClient.Delete(ctx, r.ClientID)
}
func (ctrl *AuthClient) Undelete(ctx context.Context, r *request.AuthClientUndelete) (interface{}, error) {
return api.OK(), ctrl.authClient.Undelete(ctx, r.ClientID)
}
func (ctrl AuthClient) makePayload(ctx context.Context, m *types.AuthClient, err error) (*authClientPayload, error) {
if err != nil || m == nil {
return nil, err
}
return &authClientPayload{
AuthClient: m,
IsDefault: ctrl.authClient.IsDefaultClient(m),
CanGrant: ctrl.ac.CanGrant(ctx),
CanUpdateAuthClient: ctrl.ac.CanUpdateAuthClient(ctx, m),
CanDeleteAuthClient: ctrl.ac.CanDeleteAuthClient(ctx, m),
}, nil
}
func (ctrl AuthClient) makeFilterPayload(ctx context.Context, nn types.AuthClientSet, f types.AuthClientFilter, err error) (*authClientSetPayload, error) {
if err != nil {
return nil, err
}
msp := &authClientSetPayload{Filter: f, Set: make([]*authClientPayload, len(nn))}
for i := range nn {
msp.Set[i], _ = ctrl.makePayload(ctx, nn[i], nil)
}
return msp, nil
}

View File

@@ -0,0 +1,48 @@
package rest
import (
"context"
"github.com/cortezaproject/corteza/server/pkg/api"
"github.com/cortezaproject/corteza/server/pkg/corredor"
"github.com/cortezaproject/corteza/server/system/rest/request"
"github.com/cortezaproject/corteza/server/system/service/event"
)
type (
Automation struct{}
)
func (Automation) New() *Automation {
return &Automation{}
}
func (ctrl *Automation) List(ctx context.Context, r *request.AutomationList) (interface{}, error) {
return corredor.GenericListHandler(
ctx,
corredor.Service(),
corredor.Filter{
ResourceTypePrefixes: r.ResourceTypePrefixes,
ExcludeInvalid: r.ExcludeInvalid,
ResourceTypes: r.ResourceTypes,
EventTypes: r.EventTypes,
ExcludeServerScripts: r.ExcludeServerScripts,
ExcludeClientScripts: r.ExcludeClientScripts,
},
"system",
)
}
func (ctrl *Automation) Bundle(ctx context.Context, r *request.AutomationBundle) (interface{}, error) {
return corredor.GenericBundleHandler(
ctx,
corredor.Service(),
r.Bundle,
r.Type,
r.Ext,
)
}
func (ctrl *Automation) TriggerScript(ctx context.Context, r *request.AutomationTriggerScript) (interface{}, error) {
return api.OK(), corredor.Service().Exec(ctx, r.Script, corredor.ExtendScriptArgs(event.SystemOnManual(), r.Args))
}

View File

@@ -0,0 +1,295 @@
package rest
import (
"context"
"io"
"net/http"
"time"
federationService "github.com/cortezaproject/corteza/server/federation/service"
federationTypes "github.com/cortezaproject/corteza/server/federation/types"
"github.com/cortezaproject/corteza/server/pkg/api"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/pkg/handle"
"github.com/cortezaproject/corteza/server/pkg/id"
"github.com/cortezaproject/corteza/server/system/rest/request"
"github.com/cortezaproject/corteza/server/system/service"
"github.com/cortezaproject/corteza/server/system/types"
"github.com/modern-go/reflect2"
)
type (
DalConnection struct {
svc connectionService
federationSvc federationNodeService
connectionAc connectionAccessController
}
connectionPayload struct {
*types.DalConnection
CanGrant bool `json:"canGrant"`
CanUpdateConnection bool `json:"canUpdateConnection"`
CanDeleteConnection bool `json:"canDeleteConnection"`
CanManageDalConfig bool `json:"canManageDalConfig"`
}
connectionSetPayload struct {
Filter types.DalConnectionFilter `json:"filter"`
Set []*connectionPayload `json:"set"`
}
connectionAccessController interface {
CanGrant(context.Context) bool
CanCreateDalConnection(context.Context) bool
CanUpdateDalConnection(context.Context, *types.DalConnection) bool
CanDeleteDalConnection(context.Context, *types.DalConnection) bool
CanManageDalConfigOnDalConnection(context.Context, *types.DalConnection) bool
}
connectionService interface {
FindByID(ctx context.Context, ID uint64) (*types.DalConnection, error)
Create(ctx context.Context, new *types.DalConnection) (*types.DalConnection, error)
Update(ctx context.Context, upd *types.DalConnection) (*types.DalConnection, error)
DeleteByID(ctx context.Context, ID uint64) error
UndeleteByID(ctx context.Context, ID uint64) error
Search(ctx context.Context, filter types.DalConnectionFilter) (types.DalConnectionSet, types.DalConnectionFilter, error)
}
federationNodeService interface {
Search(ctx context.Context, filter federationTypes.NodeFilter) (set federationTypes.NodeSet, f federationTypes.NodeFilter, err error)
}
)
func (DalConnection) New() *DalConnection {
return &DalConnection{
svc: service.DefaultDalConnection,
federationSvc: federationService.DefaultNode,
connectionAc: service.DefaultAccessControl,
}
}
func (ctrl DalConnection) List(ctx context.Context, r *request.DalConnectionList) (interface{}, error) {
var (
err error
dalConnections types.DalConnectionSet
f = types.DalConnectionFilter{
DalConnectionID: r.ConnectionID,
Handle: r.Handle,
Type: r.Type,
Deleted: filter.State(r.Deleted),
}
)
if f.Deleted == 0 {
f.Deleted = filter.StateExcluded
}
f.IncTotal = r.IncTotal
dalConnections, f, err = ctrl.collectConnections(ctx, f)
if err != nil {
return nil, err
}
return ctrl.makeFilterPayload(ctx, dalConnections, f)
}
func (ctrl DalConnection) Create(ctx context.Context, r *request.DalConnectionCreate) (interface{}, error) {
connection := &types.DalConnection{
Handle: r.Handle,
Type: r.Type,
Meta: r.Meta,
Config: r.Config,
}
res, err := ctrl.svc.Create(ctx, connection)
if err != nil {
return nil, err
}
return ctrl.makePayload(ctx, res), nil
}
func (ctrl DalConnection) Update(ctx context.Context, r *request.DalConnectionUpdate) (interface{}, error) {
connection := &types.DalConnection{
ID: r.ConnectionID,
Handle: r.Handle,
Type: r.Type,
Meta: r.Meta,
Config: r.Config,
UpdatedAt: r.UpdatedAt,
}
res, err := ctrl.svc.Update(ctx, connection)
if err != nil {
return nil, err
}
return ctrl.makePayload(ctx, res), nil
}
func (ctrl DalConnection) Read(ctx context.Context, r *request.DalConnectionRead) (interface{}, error) {
res, err := ctrl.svc.FindByID(ctx, r.ConnectionID)
if err != nil {
return nil, err
}
return ctrl.makePayload(ctx, res), nil
}
func (ctrl DalConnection) Delete(ctx context.Context, r *request.DalConnectionDelete) (interface{}, error) {
return api.OK(), ctrl.svc.DeleteByID(ctx, r.ConnectionID)
}
func (ctrl DalConnection) Undelete(ctx context.Context, r *request.DalConnectionUndelete) (interface{}, error) {
return api.OK(), ctrl.svc.UndeleteByID(ctx, r.ConnectionID)
}
func (ctrl DalConnection) makeFilterPayload(ctx context.Context, connections types.DalConnectionSet, f types.DalConnectionFilter) (out *connectionSetPayload, err error) {
out = &connectionSetPayload{
Filter: f,
Set: make([]*connectionPayload, 0, len(connections)),
}
for _, c := range connections {
out.Set = append(out.Set, ctrl.makePayload(ctx, c))
}
return
}
// Make payload for dal-connection
//
// # Payload is without connection params on the config prop
//
// An explicit call to /params
func (ctrl DalConnection) makePayload(ctx context.Context, c *types.DalConnection) *connectionPayload {
return &connectionPayload{
DalConnection: c,
CanGrant: ctrl.connectionAc.CanGrant(ctx),
CanUpdateConnection: ctrl.connectionAc.CanUpdateDalConnection(ctx, c),
CanDeleteConnection: ctrl.connectionAc.CanDeleteDalConnection(ctx, c),
CanManageDalConfig: ctrl.connectionAc.CanManageDalConfigOnDalConnection(ctx, c),
}
}
func (ctrl DalConnection) federatedNodeToConnection(f *federationTypes.Node) *types.DalConnection {
h, _ := handle.Cast(nil, f.Name)
return &types.DalConnection{
ID: f.ID,
Meta: types.ConnectionMeta{
Name: f.Name,
Ownership: f.Contact,
},
Handle: h,
Type: federationTypes.NodeResourceType,
//Config: types.ConnectionConfig{
// Connection: dal.NewFederatedNodeConnection(f.BaseURL, f.PairToken, f.AuthToken),
//},
CreatedAt: f.CreatedAt,
CreatedBy: f.CreatedBy,
UpdatedAt: f.UpdatedAt,
UpdatedBy: f.UpdatedBy,
DeletedAt: f.DeletedAt,
DeletedBy: f.DeletedBy,
}
}
func (ctrl DalConnection) collectConnections(ctx context.Context, filter types.DalConnectionFilter) (out types.DalConnectionSet, f types.DalConnectionFilter, err error) {
var (
dalConnections types.DalConnectionSet
federatedNodes federationTypes.NodeSet
)
if dalConnections, f, err = ctrl.svc.Search(ctx, filter); err != nil {
return nil, f, err
}
if !reflect2.IsNil(ctrl.federationSvc) {
if federatedNodes, _, err = ctrl.federationSvc.Search(ctx, federationTypes.NodeFilter{
// @todo IDs?
Deleted: f.Deleted,
}); err != nil {
return nil, f, err
}
}
out = append(out, dalConnections...)
// We're converting federation nodes to DAL connection structs so that we have
// a unified output.
//
// Eventually federation nodes will become connections, so this is ok
for _, nn := range federatedNodes {
out = append(out, ctrl.federatedNodeToConnection(nn))
}
out = ctrl.filterConnections(out, f)
return
}
func (ctrl DalConnection) filterConnections(baseConnections types.DalConnectionSet, f types.DalConnectionFilter) (out types.DalConnectionSet) {
for _, conn := range baseConnections {
include := true
if len(f.DalConnectionID) > 0 {
include = include && ctrl.inIDSet(id.Uints(f.DalConnectionID...), conn.ID)
}
if f.Handle != "" {
include = include && f.Handle == conn.Handle
}
if f.Type != "" {
include = include && f.Type == conn.Type
}
{
if f.Deleted == filter.StateExcluded {
include = include && conn.DeletedAt == nil
}
if f.Deleted == filter.StateExclusive {
include = include && conn.DeletedAt != nil
}
}
if include {
out = append(out, conn)
}
}
return
}
func (ctrl DalConnection) inIDSet(set []uint64, target uint64) (out bool) {
for _, id := range set {
out = out || id == target
}
return
}
func (ctrl DalConnection) serve(ctx context.Context, fn string, archive io.ReadSeeker, err error) (interface{}, error) {
if err != nil {
return nil, err
}
return func(w http.ResponseWriter, req *http.Request) {
w.Header().Add("Content-Disposition", "attachment; filename="+fn)
http.ServeContent(w, req, fn, time.Now(), archive)
}, nil
}

View File

@@ -0,0 +1,47 @@
package rest
import (
"context"
"io"
"net/http"
"time"
"github.com/cortezaproject/corteza/server/pkg/dal"
"github.com/cortezaproject/corteza/server/system/rest/request"
)
type (
DalDriver struct{}
dalDriverSetPayload struct {
Set []dal.Driver `json:"set"`
}
)
func (DalDriver) New() *DalDriver {
return &DalDriver{}
}
func (ctrl DalDriver) List(ctx context.Context, r *request.DalDriverList) (interface{}, error) {
return ctrl.makeFilterPayload(ctx, dal.Service().Drivers())
}
func (ctrl DalDriver) makeFilterPayload(ctx context.Context, drivers []dal.Driver) (out *dalDriverSetPayload, err error) {
out = &dalDriverSetPayload{
Set: drivers,
}
return
}
func (ctrl DalDriver) serve(ctx context.Context, fn string, archive io.ReadSeeker, err error) (interface{}, error) {
if err != nil {
return nil, err
}
return func(w http.ResponseWriter, req *http.Request) {
w.Header().Add("Content-Disposition", "attachment; filename="+fn)
http.ServeContent(w, req, fn, time.Now(), archive)
}, nil
}

View File

@@ -0,0 +1,129 @@
package rest
import (
"context"
"github.com/cortezaproject/corteza/server/pkg/api"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/pkg/id"
"github.com/cortezaproject/corteza/server/system/rest/request"
"github.com/cortezaproject/corteza/server/system/service"
"github.com/cortezaproject/corteza/server/system/types"
)
type (
DalSchemaAlteration struct {
svc alterationService
federationSvc federationNodeService
alterationAc alterationAccessController
}
alterationPayload struct {
*types.DalSchemaAlteration
CanGrant bool `json:"canGrant"`
CanUpdateSchemaAlteration bool `json:"canUpdateSchemaAlteration"`
CanDeleteSchemaAlteration bool `json:"canDeleteSchemaAlteration"`
CanManageDalConfig bool `json:"canManageDalConfig"`
}
alterationSetPayload struct {
Filter types.DalSchemaAlterationFilter `json:"filter"`
Set []*alterationPayload `json:"set"`
}
alterationAccessController interface {
CanGrant(context.Context) bool
// @todo?
}
alterationService interface {
Search(ctx context.Context, filter types.DalSchemaAlterationFilter) (types.DalSchemaAlterationSet, types.DalSchemaAlterationFilter, error)
FindByID(ctx context.Context, ID uint64) (*types.DalSchemaAlteration, error)
Apply(context.Context, ...uint64) error
Dismiss(context.Context, ...uint64) error
}
)
func (DalSchemaAlteration) New() *DalSchemaAlteration {
return &DalSchemaAlteration{
svc: service.DefaultDalSchemaAlteration,
alterationAc: service.DefaultAccessControl,
}
}
func (ctrl DalSchemaAlteration) List(ctx context.Context, r *request.DalSchemaAlterationList) (interface{}, error) {
var (
err error
set types.DalSchemaAlterationSet
f = types.DalSchemaAlterationFilter{
AlterationID: r.AlterationID,
BatchID: id.Uints(r.BatchID...),
Kind: r.Kind,
Resource: r.Resource,
ResourceType: r.ResourceType,
Deleted: filter.State(r.Deleted),
Dismissed: filter.State(r.Dismissed),
Completed: filter.State(r.Completed),
}
)
if f.Deleted == 0 {
f.Deleted = filter.StateExcluded
}
if f.Dismissed == 0 {
f.Dismissed = filter.StateExcluded
}
if f.Completed == 0 {
f.Completed = filter.StateExcluded
}
f.IncTotal = r.IncTotal
set, f, err = ctrl.svc.Search(ctx, f)
return ctrl.makeFilterPayload(ctx, set, f, err)
}
func (ctrl DalSchemaAlteration) Read(ctx context.Context, r *request.DalSchemaAlterationRead) (interface{}, error) {
res, err := ctrl.svc.FindByID(ctx, r.AlterationID)
return ctrl.makePayload(ctx, res, err)
}
func (ctrl DalSchemaAlteration) Apply(ctx context.Context, r *request.DalSchemaAlterationApply) (interface{}, error) {
return api.OK(), ctrl.svc.Apply(ctx, r.AlterationID...)
}
func (ctrl DalSchemaAlteration) Dismiss(ctx context.Context, r *request.DalSchemaAlterationDismiss) (interface{}, error) {
return api.OK(), ctrl.svc.Dismiss(ctx, r.AlterationID...)
}
func (ctrl DalSchemaAlteration) makePayload(ctx context.Context, res *types.DalSchemaAlteration, err error) (*alterationPayload, error) {
if err != nil || res == nil {
return nil, err
}
pl := &alterationPayload{
DalSchemaAlteration: res,
CanGrant: ctrl.alterationAc.CanGrant(ctx),
}
return pl, nil
}
func (ctrl DalSchemaAlteration) makeFilterPayload(ctx context.Context, rr types.DalSchemaAlterationSet, f types.DalSchemaAlterationFilter, err error) (*alterationSetPayload, error) {
if err != nil {
return nil, err
}
out := &alterationSetPayload{Filter: f, Set: make([]*alterationPayload, len(rr))}
for i := range rr {
out.Set[i], _ = ctrl.makePayload(ctx, rr[i], nil)
}
return out, nil
}

View File

@@ -0,0 +1,163 @@
package rest
import (
"context"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/pkg/id"
"github.com/cortezaproject/corteza/server/system/rest/request"
"github.com/cortezaproject/corteza/server/system/service"
"github.com/cortezaproject/corteza/server/system/types"
)
type (
DataPrivacy struct {
privacy service.DataPrivacyService
ac privacyAccessController
}
privacyAccessController interface {
CanGrant(context.Context) bool
}
privacyConnectionSetPayload struct {
Filter types.DalConnectionFilter `json:"filter"`
Set types.PrivacyDalConnectionSet `json:"set"`
}
)
func (DataPrivacy) New() *DataPrivacy {
return &DataPrivacy{
privacy: service.DefaultDataPrivacy,
ac: service.DefaultAccessControl,
}
}
func (ctrl DataPrivacy) ConnectionList(ctx context.Context, r *request.DataPrivacyConnectionList) (interface{}, error) {
var (
err error
set types.PrivacyDalConnectionSet
f = types.DalConnectionFilter{
DalConnectionID: r.ConnectionID,
Handle: r.Handle,
Type: r.Type,
Deleted: r.Deleted,
}
)
set, f, err = ctrl.privacy.FindConnections(ctx, f)
return ctrl.makeFilterConnectionPayload(ctx, set, f, err)
}
func (ctrl DataPrivacy) makeFilterConnectionPayload(_ context.Context, rr types.PrivacyDalConnectionSet, f types.DalConnectionFilter, err error) (*privacyConnectionSetPayload, error) {
if err != nil {
return nil, err
}
if len(rr) == 0 {
rr = make([]*types.PrivacyDalConnection, 0)
}
return &privacyConnectionSetPayload{Filter: f, Set: rr}, nil
}
func (ctrl DataPrivacy) RequestList(ctx context.Context, r *request.DataPrivacyRequestList) (interface{}, error) {
var (
err error
f = types.DataPrivacyRequestFilter{
RequestedBy: r.RequestedBy,
Query: r.Query,
Kind: r.Kind,
Status: r.Status,
}
)
if f.Paging, err = filter.NewPaging(r.Limit, r.PageCursor); err != nil {
return nil, err
}
if f.Sorting, err = filter.NewSorting(r.Sort); err != nil {
return nil, err
}
set, f, err := ctrl.privacy.FindRequests(ctx, f)
return ctrl.makeFilterRequestPayload(ctx, set, f, err)
}
func (ctrl DataPrivacy) makeFilterRequestPayload(_ context.Context, rr types.DataPrivacyRequestSet, f types.DataPrivacyRequestFilter, err error) (*dataPrivacyRequestSetPayload, error) {
if err != nil {
return nil, err
}
if len(rr) == 0 {
rr = make([]*types.DataPrivacyRequest, 0)
}
return &dataPrivacyRequestSetPayload{Filter: f, Set: rr}, nil
}
func (ctrl DataPrivacy) RequestCreate(ctx context.Context, r *request.DataPrivacyRequestCreate) (interface{}, error) {
req := &types.DataPrivacyRequest{
Kind: types.CastToRequestKind(r.Kind),
Payload: r.Payload,
}
return ctrl.privacy.CreateRequest(ctx, req)
}
func (ctrl DataPrivacy) RequestRead(ctx context.Context, r *request.DataPrivacyRequestRead) (interface{}, error) {
return ctrl.privacy.FindRequestByID(ctx, r.RequestID)
}
func (ctrl DataPrivacy) RequestUpdateStatus(ctx context.Context, r *request.DataPrivacyRequestUpdateStatus) (interface{}, error) {
req := &types.DataPrivacyRequest{
ID: r.RequestID,
Status: types.CastToRequestStatus(r.Status),
}
return ctrl.privacy.UpdateRequestStatus(ctx, req)
}
func (ctrl DataPrivacy) RequestCommentList(ctx context.Context, r *request.DataPrivacyRequestCommentList) (interface{}, error) {
var (
err error
f = types.DataPrivacyRequestCommentFilter{
RequestID: id.Strings(r.RequestID),
}
)
if f.Paging, err = filter.NewPaging(r.Limit, r.PageCursor); err != nil {
return nil, err
}
if f.Sorting, err = filter.NewSorting(r.Sort); err != nil {
return nil, err
}
set, f, err := ctrl.privacy.FindRequestComments(ctx, f)
return ctrl.makeFilterRequestCommentPayload(ctx, set, f, err)
}
func (ctrl DataPrivacy) makeFilterRequestCommentPayload(_ context.Context, rr types.DataPrivacyRequestCommentSet, f types.DataPrivacyRequestCommentFilter, err error) (*dataPrivacyRequestCommentSetPayload, error) {
if err != nil {
return nil, err
}
if len(rr) == 0 {
rr = make([]*types.DataPrivacyRequestComment, 0)
}
return &dataPrivacyRequestCommentSetPayload{Filter: f, Set: rr}, nil
}
func (ctrl DataPrivacy) RequestCommentCreate(ctx context.Context, r *request.DataPrivacyRequestCommentCreate) (interface{}, error) {
req := &types.DataPrivacyRequestComment{
RequestID: r.RequestID,
Comment: r.Comment,
}
return ctrl.privacy.CreateRequestComment(ctx, req)
}

Some files were not shown because too many files have changed in this diff Show More