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

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

View File

@@ -0,0 +1,137 @@
= Corteza Discovery
This package provides API endpoints and business logic to serve indexable resources.
== Access control
When Discovery capabilities are installed to Corteza server three combinations of clients, roles and users are provisioned:
- one auth client, role and user for *public* indexing
- one auth client, role and user for *protected* indexing
- one auth client, role and user for *private* indexing
This setup enables discovery indexing service to connect to the API with restricted privileges and have access only to explicitly allowed resources.
=== Additional limitations and protection of auth client
Each role is configured on each client as forced role and each client as impersonated user.
Auth clients are configured with `client_credentials` grant.
Additionally, auth clients only have `discovery` scope enabled.
=== Use of auth clients on indexer
Indexer supports configuration for multiple indexes with different access credentials.
This allows it to set up different indexing schemas and control how what data is available in an index.
== Index field mapping
=== Static mapping
API only serves dynamic index mappings for different types of records (per module).
Static mappings for resources like users, applications, modules, namespaces are not served from Corteza server API.
== Terminology
Index access restrictions or `iar`::
Can be one of public, private or protected.
Some resource-types can not be accessed publicly.
Index name::
Logicaly structured name of the resource index:
<prefix-for-distinction>-<iar>-<resource-like-suffix>
corteza-private-system-users
corteza-private-compose-records-1-2 (namespaceID: 1, moduleID: 2)
== REST API Endpoints
/api/discovery/resources/<iar>::
Returns resource types that can be indexable
Used when indexer need to crawl over all resources.
/api/discovery/resources/<iar>/types/<resource-type>::
IDs of indexable resources that can be indexed for a certain access restrictions.
Used when indexer need to crawl over all resources.
/api/discovery/resources/<iar>/<resource-type>/<id>::
Indexable resource with all details that can be indexed for a certain access restrictions.
/api/discovery/feed/<iar>?limit=<limit:uint>&cursor=<cursor:uint64>::
Feed of indexable resources that were changed from the cursor on.
Contains resource type, id and index ID that can be used for cursor
== Store structure
.Resource updates (`discovery_update_feed`)
[cols="1,1,2"]
|===
| Field | Type |
| `id`
| Uint64
| PK, pagination cursor
| `iar`
| Int (bitmask)
| Index access restriction: is public, protected and/or private
| `index_suffix`
| String
| Resource type
| `resource_id`
| Uint64
| Resource identifier
| `removed`
| Boolean
| Was resource removed?
| `created_at`
| Timestamp
| Data retention control
|===
== Data flow
We utilize EventBus to handle updates to indexable resources table when resources are created (`afterCreate`), or updated (`afterUpdate`, `afterDelete`).
[NOTE]
====
Corteza resources that support soft-delete are only removed from public and protected indexes.
Private index keeps deleted resources to allow searching through them.
====
== Lifecycle
=== Server start
. Register garbage collector that removes
. Register garbage collection
=== Server running
=== Server stop

View File

@@ -0,0 +1,81 @@
---
endpoints:
- path: "/mappings"
entrypoint: mappings
title: Index mappings
apis:
- name: list
method: GET
title: List of all
path: "/"
- path: "/resources"
entrypoint: resources
title: Resources
parameters:
get:
- { name: limit, type: "uint", title: "Limit" }
- { name: pageCursor, type: "string", title: "Page cursor" }
apis:
- name: systemUsers
method: GET
title: Resource
path: "/system/users"
parameters:
get:
- { name: userID, type: uint64, title: User ID }
- { name: deleted, type: uint, title: Exclude (0, default), include (1) or return only (2) deleted users }
- name: composeNamespaces
method: GET
title: Resource
path: "/compose/namespaces"
parameters:
get:
- { name: namespaceID, type: uint64, title: Namespace ID }
- { name: deleted, type: uint, title: Exclude (0, default), include (1) or return only (2) deleted namespaces }
- name: composeModules
method: GET
title: Resource
path: "/compose/namespaces/{namespaceID}/modules"
parameters:
path:
- { name: namespaceID, type: uint64, title: Namespace ID }
get:
- { name: moduleID, type: uint64, title: Module ID }
- { name: deleted, type: uint, title: Exclude (0, default), include (1) or return only (2) deleted modules }
- name: composeRecords
method: GET
title: Resource
path: "/compose/namespaces/{namespaceID}/modules/{moduleID}/records"
parameters:
path:
- { name: namespaceID, type: uint64, title: Namespace ID }
- { name: moduleID, type: uint64, title: Module ID }
get:
- { name: recordID, type: uint64, title: Record ID }
- { name: deleted, type: uint, title: Exclude (0, default), include (1) or return only (2) deleted records }
- path: "/feed"
entrypoint: feed
title: Change feed
imports:
- time
parameters:
get:
- { name: limit, type: "uint", title: "Limit" }
- { name: pageCursor, type: "string", title: "Page cursor" }
apis:
- name: changes
method: GET
title: List of resource changes that should be indexed
path: "/"
parameters:
get:
- { name: from, type: "*time.Time", title: "From timestamp" }
- { name: to, type: "*time.Time", title: "To timestamp" }

View File

@@ -0,0 +1,26 @@
package rest
import (
"context"
"github.com/cortezaproject/corteza/server/discovery/rest/internal/feed"
"github.com/cortezaproject/corteza/server/discovery/rest/request"
"time"
)
type (
feeder struct {
resActivityLog interface {
ResourceActivities(ctx context.Context, limit uint, cur string, from *time.Time, to *time.Time) (rsp *feed.Response, err error)
}
}
)
func Feed() *feeder {
return &feeder{
resActivityLog: feed.ResourceActivity(),
}
}
func (ctrl feeder) Changes(ctx context.Context, r *request.FeedChanges) (interface{}, error) {
return ctrl.resActivityLog.ResourceActivities(ctx, r.Limit, r.PageCursor, r.From, r.To)
}

View File

@@ -0,0 +1,57 @@
package handlers
// This file is auto-generated.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
//
// Definitions file that controls how this file is generated:
//
import (
"context"
"github.com/cortezaproject/corteza/server/discovery/rest/request"
"github.com/cortezaproject/corteza/server/pkg/api"
"github.com/go-chi/chi/v5"
"net/http"
)
type (
// Internal API interface
FeedAPI interface {
Changes(context.Context, *request.FeedChanges) (interface{}, error)
}
// HTTP API interface
Feed struct {
Changes func(http.ResponseWriter, *http.Request)
}
)
func NewFeed(h FeedAPI) *Feed {
return &Feed{
Changes: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewFeedChanges()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Changes(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
}
}
func (h Feed) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) {
r.Group(func(r chi.Router) {
r.Use(middlewares...)
r.Get("/feed/", h.Changes)
})
}

View File

@@ -0,0 +1,57 @@
package handlers
// This file is auto-generated.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
//
// Definitions file that controls how this file is generated:
//
import (
"context"
"github.com/cortezaproject/corteza/server/discovery/rest/request"
"github.com/cortezaproject/corteza/server/pkg/api"
"github.com/go-chi/chi/v5"
"net/http"
)
type (
// Internal API interface
MappingsAPI interface {
List(context.Context, *request.MappingsList) (interface{}, error)
}
// HTTP API interface
Mappings struct {
List func(http.ResponseWriter, *http.Request)
}
)
func NewMappings(h MappingsAPI) *Mappings {
return &Mappings{
List: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewMappingsList()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.List(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
}
}
func (h Mappings) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) {
r.Group(func(r chi.Router) {
r.Use(middlewares...)
r.Get("/mappings/", h.List)
})
}

View File

@@ -0,0 +1,114 @@
package handlers
// This file is auto-generated.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
//
// Definitions file that controls how this file is generated:
//
import (
"context"
"github.com/cortezaproject/corteza/server/discovery/rest/request"
"github.com/cortezaproject/corteza/server/pkg/api"
"github.com/go-chi/chi/v5"
"net/http"
)
type (
// Internal API interface
ResourcesAPI interface {
SystemUsers(context.Context, *request.ResourcesSystemUsers) (interface{}, error)
ComposeNamespaces(context.Context, *request.ResourcesComposeNamespaces) (interface{}, error)
ComposeModules(context.Context, *request.ResourcesComposeModules) (interface{}, error)
ComposeRecords(context.Context, *request.ResourcesComposeRecords) (interface{}, error)
}
// HTTP API interface
Resources struct {
SystemUsers func(http.ResponseWriter, *http.Request)
ComposeNamespaces func(http.ResponseWriter, *http.Request)
ComposeModules func(http.ResponseWriter, *http.Request)
ComposeRecords func(http.ResponseWriter, *http.Request)
}
)
func NewResources(h ResourcesAPI) *Resources {
return &Resources{
SystemUsers: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewResourcesSystemUsers()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.SystemUsers(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
ComposeNamespaces: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewResourcesComposeNamespaces()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.ComposeNamespaces(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
ComposeModules: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewResourcesComposeModules()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.ComposeModules(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
ComposeRecords: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewResourcesComposeRecords()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.ComposeRecords(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
}
}
func (h Resources) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) {
r.Group(func(r chi.Router) {
r.Use(middlewares...)
r.Get("/resources/system/users", h.SystemUsers)
r.Get("/resources/compose/namespaces", h.ComposeNamespaces)
r.Get("/resources/compose/namespaces/{namespaceID}/modules", h.ComposeModules)
r.Get("/resources/compose/namespaces/{namespaceID}/modules/{moduleID}/records", h.ComposeRecords)
})
}

View File

@@ -0,0 +1,551 @@
package documents
import (
"context"
"fmt"
"strconv"
"strings"
cmpService "github.com/cortezaproject/corteza/server/compose/service"
cmpTypes "github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/discovery/service"
"github.com/cortezaproject/corteza/server/pkg/errors"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/pkg/id"
"github.com/cortezaproject/corteza/server/pkg/options"
"github.com/cortezaproject/corteza/server/pkg/rbac"
sysService "github.com/cortezaproject/corteza/server/system/service"
sysTypes "github.com/cortezaproject/corteza/server/system/types"
)
type (
composeResources struct {
opt options.DiscoveryOpt
settings *sysTypes.AppSettings
rbac interface {
SignificantRoles(res rbac.Resource, op string) (aRR, dRR []uint64)
}
ac interface {
CanReadModule(ctx context.Context, r *cmpTypes.Module) bool
CanReadNamespace(ctx context.Context, r *cmpTypes.Namespace) bool
CanReadRecord(ctx context.Context, r *cmpTypes.Record) bool
CanReadRecordValueOnModuleField(ctx context.Context, r *cmpTypes.ModuleField) bool
CanReadChart(ctx context.Context, r *cmpTypes.Chart) bool
CanReadPage(ctx context.Context, r *cmpTypes.Page) bool
}
ns interface {
FindByID(context.Context, uint64) (*cmpTypes.Namespace, error)
Find(context.Context, cmpTypes.NamespaceFilter) (cmpTypes.NamespaceSet, cmpTypes.NamespaceFilter, error)
}
mod interface {
FindByID(context.Context, uint64, uint64) (*cmpTypes.Module, error)
Find(ctx context.Context, filter cmpTypes.ModuleFilter) (set cmpTypes.ModuleSet, f cmpTypes.ModuleFilter, err error)
}
rec interface {
Find(ctx context.Context, filter cmpTypes.RecordFilter) (set cmpTypes.RecordSet, f cmpTypes.RecordFilter, err error)
}
page interface {
Find(ctx context.Context, filter cmpTypes.PageFilter) (set cmpTypes.PageSet, f cmpTypes.PageFilter, err error)
}
usr interface {
FindByID(ctx context.Context, id uint64) (*sysTypes.User, error)
}
}
pageDetail struct {
namespaceSlug string
moduleID uint64
pageID uint64
recordID uint64
}
)
var (
modulePages map[uint64]pageDetail
)
const (
maxValueLength = 512
)
func ComposeResources() *composeResources {
return &composeResources{
opt: service.DefaultOption,
settings: sysService.CurrentSettings,
rbac: rbac.Global(),
ac: cmpService.DefaultAccessControl,
ns: cmpService.DefaultNamespace,
mod: cmpService.DefaultModule,
rec: cmpService.DefaultRecord,
page: cmpService.DefaultPage,
usr: sysService.DefaultUser,
}
}
func (d composeResources) Namespaces(ctx context.Context, limit uint, cur string, namespaceID uint64, deleted uint) (rsp *Response, err error) {
return rsp, func() (err error) {
if !d.settings.Discovery.ComposeNamespaces.Enabled {
return errors.Internal("compose namespace indexing disabled")
}
var (
resourceType = "compose:namespace" // @todo use RBAC resourceType
ps cmpTypes.PageSet
pf cmpTypes.PageFilter
nss cmpTypes.NamespaceSet
f = cmpTypes.NamespaceFilter{
Deleted: filter.State(deleted),
}
)
if namespaceID > 0 {
f.NamespaceID = append(f.NamespaceID, id.String(namespaceID))
}
if f.Paging, err = filter.NewPaging(limit, cur); err != nil {
return err
}
if nss, f, err = d.ns.Find(ctx, f); err != nil {
return err
}
rsp = &Response{
Documents: make([]Document, len(nss)),
Filter: Filter{
Limit: limit,
NextPage: f.NextPage,
},
}
for i, ns := range nss {
nsID := ns.ID
nsSlug := ns.Slug
rsp.Documents[i].ID = nsID
pf.NamespaceID = nsID
// namespace pages
if ps, pf, err = d.page.Find(ctx, pf); err != nil {
return err
}
if modulePages == nil {
modulePages = make(map[uint64]pageDetail)
}
for _, p := range ps {
moduleID := p.ModuleID
if len(nsSlug) > 0 && moduleID > 0 {
modulePages[moduleID] = pageDetail{
namespaceSlug: nsSlug,
moduleID: moduleID,
pageID: p.ID,
}
}
}
// where should this link to?
// namespace root page on the compose?
// rsp.Documents[i].URL = "" // added to source
doc := &docComposeNamespace{
ResourceType: resourceType,
NamespaceID: nsID,
Name: ns.Name,
Handle: nsSlug,
Url: d.getUrlToResource(pageDetail{namespaceSlug: nsSlug}),
Meta: docPartialComposeNamespaceMeta{
Subtitle: ns.Meta.Subtitle,
Description: ns.Meta.Description,
},
Created: makePartialChange(&ns.CreatedAt),
Updated: makePartialChange(ns.UpdatedAt),
Deleted: makePartialChange(ns.DeletedAt),
Namespace: docPartialComposeNamespace{
NamespaceID: nsID,
Name: ns.Name,
Handle: nsSlug,
},
CatchAll: []any{nsID, ns.Name, nsSlug},
}
allowedRoles, deniedRoles := d.rbac.SignificantRoles(ns, "read")
doc.Security = append(doc.Security, docSecurity{
AllowedRoles: stringifyUints64(allowedRoles),
DeniedRoles: stringifyUints64(deniedRoles),
})
rsp.Documents[i].Source = doc
}
return nil
}()
}
func (d composeResources) Modules(ctx context.Context, namespaceID uint64, limit uint, cur string, moduleID uint64, deleted uint) (rsp *Response, err error) {
return rsp, func() (err error) {
if !d.settings.Discovery.ComposeModules.Enabled {
return errors.Internal("compose module indexing disabled")
}
var (
ns *cmpTypes.Namespace
mm cmpTypes.ModuleSet
f = cmpTypes.ModuleFilter{
NamespaceID: namespaceID,
Deleted: filter.State(deleted),
}
)
if moduleID > 0 {
f.ModuleID = append(f.ModuleID, id.String(moduleID))
}
if f.Paging, err = filter.NewPaging(limit, cur); err != nil {
return
}
if mm, f, err = d.mod.Find(ctx, f); err != nil {
return
}
rsp = &Response{
Documents: make([]Document, len(mm)),
Filter: Filter{
Limit: limit,
NextPage: f.NextPage,
},
}
if ns, err = d.ns.FindByID(ctx, namespaceID); err != nil {
return
}
nsPartial := docPartialComposeNamespace{
NamespaceID: namespaceID,
Name: ns.Name,
Handle: ns.Slug,
}
for i, mod := range mm {
rsp.Documents[i].ID = mod.ID
// Where should this link to?
// module edit screen in the administration? does this make sense?
//rsp.Documents[i].URL = "@todo"
doc := &docComposeModule{
ResourceType: "compose:module", // @todo use RBAC resourceType
ModuleID: mod.ID,
Name: mod.Name,
Handle: mod.Handle,
Fields: func() []*docPartialComposeModuleField {
out := make([]*docPartialComposeModuleField, len(mod.Fields))
for i, f := range mod.Fields {
out[i] = &docPartialComposeModuleField{
Name: f.Name,
Label: f.Label,
}
}
return out
}(),
Labels: func() map[string]string {
labels := make(map[string]string, len(mod.Labels))
for k, v := range mod.Labels {
if v.Val != "" {
labels[k] = v.Val
} else if len(v.Values) > 0 {
labels[k] = strings.Join(v.Values, ",")
}
}
return labels
}(),
Created: makePartialChange(&mod.CreatedAt),
Updated: makePartialChange(mod.UpdatedAt),
Deleted: makePartialChange(mod.DeletedAt),
CatchAll: []any{mod.ID, mod.Name, mod.Handle},
Namespace: nsPartial,
Module: docPartialComposeModule{
ModuleID: mod.ID,
Name: mod.Name,
Handle: mod.Handle,
},
}
for _, f := range mod.Fields {
if f.Name != "" {
doc.CatchAll = append(doc.CatchAll, f.Name)
}
if f.Label != "" {
doc.CatchAll = append(doc.CatchAll, f.Label)
}
}
allowedRoles, deniedRoles := d.rbac.SignificantRoles(mod, "read")
doc.Security = append(doc.Security, docSecurity{
AllowedRoles: stringifyUints64(allowedRoles),
DeniedRoles: stringifyUints64(deniedRoles),
})
rsp.Documents[i].Source = doc
}
return nil
}()
}
func (d composeResources) Records(ctx context.Context, namespaceID, moduleID uint64, limit uint, cur string, recordID uint64, deleted uint) (rsp *Response, err error) {
return rsp, func() (err error) {
if !d.settings.Discovery.ComposeRecords.Enabled {
return errors.Internal("compose record indexing disabled")
}
var (
ns *cmpTypes.Namespace
mod *cmpTypes.Module
rr cmpTypes.RecordSet
f = cmpTypes.RecordFilter{
NamespaceID: namespaceID,
ModuleID: moduleID,
Deleted: filter.State(deleted),
}
)
if recordID > 0 {
f.Query = fmt.Sprintf("recordID=%d", recordID)
}
if f.Paging, err = filter.NewPaging(limit, cur); err != nil {
return err
}
if rr, f, err = d.rec.Find(ctx, f); err != nil {
return err
}
rsp = &Response{
Documents: make([]Document, len(rr)),
Filter: Filter{
Limit: limit,
NextPage: f.NextPage,
},
}
// @todo handle unreadable (access-control) namespaces
if ns, err = d.ns.FindByID(ctx, namespaceID); err != nil {
return
}
nsPartial := docPartialComposeNamespace{
NamespaceID: namespaceID,
Name: ns.Name,
Handle: ns.Slug,
}
// @todo handle unreadable (access-control) modules
if mod, err = d.mod.FindByID(ctx, namespaceID, moduleID); err != nil {
return
}
modPartial := docPartialComposeModule{
ModuleID: f.ModuleID,
Name: mod.Name,
Handle: mod.Handle,
}
users := make(map[uint64]sysTypes.User)
for i, rec := range rr {
recordID := rec.ID
rsp.Documents[i].ID = recordID
// where should this link to? record page in the compose?
// rsp.Documents[i].URL = "" // added to source
// fixme refactor me
var (
createdBy *sysTypes.User
updatedBy *sysTypes.User
deletedBy *sysTypes.User
)
if rec.CreatedBy > 0 {
createdBy, users, err = d.getUserInfo(ctx, rec.CreatedBy, users)
if err != nil {
return
}
}
if rec.UpdatedBy > 0 {
updatedBy, users, err = d.getUserInfo(ctx, rec.UpdatedBy, users)
if err != nil {
return
}
}
if rec.DeletedBy > 0 {
deletedBy, users, err = d.getUserInfo(ctx, rec.DeletedBy, users)
if err != nil {
return
}
}
// fixme refactor me END
doc := &docComposeRecord{
ResourceType: "compose:record", // @todo use RBAC resourceType
RecordID: recordID,
Namespace: nsPartial,
Module: modPartial,
Url: d.getUrlToResource(pageDetail{moduleID: moduleID, recordID: recordID}),
Meta: rec.Meta,
//Values: d.recordValues(ctx, rec, nil),
//Created: makePartialChange(&rec.CreatedAt),
//Updated: makePartialChange(rec.UpdatedAt),
//Deleted: makePartialChange(rec.DeletedAt),
Created: makeChange(&rec.CreatedAt, createdBy),
Updated: makeChange(rec.UpdatedAt, updatedBy),
Deleted: makeChange(rec.DeletedAt, deletedBy),
}
// Values and value labels
doc.Values, doc.ValueLabels, doc.CatchAll = d.recordValues(ctx, rec, mod.Fields)
allowedRoles, deniedRoles := d.rbac.SignificantRoles(rec, "read")
doc.Security = append(doc.Security, docSecurity{
AllowedRoles: stringifyUints64(allowedRoles),
DeniedRoles: stringifyUints64(deniedRoles),
})
rsp.Documents[i].Source = doc
}
return nil
}()
}
func (d composeResources) recordValues(ctx context.Context, rec *cmpTypes.Record, ff cmpTypes.ModuleFieldSet) (map[string][]interface{}, map[string]string, []any) {
var (
rval = make(map[string][]interface{})
fields = make(map[string]string)
catchAll = make([]any, 0, len(rec.Values))
)
if rec.GetModule() == nil {
return nil, nil, nil
}
_ = rec.GetModule().Fields.Walk(func(f *cmpTypes.ModuleField) error {
if !d.ac.CanReadRecordValueOnModuleField(ctx, f) {
return nil
}
var (
rv = rec.Values.FilterByName(f.Name)
vv = make([]interface{}, 0, len(rv))
field = ff.FindByName(f.Name)
)
if len(rv) == 0 {
return nil
}
for _, val := range rv {
// refs needs to be casted to string (json & unsigned 64-bit integers)!
if f.IsRef() {
aux := fmt.Sprintf("%d", val.Ref)
vv = append(vv, aux)
catchAll = append(catchAll, aux)
continue
}
if tmp, err := val.Cast(f); err == nil {
vv = append(vv, tmp)
}
catchAll = append(catchAll, clampString(val.Value, maxValueLength))
}
if len(vv) == 0 {
return nil
}
rval[f.Name] = vv
if field != nil {
fields[f.Name] = field.Label
}
return nil
})
return rval, fields, catchAll
}
// getUrlToResource construct page url for compose resources
func (d composeResources) getUrlToResource(page pageDetail) (url string) {
var (
host = d.opt.CortezaDomain
validNsSlung = len(page.namespaceSlug) > 0
validModuleID = page.moduleID > 0
validRecord = page.recordID > 0
validPageID = page.pageID > 0
)
if len(host) == 0 {
return
}
if validModuleID && validRecord {
// For record, take moduleID & recordID from params(page) then construct url from modulePages map
if p, is := modulePages[page.moduleID]; is {
url = fmt.Sprintf("%s/compose/ns/%s/pages/%d/record/%d", host, p.namespaceSlug, p.pageID, page.recordID)
}
} else if validNsSlung && validPageID {
// for module? @todo, we need moduleID ref for module home page
// we can check page.self_id is 0 that is modules home pages, but ref_module is also 0
url = fmt.Sprintf("%s/compose/ns/%s/pages/%d", host, page.namespaceSlug, page.pageID)
} else if validNsSlung {
// For namespace, just send to /pages FE handles fallback to home(default) page
url = fmt.Sprintf("%s/compose/ns/%s/pages", host, page.namespaceSlug)
}
return
}
func (d composeResources) getUserInfo(ctx context.Context, ID uint64, users map[uint64]sysTypes.User) (*sysTypes.User, map[uint64]sysTypes.User, error) {
if users == nil {
users = make(map[uint64]sysTypes.User)
}
user, ok := users[ID]
if ok {
return &user, users, nil
} else {
u, err := d.usr.FindByID(ctx, ID)
if err != nil && u != nil {
users[ID] = *u
}
return u, users, err
}
}
func stringifyUints64(uints []uint64) []string {
strings := make([]string, len(uints))
for i, num := range uints {
strings[i] = strconv.FormatUint(num, 10)
}
return strings
}
func clampString(input string, maxLength int) string {
if len(input) > maxLength {
return input[:maxLength]
}
return input
}

View File

@@ -0,0 +1,172 @@
package documents
import (
"time"
"github.com/cortezaproject/corteza/server/pkg/filter"
sysTypes "github.com/cortezaproject/corteza/server/system/types"
)
type (
Response struct {
Filter Filter `json:"filter"`
Documents []Document `json:"documents"`
}
Document struct {
ID uint64 `json:"id,string"`
URL string `json:"url,omitempty"`
Source interface{} `json:"source"`
}
Filter struct {
Limit uint `json:"limit"`
NextPage *filter.PagingCursor `json:"nextPage"`
}
docPartialUser struct {
UserID uint64 `json:"userID,string"`
Email string `json:"email,omitempty"`
Name string `json:"name,omitempty"`
Username string `json:"username,omitempty"`
Handle string `json:"handle,omitempty"`
}
docPartialChange struct {
At *time.Time `json:"at,omitempty"`
By *docPartialUser `json:"by,omitempty"`
}
docUser struct {
ResourceType string `json:"resourceType"`
UserID uint64 `json:"userID,string"`
Email string `json:"email"`
Name string `json:"name,omitempty"`
Handle string `json:"handle,omitempty"`
Suspended *time.Time `json:"suspendedAt,omitempty"`
Url string `json:"url,omitempty"`
CatchAll []any `json:"catch_all"`
Updated *docPartialChange `json:"updated,omitempty"`
Created *docPartialChange `json:"created,omitempty"`
Deleted *docPartialChange `json:"deleted,omitempty"`
Security []docSecurity `json:"security"`
}
docComposeNamespace struct {
ResourceType string `json:"resourceType"`
NamespaceID uint64 `json:"namespaceID,string"`
Name string `json:"name,omitempty"`
Handle string `json:"handle,omitempty"`
Url string `json:"url,omitempty"`
Enabled bool `json:"enabled"`
Meta docPartialComposeNamespaceMeta `json:"meta"`
CatchAll []interface{} `json:"catch_all"`
Updated *docPartialChange `json:"updated,omitempty"`
Created *docPartialChange `json:"created,omitempty"`
Deleted *docPartialChange `json:"deleted,omitempty"`
Security []docSecurity `json:"security"`
// Aggregation update
Namespace docPartialComposeNamespace `json:"namespace"`
}
docPartialComposeNamespaceMeta struct {
Subtitle string `json:"subtitle,omitempty"`
Description string `json:"description,omitempty"`
}
docPartialComposeNamespace struct {
NamespaceID uint64 `json:"namespaceID,string"`
Name string `json:"name,omitempty"`
Handle string `json:"handle,omitempty"`
}
docComposeModule struct {
ResourceType string `json:"resourceType"`
ModuleID uint64 `json:"moduleID,string"`
Name string `json:"name,omitempty"`
Handle string `json:"handle,omitempty"`
Url string `json:"url,omitempty"`
Labels map[string]string `json:"labels,omitempty"`
Fields []*docPartialComposeModuleField `json:"fields"`
Updated *docPartialChange `json:"updated,omitempty"`
Created *docPartialChange `json:"created,omitempty"`
Deleted *docPartialChange `json:"deleted,omitempty"`
Security []docSecurity `json:"security"`
// Aggregation update
Namespace docPartialComposeNamespace `json:"namespace"`
Module docPartialComposeModule `json:"module"`
CatchAll []any `json:"catch_all"`
}
docPartialComposeModuleField struct {
Name string `json:"name,omitempty"`
Label string `json:"label,omitempty"`
}
docPartialComposeModule struct {
ModuleID uint64 `json:"moduleID,string"`
Name string `json:"name,omitempty"`
Handle string `json:"handle,omitempty"`
}
docComposeRecord struct {
ResourceType string `json:"resourceType"`
RecordID uint64 `json:"recordID,string"`
Url string `json:"url,omitempty"`
Meta map[string]any `json:"labels,omitempty"`
ValueLabels map[string]string `json:"valueLabels,omitempty"`
Values map[string][]interface{} `json:"values"`
CatchAll []any `json:"catch_all"`
Updated *docPartialChange `json:"updated,omitempty"`
Created *docPartialChange `json:"created,omitempty"`
Deleted *docPartialChange `json:"deleted,omitempty"`
Security []docSecurity `json:"security"`
// Aggregation update
Namespace docPartialComposeNamespace `json:"namespace"`
Module docPartialComposeModule `json:"module"`
// Vectors content field
VectorsValue []float32 `json:"vectorsValue,omitempty"`
}
docSecurity struct {
// list of roles that are allowed to read the resource
AllowedRoles []string `json:"allowedRoles"`
// list of roles that are disallowed to read the resource
DeniedRoles []string `json:"deniedRoles"`
}
)
func makePartialChange(at *time.Time) *docPartialChange {
if at == nil {
return nil
}
// @todo handle unreadable (access-control) modules
// @todo attach security info (allow/deny roles)
return &docPartialChange{At: at}
}
func makeChange(at *time.Time, user *sysTypes.User) (out *docPartialChange) {
out = &docPartialChange{}
if at != nil {
out.At = at
}
if user != nil {
out.By = &docPartialUser{
UserID: user.ID,
Email: user.Email,
Name: user.Name,
Username: user.Username,
Handle: user.Handle,
}
}
return
}

View File

@@ -0,0 +1,111 @@
package documents
import (
"context"
"fmt"
"github.com/cortezaproject/corteza/server/discovery/service"
"github.com/cortezaproject/corteza/server/pkg/errors"
"github.com/cortezaproject/corteza/server/pkg/id"
"github.com/cortezaproject/corteza/server/pkg/options"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/pkg/rbac"
sysService "github.com/cortezaproject/corteza/server/system/service"
"github.com/cortezaproject/corteza/server/system/types"
)
type (
systemResources struct {
opt options.DiscoveryOpt
settings *types.AppSettings
rbac interface {
SignificantRoles(res rbac.Resource, op string) (aRR, dRR []uint64)
}
ac interface {
CanReadUser(ctx context.Context, r *types.User) bool
}
usr interface {
Find(context.Context, types.UserFilter) (types.UserSet, types.UserFilter, error)
}
}
)
func SystemResources() *systemResources {
return &systemResources{
opt: service.DefaultOption,
settings: sysService.CurrentSettings,
rbac: rbac.Global(),
ac: sysService.DefaultAccessControl,
usr: sysService.DefaultUser,
}
}
func (d systemResources) Users(ctx context.Context, limit uint, cur string, userID uint64, deleted uint) (rsp *Response, err error) {
return rsp, func() (err error) {
if !d.settings.Discovery.SystemUsers.Enabled {
return errors.Internal("system user indexing disabled")
}
var (
uu types.UserSet
f = types.UserFilter{
Deleted: filter.State(deleted),
}
)
if userID > 0 {
f.UserID = append(f.UserID, id.String(userID))
}
if f.Paging, err = filter.NewPaging(limit, cur); err != nil {
return err
}
if uu, f, err = d.usr.Find(ctx, f); err != nil {
return err
}
rsp = &Response{
Documents: make([]Document, len(uu)),
Filter: Filter{
Limit: limit,
NextPage: f.NextPage,
},
}
for i, u := range uu {
doc := &docUser{
ResourceType: "system:user",
UserID: u.ID,
Email: u.Email,
Name: u.Name,
Handle: u.Handle,
Suspended: u.SuspendedAt,
CatchAll: []any{u.ID, u.Email, u.Name, u.Handle},
Created: makePartialChange(&u.CreatedAt),
Updated: makePartialChange(u.UpdatedAt),
Deleted: makePartialChange(u.DeletedAt),
}
if len(d.opt.CortezaDomain) > 0 && u.ID > 0 {
doc.Url = fmt.Sprintf("%s/admin/system/user/edit/%d", d.opt.CortezaDomain, u.ID)
}
allowedRoles, deniedRoles := d.rbac.SignificantRoles(u, "read")
doc.Security = append(doc.Security, docSecurity{
AllowedRoles: stringifyUints64(allowedRoles),
DeniedRoles: stringifyUints64(deniedRoles),
})
rsp.Documents[i].ID = u.ID
rsp.Documents[i].Source = doc
}
return nil
}()
}

View File

@@ -0,0 +1,28 @@
package feed
import (
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/jmoiron/sqlx/types"
"time"
)
type (
Response struct {
Filter Filter `json:"filter"`
ActivityLogs []ActivityLog `json:"activityLogs"`
}
ActivityLog struct {
ID uint64 `json:"activityID,string"`
ResourceID uint64 `json:"resourceID,string"`
ResourceType string `json:"resourceType"`
ResourceAction string `json:"resourceAction"`
Timestamp time.Time `json:"timestamp"`
Meta types.JSONText `json:"meta"`
}
Filter struct {
Limit uint `json:"limit"`
NextPage *filter.PagingCursor `json:"nextPage"`
}
)

View File

@@ -0,0 +1,105 @@
package feed
import (
"context"
"time"
"github.com/cortezaproject/corteza/server/discovery/service"
"github.com/cortezaproject/corteza/server/discovery/types"
"github.com/cortezaproject/corteza/server/pkg/errors"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/pkg/options"
"github.com/cortezaproject/corteza/server/pkg/rbac"
)
type (
resourceActivity struct {
opt options.DiscoveryOpt
rbac interface {
SignificantRoles(res rbac.Resource, op string) (aRR, dRR []uint64)
}
ac interface {
}
resActivity service.ResourceActivityService
}
)
func ResourceActivity() *resourceActivity {
return &resourceActivity{
opt: service.DefaultOption,
rbac: rbac.Global(),
resActivity: service.DefaultResourceActivity,
}
}
func (a resourceActivity) ResourceActivities(ctx context.Context, limit uint, cur string, from *time.Time, to *time.Time) (rsp *Response, err error) {
return rsp, func() (err error) {
var (
aa types.ResourceActivitySet
f = types.ResourceActivityFilter{}
)
if from == nil || from.IsZero() {
return errors.Internal("invalid or missing from timestamp")
}
if to == nil || to.IsZero() {
now := time.Now()
to = &now
}
if from.After(*to) {
return errors.Internal("invalid from timestamp, it must be before to timestamp")
}
// Ensure both 'from' and 'to' are in UTC
utcFrom := from.UTC()
utcTo := to.UTC()
from = &utcFrom
to = &utcTo
f.FromTimestamp = from
f.ToTimestamp = to
if f.Paging, err = filter.NewPaging(limit, cur); err != nil {
return err
}
if aa, f, err = a.resActivity.Find(ctx, f); err != nil {
return err
}
rsp = &Response{
ActivityLogs: make([]ActivityLog, 0),
Filter: Filter{
Limit: limit,
NextPage: f.NextPage,
},
}
err = aa.Walk(func(a *types.ResourceActivity) error {
rsp.ActivityLogs = append(rsp.ActivityLogs, ActivityLog{
ID: a.ID,
ResourceID: a.ResourceID,
ResourceType: a.ResourceType,
ResourceAction: a.ResourceAction,
Timestamp: a.Timestamp,
Meta: a.Meta,
})
return nil
})
if err != nil {
return err
}
return nil
}()
}

View File

@@ -0,0 +1,324 @@
package mapping
import (
"context"
"fmt"
"strings"
"github.com/cortezaproject/corteza/server/compose/service"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/options"
)
type (
namespaceSearcher interface {
Find(ctx context.Context, filter types.NamespaceFilter) (types.NamespaceSet, types.NamespaceFilter, error)
}
moduleSearcher interface {
Find(ctx context.Context, filter types.ModuleFilter) (types.ModuleSet, types.ModuleFilter, error)
}
composeAccessControl interface {
CanReadRecordValueOnModuleField(ctx context.Context, r *types.ModuleField) bool
CanSearchRecordsOnModule(ctx context.Context, r *types.Module) bool
}
composeMapping struct {
ac composeAccessControl
ns namespaceSearcher
mod moduleSearcher
opts options.DiscoveryOpt
}
)
func ComposeMapping(discoveryOpts options.DiscoveryOpt) *composeMapping {
return &composeMapping{
ac: service.DefaultAccessControl,
ns: service.DefaultNamespace,
mod: service.DefaultModule,
opts: discoveryOpts,
}
}
func (m composeMapping) Namespaces(ctx context.Context) ([]*Mapping, error) {
return []*Mapping{&Mapping{
Index: "compose-namespaces",
Mapping: map[string]*property{
"resourceType": {Type: "keyword"},
"namespaceID": {Type: "long"},
"name": {Type: "keyword", Boost: 2},
"handle": {Type: "keyword", Boost: 2},
"labels": {Type: "keyword"},
"enabled": {Type: "boolean"},
"url": {Type: "text"},
"meta": {Type: "object", Properties: map[string]*property{
"subtitle": {Type: "keyword"},
"description": {Type: "text"},
}},
"created": change(),
"updated": change(),
"deleted": change(),
"security": security(),
"namespace": {
Type: "nested",
Properties: map[string]*property{
"namespaceID": {Type: "long"},
"name": {Type: "text"},
"handle": {Type: "keyword"},
},
},
"catch_all": {
Type: "text",
Analyzer: "standard",
},
},
}}, nil
}
func (m composeMapping) Modules(ctx context.Context) ([]*Mapping, error) {
return []*Mapping{&Mapping{
Index: fmt.Sprintf("compose-modules"),
Mapping: map[string]*property{
"resourceType": {Type: "keyword"},
"moduleID": {Type: "long"},
"created": change(),
"updated": change(),
"deleted": change(),
"security": security(),
"name": {Type: "text", Boost: 2},
"handle": {Type: "keyword", Boost: 2},
"url": {Type: "text"},
"labels": {
Type: "object",
},
"fields": {
Type: "object",
Properties: map[string]*property{
"name": {Type: "keyword"},
"label": {Type: "text"},
},
},
"namespace": {
Type: "nested",
Properties: map[string]*property{
"namespaceID": {Type: "long"},
"name": {Type: "text"},
"handle": {Type: "keyword"},
},
},
"module": {
Type: "nested",
Properties: map[string]*property{
"moduleID": {Type: "long"},
"name": {Type: "text"},
"handle": {Type: "keyword"},
},
},
"catch_all": {
Type: "text",
Analyzer: "standard",
},
},
}}, nil
}
func (m composeMapping) Records(ctx context.Context) ([]*Mapping, error) {
var (
nn, _, err = m.ns.Find(ctx, types.NamespaceFilter{})
out = make([]*Mapping, 0, len(nn)*50)
)
if err != nil {
return nil, err
}
return out, nn.Walk(func(ns *types.Namespace) (err error) {
// @todo do we index records in this namespace?
var (
mm types.ModuleSet
)
mm, _, err = m.mod.Find(ctx, types.ModuleFilter{NamespaceID: ns.ID})
if err != nil {
return err
}
// collect record mappings for all modules
return mm.Walk(func(mod *types.Module) error {
// @todo do we index records of module?
if m := m.records(ctx, mod, mm); m != nil {
out = append(out, m)
}
return nil
})
})
}
// Module mapping is always returned, even if it contains no properties
//
// If there are no properties indexer can assume
func (m composeMapping) records(ctx context.Context, mod *types.Module, mm types.ModuleSet) (mapping *Mapping) {
// @todo check if module can be Mappinged
mapping = &Mapping{
Index: fmt.Sprintf("compose-records-%d-%d", mod.NamespaceID, mod.ID),
Mapping: make(map[string]*property),
}
props := m.moduleFieldsMappingProperties(ctx, mod, mm)
if len(props) == 0 {
// no mapping
return
}
mapping.Mapping["resourceType"] = &property{Type: "keyword"}
mapping.Mapping["recordID"] = &property{Type: "long"}
mapping.Mapping["url"] = &property{Type: "text"}
mapping.Mapping["created"] = change()
mapping.Mapping["updated"] = change()
mapping.Mapping["deleted"] = change()
mapping.Mapping["security"] = security()
mapping.Mapping["meta"] = &property{
Type: "object",
}
mapping.Mapping["values"] = &property{
Type: "nested",
Properties: props,
}
mapping.Mapping["catch_all"] = &property{
Type: "text",
Analyzer: "standard",
}
// mapping for record is there, add fields for namespace & module
mapping.Mapping["module"] = &property{
Type: "nested",
Properties: map[string]*property{
"moduleID": {Type: "long"},
"name": {Type: "text"},
"handle": {Type: "keyword"},
},
}
mapping.Mapping["namespace"] = &property{
Type: "nested",
Properties: map[string]*property{
"namespaceID": {Type: "long"},
"name": {Type: "text"},
"handle": {Type: "keyword"},
},
}
if m.opts.EmbeddingsEnabled {
mapping.Mapping["vectorsValue"] = vector(m.opts)
}
return
}
func (m composeMapping) moduleFieldsMappingProperties(ctx context.Context, mod *types.Module, mm types.ModuleSet) map[string]*property {
mf := make(map[string]*property)
for _, f := range mod.Fields {
for k, m := range m.moduleFieldMappingProperties(ctx, f, mm) {
mf[k] = m
}
}
return mf
}
func (m composeMapping) moduleFieldMappingProperties(ctx context.Context, f *types.ModuleField, mm types.ModuleSet) map[string]*property {
if !m.ac.CanReadRecordValueOnModuleField(ctx, f) {
return nil
}
var (
p = &property{Type: "text"}
name = f.Name
)
// @todo get indexing props from f.Options!
if p.Type == "" {
// Type not explicitly set, get it by converting
switch strings.ToLower(f.Kind) {
case "bool":
p.Type = "boolean"
case "datetime":
p.Type = "date"
// https://www.elastic.co/guide/en/elasticsearch/reference/current/date.html
// @todo format param!
case "email":
p.Type = "keyword"
case "file":
// @todo ref
case "number":
// @todo what number type to use?
return nil
case "record":
//iopt := f.Options.Index()
// @todo make sure we do not fall into recursive loop
// if ifc.parents[]
// @todo if single-value then object
// https://www.elastic.co/guide/en/elasticsearch/reference/current/object.html
// @todo if multi-value then nested
// https://www.elastic.co/guide/en/elasticsearch/reference/current/nested.html
//var deepDive = 1
//
//if deepDive == 0 {
// // no
// return nil
//}
//
//m := mm.FindByID(f.Options.Uint64("moduleID"))
//if m == nil {
// return nil
//}
//
//if opt.nestingLevel < 2 {
// return append(composeModuleFieldsMappingProperties(m, mm, Context{prefix: f.Name, nestingLevel: opt.nestingLevel + 1}), m)
//}
return nil
case "select":
p.Type = "keyword"
case "string":
p.Type = "text"
case "url":
p.Type = "keyword"
case "user":
// @todo ref
return nil
}
}
return map[string]*property{name: p}
}

View File

@@ -0,0 +1,70 @@
package mapping
import "github.com/cortezaproject/corteza/server/pkg/options"
type (
Mapping struct {
Index string `json:"index"`
Mapping map[string]*property `json:"mapping"`
}
property struct {
// https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-types.html
Type string `json:"type,omitempty"`
// Boost factor
// https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-boost.html
Boost float32 `json:"boost,omitempty"`
Analyzer string `json:"analyzer,omitempty"`
Properties map[string]*property `json:"properties,omitempty"`
// for vector indexing
Dimension int `json:"dimension,omitempty"`
Method map[string]any `json:"method,omitempty"`
}
Context struct {
AccessRestriction string
}
)
func change() *property {
return &property{
Type: "nested",
Properties: map[string]*property{
"at": {Type: "date"},
"by": {Type: "nested", Properties: map[string]*property{
"id": {Type: "long"},
"email": {Type: "keyword"},
"name": {Type: "keyword"},
"handle": {Type: "keyword"},
}},
},
}
}
func security() *property {
return &property{
Properties: map[string]*property{
"allowedRoles": {Type: "long"},
"deniedRoles": {Type: "long"},
},
}
}
func vector(opts options.DiscoveryOpt) *property {
return &property{
Type: "knn_vector",
Dimension: opts.EmbeddingsDimension,
Method: map[string]any{
"name": "hnsw",
"engine": "lucene",
"parameters": map[string]any{
"ef_construction": opts.HnswEfConstruction,
"m": opts.HnswM,
},
},
}
}

View File

@@ -0,0 +1,54 @@
package mapping
import (
"context"
"github.com/cortezaproject/corteza/server/system/service"
)
type (
systemAccessControl interface {
CanSearchUsers(ctx context.Context) bool
CanSearchRoles(ctx context.Context) bool
CanSearchApplications(ctx context.Context) bool
CanSearchAuthClients(ctx context.Context) bool
}
systemMapping struct {
ac systemAccessControl
}
)
func SystemMapping() *systemMapping {
return &systemMapping{
ac: service.DefaultAccessControl,
}
}
func (svc systemMapping) Users(_ context.Context) ([]*Mapping, error) {
return []*Mapping{{
Index: "system-users",
Mapping: map[string]*property{
"resourceType": {Type: "keyword"},
"userID": {Type: "long"},
"email": {Type: "keyword", Boost: 2},
"name": {Type: "keyword", Boost: 10},
"handle": {Type: "keyword", Boost: 2},
"created": change(),
"updated": change(),
"deleted": change(),
"suspendedAt": {Type: "date"},
"security": security(),
"catch_all": {
Type: "text",
Analyzer: "standard",
},
},
}}, nil
}

View File

@@ -0,0 +1,75 @@
package rest
import (
"context"
"github.com/cortezaproject/corteza/server/discovery/rest/internal/mapping"
"github.com/cortezaproject/corteza/server/discovery/rest/request"
"github.com/cortezaproject/corteza/server/pkg/options"
"github.com/cortezaproject/corteza/server/system/service"
"github.com/cortezaproject/corteza/server/system/types"
)
type (
mappings struct {
settings *types.AppSettings
sys interface {
Users(context.Context) ([]*mapping.Mapping, error)
}
cmp interface {
Namespaces(context.Context) ([]*mapping.Mapping, error)
Modules(context.Context) ([]*mapping.Mapping, error)
Records(context.Context) ([]*mapping.Mapping, error)
}
}
)
func Mappings(discoveryOpts options.DiscoveryOpt) *mappings {
return &mappings{
settings: service.CurrentSettings,
sys: mapping.SystemMapping(),
cmp: mapping.ComposeMapping(discoveryOpts),
}
}
func (ctrl mappings) List(ctx context.Context, r *request.MappingsList) (interface{}, error) {
var (
out = make([]*mapping.Mapping, 0, 100)
// Collection of all mapping functions we support
//
// Each function is responsible to
mappingFn = make([]func(ctx context.Context) ([]*mapping.Mapping, error), 4)
)
if ctrl.settings.Discovery.SystemUsers.Enabled {
mappingFn = append(mappingFn, ctrl.sys.Users)
}
if ctrl.settings.Discovery.ComposeNamespaces.Enabled {
mappingFn = append(mappingFn, ctrl.cmp.Namespaces)
}
if ctrl.settings.Discovery.ComposeModules.Enabled {
mappingFn = append(mappingFn, ctrl.cmp.Modules)
}
if ctrl.settings.Discovery.ComposeRecords.Enabled {
mappingFn = append(mappingFn, ctrl.cmp.Records)
}
return out, func() error {
for _, fn := range mappingFn {
if fn == nil {
continue
}
mm, err := fn(ctx)
if err != nil {
return err
}
out = append(out, mm...)
}
return nil
}()
}

View File

@@ -0,0 +1,129 @@
package request
// This file is auto-generated.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
//
// Definitions file that controls how this file is generated:
//
import (
"encoding/json"
"fmt"
"github.com/cortezaproject/corteza/server/pkg/payload"
"github.com/go-chi/chi/v5"
"io"
"mime/multipart"
"net/http"
"strings"
"time"
)
// dummy vars to prevent
// unused imports complain
var (
_ = chi.URLParam
_ = multipart.ErrMessageTooLarge
_ = payload.ParseUint64s
_ = strings.ToLower
_ = io.EOF
_ = fmt.Errorf
_ = json.NewEncoder
)
type (
// Internal API interface
FeedChanges struct {
// Limit GET parameter
//
// Limit
Limit uint
// PageCursor GET parameter
//
// Page cursor
PageCursor string
// From GET parameter
//
// From timestamp
From *time.Time
// To GET parameter
//
// To timestamp
To *time.Time
}
)
// NewFeedChanges request
func NewFeedChanges() *FeedChanges {
return &FeedChanges{}
}
// Auditable returns all auditable/loggable parameters
func (r FeedChanges) Auditable() map[string]interface{} {
return map[string]interface{}{
"limit": r.Limit,
"pageCursor": r.PageCursor,
"from": r.From,
"to": r.To,
}
}
// Auditable returns all auditable/loggable parameters
func (r FeedChanges) GetLimit() uint {
return r.Limit
}
// Auditable returns all auditable/loggable parameters
func (r FeedChanges) GetPageCursor() string {
return r.PageCursor
}
// Auditable returns all auditable/loggable parameters
func (r FeedChanges) GetFrom() *time.Time {
return r.From
}
// Auditable returns all auditable/loggable parameters
func (r FeedChanges) GetTo() *time.Time {
return r.To
}
// Fill processes request and fills internal variables
func (r *FeedChanges) Fill(req *http.Request) (err error) {
{
// GET params
tmp := req.URL.Query()
if val, ok := tmp["limit"]; ok && len(val) > 0 {
r.Limit, err = payload.ParseUint(val[0]), nil
if err != nil {
return err
}
}
if val, ok := tmp["pageCursor"]; ok && len(val) > 0 {
r.PageCursor, err = val[0], nil
if err != nil {
return err
}
}
if val, ok := tmp["from"]; ok && len(val) > 0 {
r.From, err = payload.ParseISODatePtrWithErr(val[0])
if err != nil {
return err
}
}
if val, ok := tmp["to"]; ok && len(val) > 0 {
r.To, err = payload.ParseISODatePtrWithErr(val[0])
if err != nil {
return err
}
}
}
return err
}

View File

@@ -0,0 +1,54 @@
package request
// This file is auto-generated.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
//
// Definitions file that controls how this file is generated:
//
import (
"encoding/json"
"fmt"
"github.com/cortezaproject/corteza/server/pkg/payload"
"github.com/go-chi/chi/v5"
"io"
"mime/multipart"
"net/http"
"strings"
)
// dummy vars to prevent
// unused imports complain
var (
_ = chi.URLParam
_ = multipart.ErrMessageTooLarge
_ = payload.ParseUint64s
_ = strings.ToLower
_ = io.EOF
_ = fmt.Errorf
_ = json.NewEncoder
)
type (
// Internal API interface
MappingsList struct {
}
)
// NewMappingsList request
func NewMappingsList() *MappingsList {
return &MappingsList{}
}
// Auditable returns all auditable/loggable parameters
func (r MappingsList) Auditable() map[string]interface{} {
return map[string]interface{}{}
}
// Fill processes request and fills internal variables
func (r *MappingsList) Fill(req *http.Request) (err error) {
return err
}

View File

@@ -0,0 +1,470 @@
package request
// This file is auto-generated.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
//
// Definitions file that controls how this file is generated:
//
import (
"encoding/json"
"fmt"
"github.com/cortezaproject/corteza/server/pkg/payload"
"github.com/go-chi/chi/v5"
"io"
"mime/multipart"
"net/http"
"strings"
)
// dummy vars to prevent
// unused imports complain
var (
_ = chi.URLParam
_ = multipart.ErrMessageTooLarge
_ = payload.ParseUint64s
_ = strings.ToLower
_ = io.EOF
_ = fmt.Errorf
_ = json.NewEncoder
)
type (
// Internal API interface
ResourcesSystemUsers struct {
// Limit GET parameter
//
// Limit
Limit uint
// PageCursor GET parameter
//
// Page cursor
PageCursor string
// UserID GET parameter
//
// User ID
UserID uint64 `json:",string"`
// Deleted GET parameter
//
// Exclude (0
Deleted uint
}
ResourcesComposeNamespaces struct {
// Limit GET parameter
//
// Limit
Limit uint
// PageCursor GET parameter
//
// Page cursor
PageCursor string
// NamespaceID GET parameter
//
// Namespace ID
NamespaceID uint64 `json:",string"`
// Deleted GET parameter
//
// Exclude (0
Deleted uint
}
ResourcesComposeModules struct {
// NamespaceID PATH parameter
//
// Namespace ID
NamespaceID uint64 `json:",string"`
// Limit GET parameter
//
// Limit
Limit uint
// PageCursor GET parameter
//
// Page cursor
PageCursor string
// ModuleID GET parameter
//
// Module ID
ModuleID uint64 `json:",string"`
// Deleted GET parameter
//
// Exclude (0
Deleted uint
}
ResourcesComposeRecords struct {
// NamespaceID PATH parameter
//
// Namespace ID
NamespaceID uint64 `json:",string"`
// ModuleID PATH parameter
//
// Module ID
ModuleID uint64 `json:",string"`
// Limit GET parameter
//
// Limit
Limit uint
// PageCursor GET parameter
//
// Page cursor
PageCursor string
// RecordID GET parameter
//
// Record ID
RecordID uint64 `json:",string"`
// Deleted GET parameter
//
// Exclude (0
Deleted uint
}
)
// NewResourcesSystemUsers request
func NewResourcesSystemUsers() *ResourcesSystemUsers {
return &ResourcesSystemUsers{}
}
// Auditable returns all auditable/loggable parameters
func (r ResourcesSystemUsers) Auditable() map[string]interface{} {
return map[string]interface{}{
"limit": r.Limit,
"pageCursor": r.PageCursor,
"userID": r.UserID,
"deleted": r.Deleted,
}
}
// Auditable returns all auditable/loggable parameters
func (r ResourcesSystemUsers) GetLimit() uint {
return r.Limit
}
// Auditable returns all auditable/loggable parameters
func (r ResourcesSystemUsers) GetPageCursor() string {
return r.PageCursor
}
// Auditable returns all auditable/loggable parameters
func (r ResourcesSystemUsers) GetUserID() uint64 {
return r.UserID
}
// Auditable returns all auditable/loggable parameters
func (r ResourcesSystemUsers) GetDeleted() uint {
return r.Deleted
}
// Fill processes request and fills internal variables
func (r *ResourcesSystemUsers) Fill(req *http.Request) (err error) {
{
// GET params
tmp := req.URL.Query()
if val, ok := tmp["limit"]; ok && len(val) > 0 {
r.Limit, err = payload.ParseUint(val[0]), nil
if err != nil {
return err
}
}
if val, ok := tmp["pageCursor"]; ok && len(val) > 0 {
r.PageCursor, err = val[0], nil
if err != nil {
return err
}
}
if val, ok := tmp["userID"]; ok && len(val) > 0 {
r.UserID, err = payload.ParseUint64(val[0]), nil
if err != nil {
return err
}
}
if val, ok := tmp["deleted"]; ok && len(val) > 0 {
r.Deleted, err = payload.ParseUint(val[0]), nil
if err != nil {
return err
}
}
}
return err
}
// NewResourcesComposeNamespaces request
func NewResourcesComposeNamespaces() *ResourcesComposeNamespaces {
return &ResourcesComposeNamespaces{}
}
// Auditable returns all auditable/loggable parameters
func (r ResourcesComposeNamespaces) Auditable() map[string]interface{} {
return map[string]interface{}{
"limit": r.Limit,
"pageCursor": r.PageCursor,
"namespaceID": r.NamespaceID,
"deleted": r.Deleted,
}
}
// Auditable returns all auditable/loggable parameters
func (r ResourcesComposeNamespaces) GetLimit() uint {
return r.Limit
}
// Auditable returns all auditable/loggable parameters
func (r ResourcesComposeNamespaces) GetPageCursor() string {
return r.PageCursor
}
// Auditable returns all auditable/loggable parameters
func (r ResourcesComposeNamespaces) GetNamespaceID() uint64 {
return r.NamespaceID
}
// Auditable returns all auditable/loggable parameters
func (r ResourcesComposeNamespaces) GetDeleted() uint {
return r.Deleted
}
// Fill processes request and fills internal variables
func (r *ResourcesComposeNamespaces) Fill(req *http.Request) (err error) {
{
// GET params
tmp := req.URL.Query()
if val, ok := tmp["limit"]; ok && len(val) > 0 {
r.Limit, err = payload.ParseUint(val[0]), nil
if err != nil {
return err
}
}
if val, ok := tmp["pageCursor"]; ok && len(val) > 0 {
r.PageCursor, err = val[0], nil
if err != nil {
return err
}
}
if val, ok := tmp["namespaceID"]; ok && len(val) > 0 {
r.NamespaceID, err = payload.ParseUint64(val[0]), nil
if err != nil {
return err
}
}
if val, ok := tmp["deleted"]; ok && len(val) > 0 {
r.Deleted, err = payload.ParseUint(val[0]), nil
if err != nil {
return err
}
}
}
return err
}
// NewResourcesComposeModules request
func NewResourcesComposeModules() *ResourcesComposeModules {
return &ResourcesComposeModules{}
}
// Auditable returns all auditable/loggable parameters
func (r ResourcesComposeModules) Auditable() map[string]interface{} {
return map[string]interface{}{
"namespaceID": r.NamespaceID,
"limit": r.Limit,
"pageCursor": r.PageCursor,
"moduleID": r.ModuleID,
"deleted": r.Deleted,
}
}
// Auditable returns all auditable/loggable parameters
func (r ResourcesComposeModules) GetNamespaceID() uint64 {
return r.NamespaceID
}
// Auditable returns all auditable/loggable parameters
func (r ResourcesComposeModules) GetLimit() uint {
return r.Limit
}
// Auditable returns all auditable/loggable parameters
func (r ResourcesComposeModules) GetPageCursor() string {
return r.PageCursor
}
// Auditable returns all auditable/loggable parameters
func (r ResourcesComposeModules) GetModuleID() uint64 {
return r.ModuleID
}
// Auditable returns all auditable/loggable parameters
func (r ResourcesComposeModules) GetDeleted() uint {
return r.Deleted
}
// Fill processes request and fills internal variables
func (r *ResourcesComposeModules) Fill(req *http.Request) (err error) {
{
// GET params
tmp := req.URL.Query()
if val, ok := tmp["limit"]; ok && len(val) > 0 {
r.Limit, err = payload.ParseUint(val[0]), nil
if err != nil {
return err
}
}
if val, ok := tmp["pageCursor"]; ok && len(val) > 0 {
r.PageCursor, err = val[0], nil
if err != nil {
return err
}
}
if val, ok := tmp["moduleID"]; ok && len(val) > 0 {
r.ModuleID, err = payload.ParseUint64(val[0]), nil
if err != nil {
return err
}
}
if val, ok := tmp["deleted"]; ok && len(val) > 0 {
r.Deleted, err = payload.ParseUint(val[0]), nil
if err != nil {
return err
}
}
}
{
var val string
// path params
val = chi.URLParam(req, "namespaceID")
r.NamespaceID, err = payload.ParseUint64(val), nil
if err != nil {
return err
}
}
return err
}
// NewResourcesComposeRecords request
func NewResourcesComposeRecords() *ResourcesComposeRecords {
return &ResourcesComposeRecords{}
}
// Auditable returns all auditable/loggable parameters
func (r ResourcesComposeRecords) Auditable() map[string]interface{} {
return map[string]interface{}{
"namespaceID": r.NamespaceID,
"moduleID": r.ModuleID,
"limit": r.Limit,
"pageCursor": r.PageCursor,
"recordID": r.RecordID,
"deleted": r.Deleted,
}
}
// Auditable returns all auditable/loggable parameters
func (r ResourcesComposeRecords) GetNamespaceID() uint64 {
return r.NamespaceID
}
// Auditable returns all auditable/loggable parameters
func (r ResourcesComposeRecords) GetModuleID() uint64 {
return r.ModuleID
}
// Auditable returns all auditable/loggable parameters
func (r ResourcesComposeRecords) GetLimit() uint {
return r.Limit
}
// Auditable returns all auditable/loggable parameters
func (r ResourcesComposeRecords) GetPageCursor() string {
return r.PageCursor
}
// Auditable returns all auditable/loggable parameters
func (r ResourcesComposeRecords) GetRecordID() uint64 {
return r.RecordID
}
// Auditable returns all auditable/loggable parameters
func (r ResourcesComposeRecords) GetDeleted() uint {
return r.Deleted
}
// Fill processes request and fills internal variables
func (r *ResourcesComposeRecords) Fill(req *http.Request) (err error) {
{
// GET params
tmp := req.URL.Query()
if val, ok := tmp["limit"]; ok && len(val) > 0 {
r.Limit, err = payload.ParseUint(val[0]), nil
if err != nil {
return err
}
}
if val, ok := tmp["pageCursor"]; ok && len(val) > 0 {
r.PageCursor, err = val[0], nil
if err != nil {
return err
}
}
if val, ok := tmp["recordID"]; ok && len(val) > 0 {
r.RecordID, err = payload.ParseUint64(val[0]), nil
if err != nil {
return err
}
}
if val, ok := tmp["deleted"]; ok && len(val) > 0 {
r.Deleted, err = payload.ParseUint(val[0]), nil
if err != nil {
return err
}
}
}
{
var val string
// path params
val = chi.URLParam(req, "namespaceID")
r.NamespaceID, err = payload.ParseUint64(val), nil
if err != nil {
return err
}
val = chi.URLParam(req, "moduleID")
r.ModuleID, err = payload.ParseUint64(val), nil
if err != nil {
return err
}
}
return err
}

View File

@@ -0,0 +1,44 @@
package rest
import (
"context"
"github.com/cortezaproject/corteza/server/discovery/rest/internal/documents"
"github.com/cortezaproject/corteza/server/discovery/rest/request"
)
type (
resources struct {
sys interface {
Users(ctx context.Context, limit uint, cur string, userID uint64, deleted uint) (*documents.Response, error)
}
cmp interface {
Namespaces(ctx context.Context, limit uint, cur string, namespaceID uint64, deleted uint) (*documents.Response, error)
Modules(ctx context.Context, namespaceID uint64, limit uint, cur string, moduleID uint64, deleted uint) (*documents.Response, error)
Records(ctx context.Context, namespaceID, moduleID uint64, limit uint, cur string, recordID uint64, deleted uint) (*documents.Response, error)
}
}
)
func Resources() *resources {
return &resources{
sys: documents.SystemResources(),
cmp: documents.ComposeResources(),
}
}
func (ctrl resources) SystemUsers(ctx context.Context, r *request.ResourcesSystemUsers) (interface{}, error) {
return ctrl.sys.Users(ctx, r.Limit, r.PageCursor, r.UserID, r.Deleted)
}
func (ctrl resources) ComposeNamespaces(ctx context.Context, r *request.ResourcesComposeNamespaces) (interface{}, error) {
return ctrl.cmp.Namespaces(ctx, r.Limit, r.PageCursor, r.NamespaceID, r.Deleted)
}
func (ctrl resources) ComposeModules(ctx context.Context, r *request.ResourcesComposeModules) (interface{}, error) {
return ctrl.cmp.Modules(ctx, r.NamespaceID, r.Limit, r.PageCursor, r.ModuleID, r.Deleted)
}
func (ctrl resources) ComposeRecords(ctx context.Context, r *request.ResourcesComposeRecords) (interface{}, error) {
return ctrl.cmp.Records(ctx, r.NamespaceID, r.ModuleID, r.Limit, r.PageCursor, r.RecordID, r.Deleted)
}

View File

@@ -0,0 +1,21 @@
package rest
import (
"github.com/go-chi/chi/v5"
"github.com/cortezaproject/corteza/server/discovery/rest/handlers"
"github.com/cortezaproject/corteza/server/pkg/auth"
"github.com/cortezaproject/corteza/server/pkg/options"
)
func MountRoutes(discoveryOpts options.DiscoveryOpt) func(r chi.Router) {
return func(r chi.Router) {
r.Group(func(r chi.Router) {
r.Use(auth.HttpTokenValidator("discovery"))
handlers.NewResources(Resources()).MountRoutes(r)
handlers.NewFeed(Feed()).MountRoutes(r)
handlers.NewMappings(Mappings(discoveryOpts)).MountRoutes(r)
})
}
}

View File

@@ -0,0 +1,157 @@
package service
import (
"context"
"fmt"
"github.com/cortezaproject/corteza/server/pkg/eventbus"
"github.com/cortezaproject/corteza/server/pkg/id"
"github.com/cortezaproject/corteza/server/pkg/options"
"go.uber.org/zap"
"sync"
"time"
"github.com/cortezaproject/corteza/server/discovery/types"
"github.com/cortezaproject/corteza/server/store"
)
type (
resourceActivity struct {
// log for repository errors
log *zap.Logger
opt options.DiscoveryOpt
mux sync.RWMutex
// where the activity log records are kept
// store resourceActivityLogStore
store store.Storer
eventbus eventbusRegistry
decoder types.ResDecoder
}
eventbusRegistry interface {
Register(eventbus.HandlerFn, ...eventbus.HandlerRegOp) uintptr
}
ResourceActivityService interface {
Find(context.Context, types.ResourceActivityFilter) (types.ResourceActivitySet, types.ResourceActivityFilter, error)
}
)
func ResourceActivity(log *zap.Logger, opt options.DiscoveryOpt, s store.Storer, eb eventbusRegistry) *resourceActivity {
return &resourceActivity{
log: log.Named("resource-activity"),
opt: opt,
store: s,
eventbus: eb,
}
}
func (svc *resourceActivity) logger(a *types.ResourceActivity) {
zlf := []zap.Field{
zap.Uint8("recordID", uint8(a.ResourceID)),
zap.String("ResourceType", a.ResourceType),
zap.String("ResourceAction", a.ResourceAction),
zap.Time("timestamp", a.Timestamp),
}
svc.log.
With(zlf...).
// Skipping 3 callers (the most common stack)
// discovery.service.log()
// discovery.service.Record()
// (generated service function)
//
// One exception, access control, that calls Record fn directly,
// without going through generated discovery helpers
WithOptions(zap.AddCallerSkip(3)).
// This is debug logger, and we log all recordings as debug
Debug(fmt.Sprintf("%s of %s", a.ResourceAction, a.ResourceType))
}
func (svc *resourceActivity) InitResourceActivityLog(ctx context.Context, resourceType []string) (err error) {
eventType := []string{
string(types.AfterCreate),
string(types.AfterUpdate),
string(types.AfterDelete),
}
svc.mux.RLock()
defer svc.mux.RUnlock()
svc.eventbus.Register(
func(_ context.Context, ev eventbus.Event) error {
var a *types.ResourceActivity
dec, is := ev.(types.ResDecoder)
if is {
svc.log.Debug("resource changed",
zap.String("eventType", ev.EventType()),
zap.String("resourceType", ev.ResourceType()),
)
a, err = types.CastToResourceActivity(dec)
if err != nil {
svc.log.With(zap.Error(err)).Error("could not cast event to activity")
return err
}
err = svc.store.CreateResourceActivity(ctx, a)
if err != nil {
svc.log.With(zap.Error(err)).Error("could not record activity event")
return err
}
return nil
}
return nil
},
eventbus.For(resourceType...),
eventbus.On(eventType...),
)
return
}
func (svc *resourceActivity) Find(ctx context.Context, filter types.ResourceActivityFilter) (aa types.ResourceActivitySet, f types.ResourceActivityFilter, err error) {
err = func() error {
aa, f, err = store.SearchResourceActivitys(ctx, svc.store, filter)
if err != nil {
return err
}
return nil
}()
return aa, f, err
}
func (svc *resourceActivity) Record(ctx context.Context, a *types.ResourceActivity) {
if a == nil {
// nothing to record
return
}
a = enrich(ctx, a)
a.ID = id.Next()
svc.logger(a)
// We want to prevent any abrupt cancellation
// (e.g. canceled request) that would cause
// discovery to fail...
ctx = context.Background()
if err := svc.store.CreateResourceActivity(ctx, a); err != nil {
svc.log.With(zap.Error(err)).Error("could not record activity event")
}
}
// enrich activity with additional info (timestamp, ...)
func enrich(_ context.Context, a *types.ResourceActivity) *types.ResourceActivity {
if a.Timestamp.IsZero() {
a.Timestamp = time.Now()
}
return a
}

View File

@@ -0,0 +1,33 @@
package service
import (
"context"
"github.com/cortezaproject/corteza/server/pkg/eventbus"
"github.com/cortezaproject/corteza/server/pkg/options"
"github.com/cortezaproject/corteza/server/store"
"go.uber.org/zap"
)
var (
DefaultOption options.DiscoveryOpt
// DefaultStore is an interface to storage backend(s)
// ng (next-gen) is a temporary prefix
// so that we can differentiate between it and the file-only store
DefaultStore store.Storer
DefaultResourceActivity *resourceActivity
)
// Initialize discovery service
func Initialize(_ context.Context, log *zap.Logger, opt options.DiscoveryOpt, s store.Storer) (err error) {
DefaultOption = opt
// we're doing conversion to avoid having
// store interface exposed or generated inside app package
DefaultStore = s
DefaultResourceActivity = ResourceActivity(log, opt, s, eventbus.Service())
return
}

View File

@@ -0,0 +1,158 @@
package types
import (
"encoding/json"
"regexp"
"strings"
)
type (
Access int
accessJsonObjAux struct {
Public bool `json:"public"`
Private bool `json:"private"`
Protected bool `json:"protected"`
}
accessStrSliceAux []string
)
const (
Mask Access = 0b111
Private Access = 0b001
Protected Access = 0b010
Public Access = 0b100
PrivateLabel = "private"
ProtectedLabel = "protected"
PublicLabel = "public"
)
var (
accessStrDelimiter = regexp.MustCompile(`\W+`)
z = int(Protected)
)
func toAccess(str string) (a Access) {
for _, str = range accessStrDelimiter.Split(strings.ToLower(str), -1) {
switch str {
case PrivateLabel:
a |= Private
case ProtectedLabel:
a |= Protected
case PublicLabel:
a |= Public
}
}
return
}
func (a Access) Check(str string) bool {
return a.Is(toAccess(str))
}
func (a Access) MCheck(str string, mask Access) bool {
return a.Is(mask & toAccess(str))
}
func (a Access) Is(i Access) bool { return a&i != 0 }
func (a Access) IsPrivate() bool { return a.Is(Private) }
func (a Access) IsProtected() bool { return a.Is(Protected) }
func (a Access) IsPublic() bool { return a.Is(Public) }
func (a Access) String() string {
var s = ""
if a&Public == Public {
s += PrivateLabel
}
if a&Protected == Protected {
if len(s) > 0 {
s += ","
}
s += PrivateLabel
}
if a&Private == Private {
if len(s) > 0 {
s += ","
}
s += PrivateLabel
}
return s
}
func (a *Access) UnmarshalJSON(data []byte) (err error) {
if len(data) == 0 {
// empty input, nothing to parse
return nil
}
if data[0] == '"' {
*a = toAccess(string(data))
return
}
if data[0] == '{' {
aux := &accessJsonObjAux{}
if err = json.Unmarshal(data, aux); err == nil {
*a = aux.toAccess()
}
return
}
if data[0] == '[' {
aux := &accessStrSliceAux{}
if err = json.Unmarshal(data, aux); err == nil {
*a = aux.toAccess()
}
return
}
var aux int
if err = json.Unmarshal(data, &aux); err == nil {
*a = Access(aux)
}
return
}
func (a Access) MarshalJSON() ([]byte, error) {
return json.Marshal(accessJsonObjAux{
Public: a&Public != 0,
Private: a&Private != 0,
Protected: a&Protected != 0,
})
}
func (aux accessJsonObjAux) toAccess() (a Access) {
if aux.Private {
a |= Private
}
if aux.Protected {
a |= Protected
}
if aux.Public {
a |= Public
}
return a
}
func (aux accessStrSliceAux) toAccess() (a Access) {
for _, s := range aux {
a |= toAccess(s)
}
return
}

View File

@@ -0,0 +1,77 @@
package types
import (
"github.com/stretchr/testify/require"
"testing"
)
func Test_toAccess(t *testing.T) {
var (
req = require.New(t)
)
req.Equal(Private, toAccess("private"))
req.Equal(Public, toAccess("public"))
req.Equal(Protected, toAccess("protected"))
req.Equal(Private|Protected, toAccess("protected|private"))
req.Equal(Access(0), toAccess(""))
}
func TestAccess_Is(t *testing.T) {
var (
req = require.New(t)
)
req.True(Private.IsPrivate())
req.True(Private.Is(Private))
req.True(Protected.IsProtected())
req.True(Protected.Is(Protected))
req.True(Public.IsPublic())
req.True(Public.Is(Public))
req.True((Public | Protected).Is(Public))
req.True((Public | Protected).Is(Protected))
req.False((Public | Protected).Is(Private))
}
func TestAccess_UnmarshalJSON(t *testing.T) {
var (
cc = []struct {
Data string
WantErr bool
Access Access
}{
{Data: ``, Access: Access(0)},
{Data: `{}`, Access: Access(0)},
{Data: `{"private":true}`, Access: Private},
{Data: `{"private":true,"public":true}`, Access: Private | Public},
{Data: `true`, WantErr: true},
{Data: `1`, Access: Private},
{Data: `3`, Access: Private | Protected},
{Data: `"private"`, Access: Private},
{Data: `"private|public"`, Access: Private | Public},
{Data: `"private,public"`, Access: Private | Public},
}
)
for _, c := range cc {
t.Run(c.Data, func(t *testing.T) {
var (
req = require.New(t)
a Access
)
err := a.UnmarshalJSON([]byte(c.Data))
if c.WantErr {
req.Error(err)
} else {
req.NoError(err)
}
if err != nil {
return
}
req.Equal(a, c.Access)
})
}
}

View File

@@ -0,0 +1,58 @@
package types
type (
// ResponseMeta has all access restriction response
ResponseMeta struct {
Private ResourceMeta `json:"private,omitempty"`
Public ResourceMeta `json:"public,omitempty"`
Protected ResourceMeta `json:"protected,omitempty"`
}
// ResourceMeta have all resource related meta
// for what to display in what order for each resource and its fields
ResourceMeta struct {
NamespaceMeta []NameMeta `json:"namespace_meta,omitempty"`
ModuleMeta []NameMeta `json:"module_meta,omitempty"`
RecordMeta []NameMeta `json:"record_meta,omitempty"`
}
// NameMeta is single row of discovery response fields with its weight
NameMeta struct {
Name string `json:"name"`
Title string `json:"title"`
Weight int `json:"weight"`
}
/*
"discovery": {
"public": {
"result": [
{
lang: "", // en, fr
fields: [] // “names”, ..
}
]
} // struct
},
"private": {
"response": []
}
}
*/
ModuleMeta struct {
Public AccessJson `json:"public"`
Private AccessJson `json:"private"`
Protected AccessJson `json:"protected"`
}
AccessJson struct {
Result []Result `json:"result"`
}
Result struct {
Lang string `json:"lang"`
Fields []string `json:"fields"`
// @todo? TBD? excludeModuleFields, includeModuleFields <- if passed filter module field accordingly.
}
)

73
server/discovery/types/type_set.gen.go generated Normal file
View File

@@ -0,0 +1,73 @@
package types
// This file is auto-generated.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
//
// Definitions file that controls how this file is generated:
// discovery/types/types.yaml
type (
// ResourceActivitySet slice of ResourceActivity
//
// This type is auto-generated.
ResourceActivitySet []*ResourceActivity
)
// Walk iterates through every slice item and calls w(ResourceActivity) err
//
// This function is auto-generated.
func (set ResourceActivitySet) Walk(w func(*ResourceActivity) error) (err error) {
for i := range set {
if err = w(set[i]); err != nil {
return
}
}
return
}
// Filter iterates through every slice item, calls f(ResourceActivity) (bool, err) and return filtered slice
//
// This function is auto-generated.
func (set ResourceActivitySet) Filter(f func(*ResourceActivity) (bool, error)) (out ResourceActivitySet, err error) {
var ok bool
out = ResourceActivitySet{}
for i := range set {
if ok, err = f(set[i]); err != nil {
return
} else if ok {
out = append(out, set[i])
}
}
return
}
// FindByID finds items from slice by its ID property
//
// This function is auto-generated.
func (set ResourceActivitySet) FindByID(ID uint64) *ResourceActivity {
for i := range set {
if set[i].ID == ID {
return set[i]
}
}
return nil
}
// IDs returns a slice of uint64s from all items in the set
//
// This function is auto-generated.
func (set ResourceActivitySet) IDs() (IDs []uint64) {
IDs = make([]uint64, len(set))
for i := range set {
IDs[i] = set[i].ID
}
return
}

View File

@@ -0,0 +1,105 @@
package types
// This file is auto-generated.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
//
// Definitions file that controls how this file is generated:
// discovery/types/types.yaml
import (
"fmt"
"github.com/stretchr/testify/require"
"testing"
)
func TestResourceActivitySetWalk(t *testing.T) {
var (
value = make(ResourceActivitySet, 3)
req = require.New(t)
)
// check walk with no errors
{
err := value.Walk(func(*ResourceActivity) error {
return nil
})
req.NoError(err)
}
// check walk with error
req.Error(value.Walk(func(*ResourceActivity) error { return fmt.Errorf("walk error") }))
}
func TestResourceActivitySetFilter(t *testing.T) {
var (
value = make(ResourceActivitySet, 3)
req = require.New(t)
)
// filter nothing
{
set, err := value.Filter(func(*ResourceActivity) (bool, error) {
return true, nil
})
req.NoError(err)
req.Equal(len(set), len(value))
}
// filter one item
{
found := false
set, err := value.Filter(func(*ResourceActivity) (bool, error) {
if !found {
found = true
return found, nil
}
return false, nil
})
req.NoError(err)
req.Len(set, 1)
}
// filter error
{
_, err := value.Filter(func(*ResourceActivity) (bool, error) {
return false, fmt.Errorf("filter error")
})
req.Error(err)
}
}
func TestResourceActivitySetIDs(t *testing.T) {
var (
value = make(ResourceActivitySet, 3)
req = require.New(t)
)
// construct objects
value[0] = new(ResourceActivity)
value[1] = new(ResourceActivity)
value[2] = new(ResourceActivity)
// set ids
value[0].ID = 1
value[1].ID = 2
value[2].ID = 3
// Find existing
{
val := value.FindByID(2)
req.Equal(uint64(2), val.ID)
}
// Find non-existing
{
val := value.FindByID(4)
req.Nil(val)
}
// List IDs from set
{
val := value.IDs()
req.Equal(len(val), len(value))
}
}

View File

@@ -0,0 +1,192 @@
package types
import (
"encoding/json"
"fmt"
"time"
composeTypes "github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/pkg/id"
systemTypes "github.com/cortezaproject/corteza/server/system/types"
"github.com/jmoiron/sqlx/types"
)
type (
// ResourceAction determinants resource action event
ResourceAction string
// ResourceActivity Standardized data structure for resource activity
ResourceActivity struct {
ID uint64 `json:"activityID,string"`
// ResourceID of the corteza resource
ResourceID uint64 `json:"resourceID,string"`
// ResourceType
ResourceType string `json:"resourceType"`
// ResourceAction Type of action
ResourceAction string `json:"resourceAction"`
// Timestamp of the raised event
Timestamp time.Time `json:"timestamp"`
// Meta of the related resources
Meta types.JSONText `json:"meta"`
}
ResourceActivityFilter struct {
FromTimestamp *time.Time `json:"from"`
ToTimestamp *time.Time `json:"to"`
// Check fn is called by store backend for each resource found function can
// modify the activity and return false if store should not return it
//
// Store then loads additional resources to satisfy the paging parameters
Check func(*ResourceActivity) (bool, error) `json:"-"`
// Standard helpers for paging and sorting
filter.Sorting
filter.Paging
}
ResourceActivityMeta struct {
NamespaceID uint64 `json:"namespaceID,string"`
ModuleID uint64 `json:"moduleID,string"`
}
ResDecoder interface {
EventType() string
ResourceType() string
}
userDecoder interface {
ResDecoder
User() *systemTypes.User
OldUser() *systemTypes.User
}
nsDecoder interface {
ResDecoder
Namespace() *composeTypes.Namespace
OldNamespace() *composeTypes.Namespace
}
mDecoder interface {
ResDecoder
Module() *composeTypes.Module
OldModule() *composeTypes.Module
}
recDecoder interface {
ResDecoder
Record() *composeTypes.Record
OldRecord() *composeTypes.Record
}
)
const (
AfterCreate ResourceAction = "afterCreate"
AfterUpdate ResourceAction = "afterUpdate"
AfterDelete ResourceAction = "afterDelete"
ResourceActivityResourceType = "corteza::generic:resource-activity"
)
func (s ResourceAction) String() string {
switch s {
case AfterCreate:
return "create"
case AfterUpdate:
return "update"
case AfterDelete:
return "delete"
}
return ""
}
func CastToResourceActivity(dec ResDecoder) (a *ResourceActivity, err error) {
a = &ResourceActivity{
ID: id.Next(),
Timestamp: time.Now(),
ResourceType: dec.ResourceType(),
ResourceAction: ResourceAction(dec.EventType()).String(),
}
setResourceID := func(ID uint64) {
a.ResourceID = ID
}
setMeta := func(nsID, mID uint64) error {
var meta ResourceActivityMeta
if nsID > 0 {
meta.NamespaceID = nsID
}
if mID > 0 {
meta.ModuleID = mID
}
a.Meta, err = json.Marshal(meta)
if err != nil {
return err
}
return nil
}
switch a.ResourceType {
case "system:user": // @todo system/service/service.go#134
if v, ok := dec.(userDecoder); ok {
user := v.User()
// fallback to OldUser for afterDelete event
if user == nil {
user = v.OldUser()
}
if user != nil {
setResourceID(user.ID)
}
}
case (composeTypes.Namespace{}).LabelResourceKind():
if v, ok := dec.(nsDecoder); ok {
ns := v.Namespace()
// fallback to OldNamespace for afterDelete event
if ns == nil {
ns = v.OldNamespace()
}
if ns != nil {
setResourceID(ns.ID)
}
}
case (composeTypes.Module{}).LabelResourceKind():
if v, ok := dec.(mDecoder); ok {
mod := v.Module()
// fallback to OldModule for afterDelete event
if mod == nil {
mod = v.OldModule()
}
if mod != nil {
setResourceID(mod.ID)
err = setMeta(mod.NamespaceID, 0)
if err != nil {
return
}
}
}
case "compose:record":
if v, ok := dec.(recDecoder); ok {
rec := v.Record()
// fallback to OldRecord for afterDelete event
if rec == nil {
rec = v.OldRecord()
}
if rec != nil {
setResourceID(rec.ID)
err = setMeta(rec.NamespaceID, rec.ModuleID)
if err != nil {
return
}
}
}
default:
return a, fmt.Errorf("unsupported resource type")
}
return
}

View File

@@ -0,0 +1,2 @@
types:
ResourceActivity: {}