snapshot of https://github.com/cortezaproject/corteza.git (2024.9.x, 2026-09-18; blobs > 131072B omitted)
Some checks failed
SonarQube / SonarQube (push) Failing after 5m28s
E2e test on main branch push / server and client test (map[name:admin port:8080]) (push) Failing after 3m33s
Server and lib tests / Library test (js) (push) Failing after 1m36s
Server and lib tests / Library test (vue) (push) Successful in 2m24s
Server and lib tests / Web client test (admin) (push) Successful in 2m52s
Server and lib tests / Web client test (compose) (push) Successful in 3m23s
Server and lib tests / Web client test (discovery) (push) Successful in 2m49s
Server and lib tests / Web client test (one) (push) Successful in 2m52s
Server and lib tests / Web client test (privacy) (push) Successful in 2m44s
Server and lib tests / Web client test (reporter) (push) Successful in 2m56s
Server and lib tests / Web client test (workflow) (push) Successful in 3m0s
Server and lib tests / Server tests (push) Failing after 4m55s
E2e test on main branch push / notify-failure (push) Failing after 9s
Server and lib tests / Send matrix testing notification (push) Successful in 2s
Mark stale issues and pull requests / stale (push) Successful in 40s
Some checks failed
SonarQube / SonarQube (push) Failing after 5m28s
E2e test on main branch push / server and client test (map[name:admin port:8080]) (push) Failing after 3m33s
Server and lib tests / Library test (js) (push) Failing after 1m36s
Server and lib tests / Library test (vue) (push) Successful in 2m24s
Server and lib tests / Web client test (admin) (push) Successful in 2m52s
Server and lib tests / Web client test (compose) (push) Successful in 3m23s
Server and lib tests / Web client test (discovery) (push) Successful in 2m49s
Server and lib tests / Web client test (one) (push) Successful in 2m52s
Server and lib tests / Web client test (privacy) (push) Successful in 2m44s
Server and lib tests / Web client test (reporter) (push) Successful in 2m56s
Server and lib tests / Web client test (workflow) (push) Successful in 3m0s
Server and lib tests / Server tests (push) Failing after 4m55s
E2e test on main branch push / notify-failure (push) Failing after 9s
Server and lib tests / Send matrix testing notification (push) Successful in 2s
Mark stale issues and pull requests / stale (push) Successful in 40s
This commit is contained in:
21
server/pkg/actionlog/canned_policies.go
Normal file
21
server/pkg/actionlog/canned_policies.go
Normal 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()
|
||||
}
|
||||
50
server/pkg/actionlog/canned_policies_test.go
Normal file
50
server/pkg/actionlog/canned_policies_test.go
Normal 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 ")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
38
server/pkg/actionlog/context.go
Normal file
38
server/pkg/actionlog/context.go
Normal 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 ""
|
||||
}
|
||||
132
server/pkg/actionlog/policy.go
Normal file
132
server/pkg/actionlog/policy.go
Normal 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
|
||||
}
|
||||
118
server/pkg/actionlog/policy_test.go
Normal file
118
server/pkg/actionlog/policy_test.go
Normal 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 ")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
149
server/pkg/actionlog/service.go
Normal file
149
server/pkg/actionlog/service.go
Normal 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
73
server/pkg/actionlog/type_set.gen.go
generated
Normal 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
105
server/pkg/actionlog/type_set.gen_test.go
generated
Normal 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))
|
||||
}
|
||||
}
|
||||
243
server/pkg/actionlog/types.go
Normal file
243
server/pkg/actionlog/types.go
Normal 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) }
|
||||
3
server/pkg/actionlog/types.yaml
Normal file
3
server/pkg/actionlog/types.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
package: actionlog
|
||||
types:
|
||||
Action: {}
|
||||
70
server/pkg/actionlog/types_test.go
Normal file
70
server/pkg/actionlog/types_test.go
Normal 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()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user