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
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:
102
server/system/apigw_filter.cue
Normal file
102
server/system/apigw_filter.cue
Normal 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
|
||||
"""
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
132
server/system/apigw_route.cue
Normal file
132
server/system/apigw_route.cue
Normal 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
|
||||
"""
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
120
server/system/application.cue
Normal file
120
server/system/application.cue
Normal 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"}]
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
69
server/system/attachment.cue
Normal file
69
server/system/attachment.cue
Normal 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"] },
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
137
server/system/auth_client.cue
Normal file
137
server/system/auth_client.cue
Normal 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
|
||||
"""
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
59
server/system/auth_confirmed_client.cue
Normal file
59
server/system/auth_confirmed_client.cue
Normal 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"] },
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
93
server/system/auth_oa2token.cue
Normal file
93
server/system/auth_oa2token.cue
Normal 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" } ] },
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
74
server/system/auth_session.cue
Normal file
74
server/system/auth_session.cue
Normal 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" } ] },
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
444
server/system/automation/actionlog_handler.gen.go
generated
Normal file
444
server/system/automation/actionlog_handler.gen.go
generated
Normal 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)
|
||||
},
|
||||
}
|
||||
}
|
||||
154
server/system/automation/actionlog_handler.go
Normal file
154
server/system/automation/actionlog_handler.go
Normal 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
|
||||
}
|
||||
78
server/system/automation/actionlog_handler.yaml
Normal file
78
server/system/automation/actionlog_handler.yaml
Normal 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
1610
server/system/automation/expr_types.gen.go
generated
Normal file
File diff suppressed because it is too large
Load Diff
325
server/system/automation/expr_types.go
Normal file
325
server/system/automation/expr_types.go
Normal 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)
|
||||
}
|
||||
97
server/system/automation/expr_types.yaml
Normal file
97
server/system/automation/expr_types.yaml
Normal 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'
|
||||
148
server/system/automation/expr_types_test.go
Normal file
148
server/system/automation/expr_types_test.go
Normal 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")
|
||||
})
|
||||
}
|
||||
124
server/system/automation/notification_handler.gen.go
generated
Normal file
124
server/system/automation/notification_handler.gen.go
generated
Normal 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 = ¬ificationSendArgs{
|
||||
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)
|
||||
},
|
||||
}
|
||||
}
|
||||
104
server/system/automation/notification_handler.go
Normal file
104
server/system/automation/notification_handler.go
Normal 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 := ¬ificationHandler{
|
||||
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
|
||||
}
|
||||
30
server/system/automation/notification_handler.yaml
Normal file
30
server/system/automation/notification_handler.yaml
Normal 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 } } }
|
||||
412
server/system/automation/rbac_handler.gen.go
generated
Normal file
412
server/system/automation/rbac_handler.gen.go
generated
Normal 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
|
||||
},
|
||||
}
|
||||
}
|
||||
138
server/system/automation/rbac_handler.go
Normal file
138
server/system/automation/rbac_handler.go
Normal 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
|
||||
}
|
||||
72
server/system/automation/rbac_handler.yaml
Normal file
72
server/system/automation/rbac_handler.yaml
Normal 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 }
|
||||
956
server/system/automation/reminders_handler.gen.go
generated
Normal file
956
server/system/automation/reminders_handler.gen.go
generated
Normal 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)
|
||||
},
|
||||
}
|
||||
}
|
||||
288
server/system/automation/reminders_handler.go
Normal file
288
server/system/automation/reminders_handler.go
Normal 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
|
||||
}
|
||||
173
server/system/automation/reminders_handler.yaml
Normal file
173
server/system/automation/reminders_handler.yaml
Normal 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
|
||||
1424
server/system/automation/roles_handler.gen.go
generated
Normal file
1424
server/system/automation/roles_handler.gen.go
generated
Normal file
File diff suppressed because it is too large
Load Diff
434
server/system/automation/roles_handler.go
Normal file
434
server/system/automation/roles_handler.go
Normal 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
|
||||
}
|
||||
237
server/system/automation/roles_handler.yaml
Normal file
237
server/system/automation/roles_handler.yaml
Normal 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
|
||||
1025
server/system/automation/templates_handler.gen.go
generated
Normal file
1025
server/system/automation/templates_handler.gen.go
generated
Normal file
File diff suppressed because it is too large
Load Diff
341
server/system/automation/templates_handler.go
Normal file
341
server/system/automation/templates_handler.go
Normal 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
|
||||
}
|
||||
207
server/system/automation/templates_handler.yaml
Normal file
207
server/system/automation/templates_handler.yaml
Normal 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
|
||||
141
server/system/automation/templates_handler_test.go
Normal file
141
server/system/automation/templates_handler_test.go
Normal 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)
|
||||
}
|
||||
1261
server/system/automation/users_handler.gen.go
generated
Normal file
1261
server/system/automation/users_handler.gen.go
generated
Normal file
File diff suppressed because it is too large
Load Diff
383
server/system/automation/users_handler.go
Normal file
383
server/system/automation/users_handler.go
Normal 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
|
||||
}
|
||||
215
server/system/automation/users_handler.yaml
Normal file
215
server/system/automation/users_handler.yaml
Normal 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
|
||||
113
server/system/automation/valuestore_handler.gen.go
generated
Normal file
113
server/system/automation/valuestore_handler.gen.go
generated
Normal 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
|
||||
},
|
||||
}
|
||||
}
|
||||
33
server/system/automation/valuestore_handler.go
Normal file
33
server/system/automation/valuestore_handler.go
Normal 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
|
||||
}
|
||||
21
server/system/automation/valuestore_handler.yaml
Normal file
21
server/system/automation/valuestore_handler.yaml
Normal 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
|
||||
27
server/system/commands/commands.go
Normal file
27
server/system/commands/commands.go
Normal 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())
|
||||
}
|
||||
}
|
||||
237
server/system/commands/exporter.go
Normal file
237
server/system/commands/exporter.go
Normal 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
|
||||
}
|
||||
133
server/system/commands/importer.go
Normal file
133
server/system/commands/importer.go
Normal 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
|
||||
}
|
||||
246
server/system/commands/rbac.go
Normal file
246
server/system/commands/rbac.go
Normal 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
|
||||
}
|
||||
84
server/system/commands/roles.go
Normal file
84
server/system/commands/roles.go
Normal 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)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
228
server/system/commands/settings.go
Normal file
228
server/system/commands/settings.go
Normal 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
|
||||
}
|
||||
125
server/system/commands/sink.go
Normal file
125
server/system/commands/sink.go
Normal 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
|
||||
}
|
||||
311
server/system/commands/users.go
Normal file
311
server/system/commands/users.go
Normal 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
|
||||
}
|
||||
89
server/system/component.cue
Normal file
89
server/system/component.cue
Normal 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"
|
||||
}
|
||||
}
|
||||
81
server/system/credential.cue
Normal file
81
server/system/credential.cue
Normal 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
|
||||
"""
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
103
server/system/dal_connection.cue
Normal file
103
server/system/dal_connection.cue
Normal 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)
|
||||
"""
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
113
server/system/dal_schema_alteration.cue
Normal file
113
server/system/dal_schema_alteration.cue
Normal 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.
|
||||
"""
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
76
server/system/dal_sensitivity_level.cue
Normal file
76
server/system/dal_sensitivity_level.cue
Normal 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
|
||||
"""
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
99
server/system/data_privacy_request.cue
Normal file
99
server/system/data_privacy_request.cue
Normal 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: []
|
||||
}
|
||||
}
|
||||
}
|
||||
57
server/system/data_privacy_request_comment.cue
Normal file
57
server/system/data_privacy_request_comment.cue
Normal 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
1294
server/system/envoy/store_decode.gen.go
generated
Normal file
File diff suppressed because it is too large
Load Diff
110
server/system/envoy/store_decode.go
Normal file
110
server/system/envoy/store_decode.go
Normal 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
2458
server/system/envoy/store_encode.gen.go
generated
Normal file
File diff suppressed because it is too large
Load Diff
173
server/system/envoy/store_encode.go
Normal file
173
server/system/envoy/store_encode.go
Normal 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
|
||||
}
|
||||
123
server/system/envoy/store_encode_locale.go
Normal file
123
server/system/envoy/store_encode_locale.go
Normal 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
|
||||
}
|
||||
77
server/system/envoy/store_encode_rbac.go
Normal file
77
server/system/envoy/store_encode_rbac.go
Normal 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
|
||||
}
|
||||
68
server/system/envoy/store_encode_setting.go
Normal file
68
server/system/envoy/store_encode_setting.go
Normal 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
|
||||
}
|
||||
119
server/system/envoy/user_getter.go
Normal file
119
server/system/envoy/user_getter.go
Normal 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
3779
server/system/envoy/yaml_decode.gen.go
generated
Normal file
File diff suppressed because it is too large
Load Diff
211
server/system/envoy/yaml_decode.go
Normal file
211
server/system/envoy/yaml_decode.go
Normal 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
1121
server/system/envoy/yaml_encode.gen.go
generated
Normal file
File diff suppressed because it is too large
Load Diff
339
server/system/envoy/yaml_encode.go
Normal file
339
server/system/envoy/yaml_encode.go
Normal 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
466
server/system/model/corteza.gen.go
generated
Normal 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
26
server/system/model/init.gen.go
generated
Normal 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
2908
server/system/model/models.gen.go
generated
Normal file
File diff suppressed because it is too large
Load Diff
65
server/system/notification.cue
Normal file
65
server/system/notification.cue
Normal 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
100
server/system/queue.cue
Normal 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
|
||||
"""
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
54
server/system/queue_message.cue
Normal file
54
server/system/queue_message.cue
Normal 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: []
|
||||
}
|
||||
}
|
||||
}
|
||||
74
server/system/reminder.cue
Normal file
74
server/system/reminder.cue
Normal 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"] }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
9
server/system/renderer/funcs.go
Normal file
9
server/system/renderer/funcs.go
Normal 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)
|
||||
}
|
||||
}
|
||||
108
server/system/renderer/generic_html.go
Normal file
108
server/system/renderer/generic_html.go
Normal 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
|
||||
}
|
||||
103
server/system/renderer/generic_html_test.go
Normal file
103
server/system/renderer/generic_html_test.go
Normal 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><h1>Title 1</h1></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)
|
||||
}
|
||||
}
|
||||
74
server/system/renderer/generic_text.go
Normal file
74
server/system/renderer/generic_text.go
Normal 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
|
||||
}
|
||||
387
server/system/renderer/gotenbergPDF.go
Normal file
387
server/system/renderer/gotenbergPDF.go
Normal 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 "", ""
|
||||
}
|
||||
87
server/system/renderer/html.go
Normal file
87
server/system/renderer/html.go
Normal 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
|
||||
}
|
||||
59
server/system/renderer/renderer.go
Normal file
59
server/system/renderer/renderer.go
Normal 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
|
||||
}
|
||||
43
server/system/renderer/text.go
Normal file
43
server/system/renderer/text.go
Normal 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
|
||||
}
|
||||
65
server/system/renderer/types.go
Normal file
65
server/system/renderer/types.go
Normal 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
111
server/system/report.cue
Normal 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 }
|
||||
}
|
||||
139
server/system/reporting/frame.go
Normal file
139
server/system/reporting/frame.go
Normal 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
|
||||
}
|
||||
141
server/system/reporting/frame_builder.go
Normal file
141
server/system/reporting/frame_builder.go
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
741
server/system/reporting/reporting.go
Normal file
741
server/system/reporting/reporting.go
Normal 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
|
||||
}
|
||||
328
server/system/reporting/reporting_test.go
Normal file
328
server/system/reporting/reporting_test.go
Normal 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])
|
||||
}
|
||||
99
server/system/resource_translation.cue
Normal file
99
server/system/resource_translation.cue
Normal 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
2779
server/system/rest.yaml
Normal file
File diff suppressed because it is too large
Load Diff
194
server/system/rest/actionlog.go
Normal file
194
server/system/rest/actionlog.go
Normal 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)
|
||||
}
|
||||
156
server/system/rest/apigw_filter.go
Normal file
156
server/system/rest/apigw_filter.go
Normal 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
|
||||
}
|
||||
180
server/system/rest/apigw_profiler.go
Normal file
180
server/system/rest/apigw_profiler.go
Normal 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
|
||||
}
|
||||
162
server/system/rest/apigw_route.go
Normal file
162
server/system/rest/apigw_route.go
Normal 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
|
||||
}
|
||||
254
server/system/rest/application.go
Normal file
254
server/system/rest/application.go
Normal 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
|
||||
}
|
||||
187
server/system/rest/attachment.go
Normal file
187
server/system/rest/attachment.go
Normal 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
|
||||
}
|
||||
81
server/system/rest/auth.go
Normal file
81
server/system/rest/auth.go
Normal 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
|
||||
}
|
||||
183
server/system/rest/auth_client.go
Normal file
183
server/system/rest/auth_client.go
Normal 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
|
||||
}
|
||||
48
server/system/rest/automation.go
Normal file
48
server/system/rest/automation.go
Normal 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))
|
||||
}
|
||||
295
server/system/rest/dal_connection.go
Normal file
295
server/system/rest/dal_connection.go
Normal 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
|
||||
}
|
||||
47
server/system/rest/dal_drivers.go
Normal file
47
server/system/rest/dal_drivers.go
Normal 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
|
||||
}
|
||||
129
server/system/rest/dal_schema_alteration.go
Normal file
129
server/system/rest/dal_schema_alteration.go
Normal 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
|
||||
}
|
||||
163
server/system/rest/data_privacy.go
Normal file
163
server/system/rest/data_privacy.go
Normal 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
Reference in New Issue
Block a user