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,21 @@
package actionlog
func MakeDebugPolicy() policyMatcher {
return NewPolicyAll()
}
func MakeProductionPolicy() policyMatcher {
return NewPolicyAll(
NewPolicyAny(
// Match all actions from automation
NewPolicyMatchRequestOrigin(RequestOrigin_Automation),
// Ignore debug actions
NewPolicyNegate(NewPolicyMatchSeverity(Debug, Info)),
),
)
}
func MakeDisabledPolicy() policyMatcher {
return NewPolicyNone()
}

View File

@@ -0,0 +1,50 @@
package actionlog
import (
"testing"
)
func TestCannedPolies(t *testing.T) {
tests := []struct {
name string
actn *Action
mtch policyMatcher
want bool
}{
{
"debug policy should pass on anything",
&Action{},
MakeDebugPolicy(),
true,
},
{
"production policy should record notice",
&Action{Severity: Notice},
MakeProductionPolicy(),
true,
},
{
"production policy should not record debug",
&Action{Severity: Debug},
MakeProductionPolicy(),
false,
},
{
"disabled policy should not record anything",
&Action{Severity: Alert},
MakeDisabledPolicy(),
false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.want != tt.mtch.Match(tt.actn) {
if tt.want {
t.Errorf("expecting to match")
} else {
t.Errorf("expecting not to match ")
}
}
})
}
}

View File

@@ -0,0 +1,38 @@
package actionlog
import (
"context"
)
// Key to use when setting the request ID.
type ctxKey int
const (
RequestOrigin_APP_Init = "app/init"
RequestOrigin_APP_Upgrade = "app/upgrade"
RequestOrigin_APP_Activate = "app/activate"
RequestOrigin_APP_Provision = "app/provision"
RequestOrigin_APP_Run = "app/run"
RequestOrigin_API_REST = "api/rest"
RequestOrigin_API_GRPC = "api/grpc"
RequestOrigin_Auth = "auth"
RequestOrigin_Automation = "automation"
)
// RequestOriginKey is the key that holds th unique request ID in a request context.
const requestOriginKey ctxKey = 0
// RequestOriginToContext stores request origin to context
func RequestOriginToContext(ctx context.Context, origin string) context.Context {
return context.WithValue(ctx, requestOriginKey, origin)
}
// RequestOriginFromContext returns remote IP address from context
func RequestOriginFromContext(ctx context.Context) string {
v := ctx.Value(requestOriginKey)
if str, ok := v.(string); ok {
return str
}
return ""
}

View File

@@ -0,0 +1,132 @@
package actionlog
import "github.com/cortezaproject/corteza/server/pkg/slice"
type (
policyMatcher interface {
Match(*Action) bool
}
logPolicyAny struct {
mm []policyMatcher
}
logPolicyAll struct {
mm []policyMatcher
}
logPolicyNegate struct {
m policyMatcher
}
logPolicyMatchRequestOrigin struct {
origin map[string]bool
}
logPolicyMatchResource struct {
resources map[string]bool
}
logPolicyMatchAction struct {
actions map[string]bool
}
logPolicyMatchSeverity struct {
severities map[Severity]bool
}
logPolicyNoop struct {
v bool
}
)
// NewPolicyNone ignores all action logs
func NewPolicyNone() policyMatcher {
return &logPolicyNoop{v: false}
}
// NewPolicyAny returns policy where at least one of the sub-policies should match
func NewPolicyAny(mm ...policyMatcher) policyMatcher {
return &logPolicyAny{mm: mm}
}
func (p logPolicyAny) Match(a *Action) bool {
for _, m := range p.mm {
if m.Match(a) {
return true
}
}
return false
}
// NewPolicyAll returns policy where all sub-policies should match
func NewPolicyAll(mm ...policyMatcher) policyMatcher {
return &logPolicyAll{mm: mm}
}
func (p logPolicyAll) Match(a *Action) bool {
for _, m := range p.mm {
if !m.Match(a) {
return false
}
}
return true
}
// NewPolicyNegate negates passed policy
func NewPolicyNegate(m policyMatcher) policyMatcher {
return &logPolicyNegate{m: m}
}
func (p logPolicyNegate) Match(a *Action) bool {
return !p.m.Match(a)
}
// NewPolicyMatchResource matches resources
func NewPolicyMatchResource(rr ...string) policyMatcher {
return &logPolicyMatchResource{resources: slice.ToStringBoolMap(rr)}
}
func (p logPolicyMatchResource) Match(a *Action) bool {
return p.resources[a.Resource]
}
// NewPolicyMatchAction matches action
func NewPolicyMatchAction(aa ...string) policyMatcher {
return &logPolicyMatchAction{actions: slice.ToStringBoolMap(aa)}
}
func (p logPolicyMatchAction) Match(a *Action) bool {
return p.actions[a.Action]
}
// NewPolicyMatchSeverity matches severity
func NewPolicyMatchSeverity(ss ...Severity) policyMatcher {
var p = &logPolicyMatchSeverity{severities: make(map[Severity]bool)}
for _, s := range ss {
p.severities[s] = true
}
return p
}
func (p logPolicyMatchSeverity) Match(a *Action) bool {
return p.severities[a.Severity]
}
// NewPolicyMatchRequestOrigin matches resources
func NewPolicyMatchRequestOrigin(rr ...string) policyMatcher {
return &logPolicyMatchRequestOrigin{origin: slice.ToStringBoolMap(rr)}
}
func (p logPolicyMatchRequestOrigin) Match(a *Action) bool {
return p.origin[a.RequestOrigin]
}
// Match Internal policy
func (p logPolicyNoop) Match(*Action) bool {
return p.v
}

View File

@@ -0,0 +1,118 @@
package actionlog
import (
"testing"
)
func TestPolicyMatchers(t *testing.T) {
tests := []struct {
name string
actn *Action
mtch policyMatcher
want bool
}{
{
"simple any",
&Action{},
NewPolicyAny(),
false,
},
{
"simple all",
&Action{},
NewPolicyAll(),
true,
},
{
"should match resource",
&Action{Resource: "foo"},
NewPolicyMatchResource("foo"),
true,
},
{
"should not match resource",
&Action{Resource: "bar"},
NewPolicyMatchResource("foo"),
false,
},
{
"should match one of resource",
&Action{Resource: "baz"},
NewPolicyMatchResource("foo", "bar", "baz"),
true,
},
{
"should match action",
&Action{Action: "foo"},
NewPolicyMatchAction("foo"),
true,
},
{
"should not match action",
&Action{Action: "bar"},
NewPolicyMatchAction("foo"),
false,
},
{
"should match one of action",
&Action{Action: "baz"},
NewPolicyMatchAction("foo", "bar", "baz"),
true,
},
{
"should match severity",
&Action{Severity: Emergency},
NewPolicyMatchSeverity(Emergency),
true,
},
{
"should not match severity",
&Action{Severity: Debug},
NewPolicyMatchSeverity(Alert),
false,
},
{
"should match one of severity",
&Action{Severity: Warning},
NewPolicyMatchSeverity(Emergency, Debug, Warning),
true,
},
{
"should not match one of severity",
&Action{Severity: Info},
NewPolicyNegate(NewPolicyMatchSeverity(Emergency, Debug, Warning)),
true,
},
{
"complex match",
&Action{Severity: Warning, Resource: "foo", Action: "do"},
NewPolicyAll(
NewPolicyMatchResource("foo"),
NewPolicyMatchAction("do"),
NewPolicyMatchSeverity(Warning),
),
true,
},
{
"complex miss",
&Action{Severity: Warning, Resource: "bar", Action: "do"},
NewPolicyAll(
NewPolicyMatchResource("foo"),
NewPolicyMatchAction("do"),
NewPolicyMatchSeverity(Warning),
),
false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.want != tt.mtch.Match(tt.actn) {
if tt.want {
t.Errorf("expecting to match")
} else {
t.Errorf("expecting not to match ")
}
}
})
}
}

View File

@@ -0,0 +1,149 @@
package actionlog
import (
"context"
"strings"
"time"
"github.com/cortezaproject/corteza/server/pkg/id"
"github.com/cortezaproject/corteza/server/pkg/logger"
"github.com/go-chi/chi/v5/middleware"
"go.uber.org/zap"
"github.com/cortezaproject/corteza/server/pkg/api"
"github.com/cortezaproject/corteza/server/pkg/auth"
)
type (
service struct {
// where the audit log records are kept
store actionlogStore
// Also write audit events here
tee *zap.Logger
// logger for repository errors
logger *zap.Logger
policy policyMatcher
}
Recorder interface {
Record(context.Context, *Action)
Find(context.Context, Filter) (ActionSet, Filter, error)
}
actionlogStore interface {
SearchActionlogs(ctx context.Context, f Filter) (ActionSet, Filter, error)
CreateActionlog(ctx context.Context, rr ...*Action) error
}
)
// NewService initializes action log service
func NewService(s actionlogStore, logger, tee *zap.Logger, policy policyMatcher) (svc *service) {
if tee == nil {
tee = zap.NewNop()
}
svc = &service{
tee: tee.Named("actionlog"),
logger: logger.Named("actionlog"),
store: s,
policy: policy,
}
return
}
func (svc service) Record(ctx context.Context, a *Action) {
if a == nil {
// nothing to record
return
}
a = enrich(ctx, a)
a.ID = id.Next()
svc.log(a)
if !svc.policy.Match(a) {
// policy does not allow us to record this
return
}
// We want to prevent any abrupt cancelation
// (eg canceled request) that would cause
// auditlog to fail...
ctx = context.Background()
if err := svc.store.CreateActionlog(ctx, a); err != nil {
svc.logger.With(zap.Error(err)).Error("could not record audit event")
}
}
func (svc service) log(a *Action) {
zlf := []zap.Field{
zap.Time("timestamp", a.Timestamp),
zap.String("requestOrigin", a.RequestOrigin),
zap.String("requestID", a.RequestID),
zap.String("actorIPAddr", a.ActorIPAddr),
logger.Uint64("actorID", a.ActorID),
zap.String("resource", a.Resource),
zap.String("action", a.Action),
zap.Uint8("severity", uint8(a.Severity)),
zap.String("error", a.Error),
zap.String("description", a.Description),
zap.Bool("policy-match", svc.policy.Match(a)),
zap.Any("meta", a.Meta),
}
svc.tee.
With(zlf...).
// Skipping 3 callers (the most common stack)
// actionlog.service.log()
// actionlog.service.Record()
// (generated service function)
//
// One exception, access control, that calls Record fn directly,
// without going through generated actionlog helpers
WithOptions(zap.AddCallerSkip(3)).
// This is debug logger and we log all recordings as debug
Debug(a.Description)
}
func (svc service) Find(ctx context.Context, flt Filter) (ActionSet, Filter, error) {
return svc.store.SearchActionlogs(ctx, flt)
}
// Enriches action with additional info (ip, actor id, request id...)
func enrich(ctx context.Context, a *Action) *Action {
if a.Timestamp.IsZero() {
a.Timestamp = time.Now()
}
a.RequestOrigin = RequestOriginFromContext(ctx)
// Relies on chi's middleware to get to the request ID
// This does not hurt us for now.
a.RequestID = middleware.GetReqID(ctx)
// uses pkg/auth to extract stored identity from context
a.ActorID = auth.GetIdentityFromContext(ctx).Identity()
// IP from the request,
// we're splitting by space & colon to remove any additional (proxy) IPs
// and ports from the string
if tmp := strings.SplitN(api.RemoteAddrFromContext(ctx), " ", 2); len(tmp) > 0 {
// split by : (ip:port)
if tmp = strings.SplitN(tmp[0], ":", 2); len(tmp) > 0 {
const maxLen = 16
ipAddr := tmp[0]
if len(ipAddr) > maxLen {
ipAddr = ipAddr[:maxLen-1]
}
a.ActorIPAddr = ipAddr
}
}
return a
}

73
server/pkg/actionlog/type_set.gen.go generated Normal file
View File

@@ -0,0 +1,73 @@
package actionlog
// This file is auto-generated.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
//
// Definitions file that controls how this file is generated:
// pkg/actionlog/types.yaml
type (
// ActionSet slice of Action
//
// This type is auto-generated.
ActionSet []*Action
)
// Walk iterates through every slice item and calls w(Action) err
//
// This function is auto-generated.
func (set ActionSet) Walk(w func(*Action) error) (err error) {
for i := range set {
if err = w(set[i]); err != nil {
return
}
}
return
}
// Filter iterates through every slice item, calls f(Action) (bool, err) and return filtered slice
//
// This function is auto-generated.
func (set ActionSet) Filter(f func(*Action) (bool, error)) (out ActionSet, err error) {
var ok bool
out = ActionSet{}
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 ActionSet) FindByID(ID uint64) *Action {
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 ActionSet) IDs() (IDs []uint64) {
IDs = make([]uint64, len(set))
for i := range set {
IDs[i] = set[i].ID
}
return
}

105
server/pkg/actionlog/type_set.gen_test.go generated Normal file
View File

@@ -0,0 +1,105 @@
package actionlog
// This file is auto-generated.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
//
// Definitions file that controls how this file is generated:
// pkg/actionlog/types.yaml
import (
"fmt"
"github.com/stretchr/testify/require"
"testing"
)
func TestActionSetWalk(t *testing.T) {
var (
value = make(ActionSet, 3)
req = require.New(t)
)
// check walk with no errors
{
err := value.Walk(func(*Action) error {
return nil
})
req.NoError(err)
}
// check walk with error
req.Error(value.Walk(func(*Action) error { return fmt.Errorf("walk error") }))
}
func TestActionSetFilter(t *testing.T) {
var (
value = make(ActionSet, 3)
req = require.New(t)
)
// filter nothing
{
set, err := value.Filter(func(*Action) (bool, error) {
return true, nil
})
req.NoError(err)
req.Equal(len(set), len(value))
}
// filter one item
{
found := false
set, err := value.Filter(func(*Action) (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(*Action) (bool, error) {
return false, fmt.Errorf("filter error")
})
req.Error(err)
}
}
func TestActionSetIDs(t *testing.T) {
var (
value = make(ActionSet, 3)
req = require.New(t)
)
// construct objects
value[0] = new(Action)
value[1] = new(Action)
value[2] = new(Action)
// 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,243 @@
package actionlog
import (
"database/sql/driver"
"encoding/json"
"strconv"
"time"
"github.com/cortezaproject/corteza/server/pkg/sql"
"github.com/cortezaproject/corteza/server/pkg/filter"
)
type (
// Any additional data
// that can be packed with the raised audit event
Meta map[string]interface{}
// Severity determinants event severity level
Severity uint8
// Standardized data structure for audit log events
Action struct {
ID uint64 `json:"actionID,string"`
// Timestamp of the raised event
Timestamp time.Time `json:"timestamp"`
// Origin of the action (rest-api, cli, grpc, system)
RequestOrigin string `json:"requestOrigin"`
// Request ID
RequestID string `json:"requestID"`
// This can contain a series of IP addresses (when proxied)
// https://en.wikipedia.org/wiki/X-Forwarded-For#Format
ActorIPAddr string `json:"actorIPAddr"`
// ID of the user (if not anonymous)
ActorID uint64 `json:"actorID,string"`
// Resource
Resource string `json:"resource"`
// Type of action
Action string `json:"action"`
// Error, if any
Error string `json:"error"`
// Action severity
Severity Severity `json:"severity"`
// Description of the event
Description string `json:"description"`
// Meta data, resource specific values
Meta Meta `json:"meta"`
}
Filter struct {
FromTimestamp *time.Time `json:"from"`
ToTimestamp *time.Time `json:"to"`
BeforeActionID uint64 `json:"beforeActionID"`
ActorID []string `json:"actorID"`
Origin string `json:"origin"`
Resource string `json:"resource"`
Action string `json:"action"`
Limit uint `json:"limit"`
// Standard helpers for sorting
filter.Sorting
}
loggableMetaValue interface {
ActionLogMetaValue() (interface{}, bool)
}
)
// Severity constants
//
// not using log/syslog LOG_* constants as they are only
// available outside windows env.
const (
Emergency Severity = iota
Alert
Critical
Error
Warning
Notice
Info
Debug
ActionResourceType = "corteza::generic:action"
)
func (a *Action) ToAction() *Action { return a }
// Set value as key to meta, or skip it if value is empty (and omitempty is set)
func (m Meta) Set(key string, in interface{}, omitempty bool) {
// if value has fn() ActionLogMetaValue that can
// reformat value and (maybe) omit empty values, call that first
if lmv, is := in.(loggableMetaValue); is {
if v, empty := lmv.ActionLogMetaValue(); !omitempty || !empty {
m[key] = v
}
return
}
// for the rest, we need to determine what kind of
if str, is := in.(string); is {
if !omitempty || len(str) > 0 {
m[key] = str
}
return
}
if bb, is := in.([]byte); is {
if !omitempty || len(bb) > 0 {
m[key] = bb
}
return
}
if b, is := in.(bool); is {
m[key] = b
return
}
{
// properly encode big numbers
// before storing them as JSON
num := func(n interface{}) interface{} {
switch n := n.(type) {
case int:
return int64(n)
case int8:
return int64(n)
case int16:
return int64(n)
case int32:
return int64(n)
case int64:
return int64(n)
case uint:
return uint64(n)
case uintptr:
return uint64(n)
case uint8:
return uint64(n)
case uint16:
return uint64(n)
case uint32:
return uint64(n)
case uint64:
return uint64(n)
case float32:
return float64(n)
case float64:
return n
}
return n
}(in)
switch num := num.(type) {
case int64:
if !omitempty || num > 0 {
m[key] = strconv.FormatInt(num, 10)
}
return
case uint64:
if !omitempty || num > 0 {
m[key] = strconv.FormatUint(num, 10)
}
return
case float64:
if !omitempty || num > 0 {
m[key] = in
}
return
}
}
// for the rest (string, slices, etc..)
// just set the value
m[key] = in
}
func (s Severity) String() string {
switch s {
case Emergency:
return "emergency"
case Alert:
return "alert"
case Critical:
return "critical"
case Error:
return "err"
case Warning:
return "warning"
case Notice:
return "notice"
case Info:
return "info"
case Debug:
return "debug"
}
return ""
}
func NewSeverity(s string) Severity {
switch s {
case "emergency":
return Emergency
case "alert":
return Alert
case "critical":
return Critical
case "err":
return Error
case "warning":
return Warning
case "notice":
return Notice
case "info":
return Info
}
return Debug
}
func (m *Meta) Scan(src any) error { return sql.ParseJSON(src, m) }
func (m Meta) Value() (driver.Value, error) { return json.Marshal(m) }

View File

@@ -0,0 +1,3 @@
package: actionlog
types:
Action: {}

View File

@@ -0,0 +1,70 @@
package actionlog
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
type (
metaFoo struct{ val string }
metaBar struct{ val string }
)
func (f metaFoo) ActionLogMetaValue() (interface{}, bool) {
return strings.ToUpper(f.val), len(f.val) == 0
}
func TestMeta_Set(t *testing.T) {
tests := []struct {
name string
val interface{}
omitempty bool
out Meta
}{
{"string value, omit", "str", true, Meta{"t": "str"}},
{"string value, keep", "str", false, Meta{"t": "str"}},
{"string empty, omit", "", true, Meta{}},
{"string empty, keep", "", false, Meta{"t": ""}},
{"byte value, omit", []byte{'b', 'y', 't', 'e'}, true, Meta{"t": []byte{'b', 'y', 't', 'e'}}},
{"byte value, keep", []byte{'b', 'y', 't', 'e'}, false, Meta{"t": []byte{'b', 'y', 't', 'e'}}},
{"byte empty, omit", []byte{}, true, Meta{}},
{"byte empty, keep", []byte{}, false, Meta{"t": []byte{}}},
{"bool true", true, false, Meta{"t": true}},
{"bool false", false, false, Meta{"t": false}},
{"int value, omit", 1, true, Meta{"t": "1"}},
{"int value, keep", 1, false, Meta{"t": "1"}},
{"int empty, omit", 0, true, Meta{}},
{"int empty, keep", 0, false, Meta{"t": "0"}},
{"uint64 empty, omit", uint64(0), true, Meta{}},
{"uint64 empty, keep", uint64(0), false, Meta{"t": "0"}},
{"bigint", 244783268048994492, false, Meta{"t": "244783268048994492"}},
{"int64", int64(244783268048994492), false, Meta{"t": "244783268048994492"}},
{"uint64()", uint64(244783268048994492), false, Meta{"t": "244783268048994492"}},
{"int", 42, false, Meta{"t": "42"}},
{"float64", float64(42), false, Meta{"t": float64(42)}},
{"float64", float64(244783268048994492), false, Meta{"t": 2.447832680489945e+17}},
{"slice", []int{0, 1}, false, Meta{"t": []int{0, 1}}},
{"meta foo", metaFoo{"value"}, false, Meta{"t": "VALUE"}},
{"meta foo, omit", metaFoo{""}, true, Meta{}},
// @todo raw
{"meta bar", metaBar{"value"}, false, Meta{"t": metaBar{"value"}}},
{"meta bar", metaBar{}, true, Meta{"t": metaBar{""}}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := assert.New(t)
m := make(Meta)
m.Set("t", tt.val, tt.omitempty)
if !a.Equal(tt.out, m) {
t.Fail()
}
})
}
}