Files
upstream-corteza/server/compose/service/record_test.go
upstream-mirror 7e385fc126
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
snapshot of https://github.com/cortezaproject/corteza.git (2024.9.x, 2026-09-18; blobs > 131072B omitted)
2026-09-18 01:22:23 +05:00

1069 lines
33 KiB
Go

package service
import (
"context"
"fmt"
"testing"
"github.com/cortezaproject/corteza/server/pkg/dal"
"github.com/cortezaproject/corteza/server/pkg/eventbus"
"github.com/cortezaproject/corteza/server/store/adapters/rdbms"
"github.com/cortezaproject/corteza/server/compose/service/values"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/auth"
"github.com/cortezaproject/corteza/server/pkg/errors"
"github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/cortezaproject/corteza/server/pkg/logger"
"github.com/cortezaproject/corteza/server/pkg/rbac"
"github.com/cortezaproject/corteza/server/store"
"github.com/cortezaproject/corteza/server/store/adapters/rdbms/drivers/sqlite"
sysService "github.com/cortezaproject/corteza/server/system/service"
sysTypes "github.com/cortezaproject/corteza/server/system/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
)
func makeTestRecordService(t *testing.T, mods ...any) *record {
var (
err error
req = require.New(t)
log = zap.NewNop()
)
for _, m := range mods {
switch c := m.(type) {
case *zap.Logger:
t.Log("using custom Logger to initialize Record service")
log = c
}
}
var (
ctx = logger.ContextWithValue(context.Background(), log)
svc = &record{
sanitizer: values.Sanitizer(),
formatter: values.Formatter(),
eventbus: eventbus.New(),
}
)
svc.validator = defaultValidator(svc)
for _, m := range mods {
switch c := m.(type) {
case rbacService:
t.Log("using custom RBAC to initialize Record service")
svc.ac = &accessControl{rbac: c}
case store.Storer:
t.Log("using custom Store to initialize Record service")
svc.store = c
case dal.FullService:
t.Log("using custom DAL to initialize Record service")
t.Log("make sure you manually reload models!")
svc.dal = c
}
}
if svc.ac == nil {
svc.ac = &accessControl{rbac: rbac.NewService(log, nil)}
}
if svc.store == nil {
svc.store, err = sqlite.ConnectInMemoryWithDebug(ctx)
req.NoError(err)
req.NoError(store.Upgrade(ctx, log, svc.store))
req.NoError(store.TruncateUsers(ctx, svc.store))
req.NoError(store.TruncateRoles(ctx, svc.store))
req.NoError(store.TruncateComposeNamespaces(ctx, svc.store))
req.NoError(store.TruncateComposeModules(ctx, svc.store))
req.NoError(store.TruncateComposeModuleFields(ctx, svc.store))
req.NoError(store.TruncateRbacRules(ctx, svc.store))
}
resourceMaker(ctx, t, svc.store, mods...)
if svc.dal == nil {
t.Log("adding default DAL service")
dalAux, err := dal.New(zap.NewNop(), true)
req.NoError(err)
const (
recordsTable = "compose_record"
)
req.NoError(
dalAux.ReplaceConnection(
ctx,
dal.MakeConnection(1, svc.store.ToDalConn(),
dal.ConnectionParams{},
dal.ConnectionConfig{ModelIdent: recordsTable},
),
true,
),
)
svc.dal = dalAux
// assuming store will always be RDBMS/SQLite and we can just run delete
//
// this could be done more "properly" by invoking DAL to truncate records
// but at the momment we need to provide the right model/module.
_, err = svc.store.(*rdbms.Store).DB.ExecContext(ctx, "DELETE FROM "+recordsTable)
req.NoError(err)
t.Log("reloading DAL models")
req.NoError(DalModelReload(ctx, svc.store, sysService.DefaultDalSchemaAlteration, dalAux))
}
return svc
}
func ModelRef(svc *record, moduleID uint64) *dal.Model {
type (
modelFinder interface {
FindModelByResourceID(connectionID uint64, resourceID uint64) *dal.Model
}
)
var (
aux any = svc.dal
)
return aux.(modelFinder).FindModelByResourceID(1, moduleID)
}
func verifyRecErrSet(t *testing.T, err any) {
if rves, is := err.(*types.RecordValueErrorSet); is {
if rves.IsValid() {
return
}
t.Logf("Record errors:")
for _, e := range rves.Set {
t.Logf(" => [%s] %s", e.Kind, e.Message)
t.Logf(" %v", e.Meta)
}
t.FailNow()
}
if err, is := err.(*errors.Error); is {
t.Errorf("unexpected error: %s", err.Error())
for k, v := range err.Meta() {
t.Errorf(" %+v: %+v", k, v)
}
t.FailNow()
}
if err, is := err.(error); is {
t.Errorf("unexpected error: %s", err.Error())
t.FailNow()
}
}
func TestGeneralValueSetValidation(t *testing.T) {
var (
req = require.New(t)
module = &types.Module{
Fields: types.ModuleFieldSet{
&types.ModuleField{Name: "single1"},
&types.ModuleField{Name: "multi1", Multi: true},
&types.ModuleField{Name: "ref1", Kind: "Record"},
&types.ModuleField{Name: "multiRef1", Kind: "Record", Multi: true},
},
}
rvs types.RecordValueSet
err error
)
rvs = types.RecordValueSet{{Name: "single1", Value: "single"}}
err = RecordValueSanitization(module, rvs)
req.NoError(err)
rvs = types.RecordValueSet{{Name: "unknown", Value: "single"}}
err = RecordValueSanitization(module, rvs)
req.True(err != nil, "expecting RecordValueSanitization() to return an error, got nil")
rvs = types.RecordValueSet{{Name: "single1", Value: "single"}, {Name: "single1", Value: "single2"}}
err = RecordValueSanitization(module, rvs)
req.Error(err)
rvs = types.RecordValueSet{{Name: "multi1", Value: "multi1"}, {Name: "multi1", Value: "multi1"}}
err = RecordValueSanitization(module, rvs)
req.NoError(err)
rvs = types.RecordValueSet{{Name: "ref1", Value: "non numeric value"}}
err = RecordValueSanitization(module, rvs)
req.Error(err)
rvs = types.RecordValueSet{{Name: "ref1", Value: "12345"}}
err = RecordValueSanitization(module, rvs)
req.NoError(err)
rvs = types.RecordValueSet{{Name: "multiRef1", Value: "12345"}, {Name: "multiRef1", Value: "67890"}}
err = RecordValueSanitization(module, rvs)
req.NoError(err)
req.Len(rvs, 2, "expecting 2 record values after sanitization, got %d", len(rvs))
rvs = types.RecordValueSet{{Name: "ref1", Value: ""}}
err = RecordValueSanitization(module, rvs)
req.NoError(err)
}
func TestDefaultValueSetting(t *testing.T) {
var (
a = assert.New(t)
mod = &types.Module{
Fields: types.ModuleFieldSet{
&types.ModuleField{Name: "single", DefaultValue: []*types.RecordValue{{Value: "s"}}},
&types.ModuleField{Name: "multi", Multi: true, DefaultValue: []*types.RecordValue{{Value: "m1", Place: 0}, {Value: "m2", Place: 1}}},
},
}
chk = func(vv types.RecordValueSet, f string, p uint, v string) {
a.True(vv.Has("multi", p))
a.Equal(v, vv.Get(f, p).Value)
}
)
out := RecordValueDefaults(mod, nil)
chk(out, "single", 0, "s")
chk(out, "multi", 0, "m1")
chk(out, "multi", 1, "m2")
}
func TestRecord_boolFieldPermissionIssueKBR(t *testing.T) {
var (
err error
ctx = context.Background()
req = require.New(t)
u = &sysTypes.User{ID: nextID()}
ns = &types.Namespace{ID: nextID()}
modConf = types.ModuleConfig{DAL: types.ModuleConfigDAL{ConnectionID: 1}}
mod = &types.Module{ID: nextID(), NamespaceID: ns.ID, Config: modConf}
stringField = &types.ModuleField{ID: nextID(), ModuleID: mod.ID, Name: "string", Kind: "String"}
boolField = &types.ModuleField{ID: nextID(), ModuleID: mod.ID, Name: "bool", Kind: "Boolean"}
authRoleID uint64 = 1
readerRole = &sysTypes.Role{Name: "reader", ID: nextID()}
writerRole = &sysTypes.Role{Name: "writer", ID: nextID()}
//
rbacService = rbac.NewService(
zap.NewNop(),
//logger.MakeDebugLogger(),
nil,
)
svc = makeTestRecordService(t,
rbacService,
logger.MakeDebugLogger(),
u,
ns,
mod,
stringField,
boolField,
)
recChecked, recUnchecked *types.Record
valChecked = types.RecordValueSet{
&types.RecordValue{Name: "string", Value: "abc"},
&types.RecordValue{Name: "bool", Value: "1"},
}
valUnchecked = types.RecordValueSet{
&types.RecordValue{Name: "string", Value: "abc"},
}
)
svc.validator = defaultValidator(svc)
rbacService.UpdateRoles(
rbac.CommonRole.Make(readerRole.ID, readerRole.Name),
rbac.CommonRole.Make(writerRole.ID, writerRole.Name),
rbac.AuthenticatedRole.Make(authRoleID, "authenticated"),
)
rbacService.Grant(ctx,
// base permissions
rbac.AllowRule(authRoleID, mod.RbacResource(), "record.create"),
rbac.AllowRule(authRoleID, types.RecordRbacResource(0, 0, 0), "read"),
rbac.AllowRule(authRoleID, types.RecordRbacResource(0, 0, 0), "update"),
rbac.DenyRule(authRoleID, types.ModuleFieldRbacResource(0, 0, 0), "record.value.read"),
rbac.DenyRule(authRoleID, types.ModuleFieldRbacResource(0, 0, 0), "record.value.update"),
// special perm for writer
rbac.AllowRule(writerRole.ID, stringField.RbacResource(), "record.value.read"),
rbac.AllowRule(writerRole.ID, stringField.RbacResource(), "record.value.update"),
rbac.AllowRule(writerRole.ID, boolField.RbacResource(), "record.value.read"),
rbac.AllowRule(writerRole.ID, boolField.RbacResource(), "record.value.update"),
// special perm for reader
rbac.AllowRule(readerRole.ID, stringField.RbacResource(), "record.value.read"),
rbac.AllowRule(readerRole.ID, boolField.RbacResource(), "record.value.read"),
rbac.DenyRule(readerRole.ID, boolField.RbacResource(), "record.value.update"),
)
{
// security context w/ writer role
ctx = auth.SetIdentityToContext(ctx, auth.Authenticated(u.ID, writerRole.ID, authRoleID))
recChecked, _, err = svc.Create(ctx, &types.Record{ModuleID: mod.ID, NamespaceID: ns.ID, Values: valChecked})
verifyRecErrSet(t, err)
req.NotNil(recChecked.Values.Get("bool", 0), "should be checked")
req.Equal("1", recChecked.Values.Get("bool", 0).Value)
recUnchecked, _, err = svc.Create(ctx, &types.Record{ModuleID: mod.ID, NamespaceID: ns.ID, Values: valUnchecked})
req.NoError(err)
req.Nil(recUnchecked.Values.Get("bool", 0))
// *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
// security context w/ reader role
ctx = auth.SetIdentityToContext(ctx, auth.Authenticated(u.ID, readerRole.ID, authRoleID))
recChecked.Values = types.RecordValueSet{
&types.RecordValue{Name: "string", Value: "abc"},
}
recChecked, _, err = svc.Update(ctx, recChecked)
req.NoError(err)
req.NotNil(recChecked.Values.Get("bool", 0), "should still be checked")
req.Equal("1", recChecked.Values.Get("bool", 0).Value)
recUnchecked.Values = types.RecordValueSet{
&types.RecordValue{Name: "string", Value: "abc"},
}
recUnchecked, _, err = svc.Update(ctx, recUnchecked)
req.NoError(err)
req.Nil(recUnchecked.Values.Get("bool", 0), "should not be checked anymore")
// *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
// security context w/ writer role
ctx = auth.SetIdentityToContext(ctx, auth.Authenticated(u.ID, writerRole.ID, authRoleID))
recChecked.Values = types.RecordValueSet{
&types.RecordValue{Name: "string", Value: "abc"},
&types.RecordValue{Name: "bool", Value: "1"},
}
recChecked, _, err = svc.Update(ctx, recChecked)
req.NoError(err)
req.NotNil(recChecked.Values.Get("bool", 0), "should checked again")
req.Equal("1", recChecked.Values.Get("bool", 0).Value, "should checked again")
}
}
func TestRecord_defValueFieldPermissionIssue(t *testing.T) {
var (
err error
req = require.New(t)
ctx = context.Background()
rbacService = rbac.NewService(
//zap.NewNop(),
logger.MakeDebugLogger(),
nil,
)
user = &sysTypes.User{ID: nextID()}
modConf = types.ModuleConfig{DAL: types.ModuleConfigDAL{ConnectionID: 1}}
ns = &types.Namespace{ID: nextID()}
mod = &types.Module{ID: nextID(), NamespaceID: ns.ID, Config: modConf}
writableField = &types.ModuleField{ID: nextID(), ModuleID: mod.ID, NamespaceID: ns.ID, Name: "writable", Kind: "String", DefaultValue: types.RecordValueSet{{Value: "def-w"}}}
readableField = &types.ModuleField{ID: nextID(), ModuleID: mod.ID, NamespaceID: ns.ID, Name: "readable", Kind: "String", DefaultValue: types.RecordValueSet{{Value: "def-r"}}}
svc = makeTestRecordService(t,
rbacService,
user,
ns,
mod,
writableField,
readableField,
)
authRoleID = nextID()
editorRole = &sysTypes.Role{Name: "editor", ID: nextID()}
recPartial *types.Record
valueExtractor = func(rec *types.Record, ff ...string) (out string) {
if rec == nil {
return "<NIL RECORD>"
}
if rec.Values == nil {
return "<NIL RECORD VALUES>"
}
for _, f := range ff {
out += "<"
if v := rec.Values.Get(f, 0); v != nil {
out += v.Value
} else {
out += "NULL"
}
out += ">"
}
return
}
)
t.Log("creating namespace, module and fields")
svc.validator = defaultValidator(svc)
t.Log("setting up security")
rbacService.UpdateRoles(
rbac.CommonRole.Make(editorRole.ID, editorRole.Name),
rbac.AuthenticatedRole.Make(authRoleID, "authenticated"),
)
rbacService.Grant(ctx,
// base permissions
rbac.AllowRule(authRoleID, mod.RbacResource(), "record.create"),
rbac.AllowRule(authRoleID, types.RecordRbacResource(0, 0, 0), "read"),
rbac.AllowRule(authRoleID, types.RecordRbacResource(0, 0, 0), "update"),
rbac.AllowRule(authRoleID, types.ModuleFieldRbacResource(0, 0, 0), "record.value.read"),
rbac.AllowRule(authRoleID, types.ModuleFieldRbacResource(0, 0, 0), "record.value.update"),
// expl. deny value updates for editor on readable field (still allowed to write on writable field)
rbac.DenyRule(editorRole.ID, writableField.RbacResource(), "record.value.update"),
)
{
t.Log("creating record with w/o editor role (expecting defaults")
ctx = auth.SetIdentityToContext(ctx, auth.Authenticated(user.ID, authRoleID))
recPartial, _, err = svc.Create(ctx, &types.Record{ModuleID: mod.ID, NamespaceID: ns.ID, Values: types.RecordValueSet{}})
verifyRecErrSet(t, err)
req.Equal("<def-w><def-r>", valueExtractor(recPartial, "writable", "readable"))
t.Log("creating record with w/o editor role (must be able to crate & update record and modify both fields)")
recPartial, _, err = svc.Create(ctx, &types.Record{ModuleID: mod.ID, NamespaceID: ns.ID, Values: types.RecordValueSet{
&types.RecordValue{Name: "writable", Value: "w"},
&types.RecordValue{Name: "readable", Value: "r"},
}})
verifyRecErrSet(t, err)
req.Equal("<w><r>", valueExtractor(recPartial, "writable", "readable"))
t.Log("updating record removing one of the values")
recPartial.Values = types.RecordValueSet{&types.RecordValue{Name: "writable", Value: "w2"}}
recPartial, _, err = svc.Update(ctx, recPartial)
verifyRecErrSet(t, err)
req.Equal("<w2><NULL>", valueExtractor(recPartial, "writable", "readable"))
}
{
t.Log("creating record with editor role (expecting defaults")
ctx = auth.SetIdentityToContext(ctx, auth.Authenticated(user.ID, authRoleID, editorRole.ID))
recPartial, _, err = svc.Create(ctx, &types.Record{ModuleID: mod.ID, NamespaceID: ns.ID, Values: types.RecordValueSet{}})
verifyRecErrSet(t, err)
req.Equal("<def-w><def-r>", valueExtractor(recPartial, "writable", "readable"))
t.Log("creating record with editor role (must be able to crate & update record and modify both fields)")
recPartial, _, err = svc.Create(ctx, &types.Record{ModuleID: mod.ID, NamespaceID: ns.ID, Values: types.RecordValueSet{
// this is the def. value set
&types.RecordValue{Name: "writable", Value: "def-w"},
&types.RecordValue{Name: "readable", Value: "r"},
}})
verifyRecErrSet(t, err)
req.Equal("<def-w><r>", valueExtractor(recPartial, "writable", "readable"))
}
}
func TestRecord_refAccessControl(t *testing.T) {
var (
req = require.New(t)
// uncomment to enable sql conn debugging
ctx = logger.ContextWithValue(context.Background(), logger.MakeDebugLogger())
//ctx = context.Background()
s, err = sqlite.ConnectInMemoryWithDebug(ctx)
)
req.NoError(err)
req.NoError(store.Upgrade(ctx, zap.NewNop(), s))
req.NoError(store.TruncateComposeNamespaces(ctx, s))
req.NoError(store.TruncateComposeModules(ctx, s))
req.NoError(store.TruncateComposeModuleFields(ctx, s))
req.NoError(store.TruncateRbacRules(ctx, s))
var (
rbacService = rbac.NewService(
//zap.NewNop(),
logger.MakeDebugLogger(),
nil,
)
nextIDi uint64 = 1
nextID = func() uint64 {
nextIDi++
return nextIDi
}
modConf = types.ModuleConfig{DAL: types.ModuleConfigDAL{ConnectionID: 1}}
user = &sysTypes.User{ID: nextID()}
ns = &types.Namespace{ID: nextID()}
mod1 = &types.Module{ID: nextID(), NamespaceID: ns.ID, Name: "mod one", Config: modConf}
mod2 = &types.Module{ID: nextID(), NamespaceID: ns.ID, Name: "mod two", Config: modConf}
mod1strField = &types.ModuleField{ID: nextID(), NamespaceID: ns.ID, ModuleID: mod1.ID, Name: "str", Kind: "String"}
mod2refField = &types.ModuleField{
ID: nextID(),
NamespaceID: ns.ID,
ModuleID: mod2.ID,
Name: "ref",
Kind: "Record",
Options: types.ModuleFieldOptions{
"moduleID": mod1.ID,
},
}
testerRole = &sysTypes.Role{Name: "tester", ID: nextID()}
svc = makeTestRecordService(t,
rbacService,
user,
ns,
mod1,
mod2,
mod1strField,
mod2refField,
)
mod1rec1 = &types.Record{
NamespaceID: ns.ID,
ModuleID: mod1.ID,
Values: types.RecordValueSet{
&types.RecordValue{Name: "str", Value: "abc"},
},
}
mod2rec1 = &types.Record{NamespaceID: ns.ID, ModuleID: mod2.ID}
)
svc.validator = defaultValidator(svc)
t.Log("inform rbac service about new roles")
rbacService.UpdateRoles(
rbac.CommonRole.Make(testerRole.ID, testerRole.Name),
)
t.Log("log-in with test user ")
ctx = auth.SetIdentityToContext(ctx, auth.Authenticated(user.ID, testerRole.ID))
_ = mod2rec1
{
t.Log("creating record on 1st module; should failed because we do not have permissions to create records")
_, _, err = svc.Create(ctx, mod1rec1)
req.EqualError(err, "not allowed to create records")
t.Logf("granting permissions to create records on this module")
req.NoError(rbacService.Grant(ctx, rbac.AllowRule(testerRole.ID, mod1.RbacResource(), "record.create")))
t.Log("retry creating record on 1st module; should fail because we do not have permissions to update field")
_, _, err = svc.Create(ctx, mod1rec1)
req.Error(err)
req.True(types.IsRecordValueErrorSet(err).HasKind("updateDenied"))
t.Logf("granting permissions to update records values on module field")
req.NoError(rbacService.Grant(ctx, rbac.AllowRule(testerRole.ID, mod1strField.RbacResource(), "record.value.update")))
t.Log("retry creating record on 1st module; should succeed")
mod1rec1, _, err = svc.Create(ctx, mod1rec1)
req.NoError(err)
}
{
t.Log("can record be read")
_, _, err = svc.FindByID(ctx, mod1rec1.NamespaceID, mod1rec1.ModuleID, mod1rec1.ID)
req.EqualError(err, "not allowed to read this record")
}
{
t.Log("link 2nd record to 1st one")
mod2rec1.Values = mod2rec1.Values.Set(&types.RecordValue{Name: "ref", Value: fmt.Sprintf("%d", mod1rec1.ID)})
t.Log("create record on 2nd module with ref to record on the 1st module; must fail, no create perm")
_, _, err = svc.Create(ctx, mod2rec1)
req.EqualError(err, "not allowed to create records")
t.Log("grant record.create on namespace level")
req.NoError(rbacService.Grant(ctx, rbac.AllowRule(testerRole.ID, types.ModuleRbacResource(ns.ID, 0), "record.create")))
t.Log("grant record.value.update on namespace level")
req.NoError(rbacService.Grant(ctx, rbac.AllowRule(testerRole.ID, types.ModuleFieldRbacResource(ns.ID, 0, 0), "record.value.update")))
t.Log("create record on 2nd module with ref to record on the 1st module; most fail, not allowed to read (referenced) mod1rec1")
_, _, err = svc.Create(ctx, mod2rec1)
req.EqualError(err, "invalid record value input")
t.Log("grant read on record")
req.NoError(rbacService.Grant(ctx, rbac.AllowRule(testerRole.ID, mod1rec1.RbacResource(), "read")))
t.Log("create record on 2nd module with ref to record on the 1st module")
mod2rec1, _, err = svc.Create(ctx, mod2rec1)
verifyRecErrSet(t, err)
}
{
t.Log("update record on 2nd module with unchanged values; must fail, no update permissions")
_, _, err = svc.Update(ctx, mod2rec1)
req.EqualError(err, "not allowed to update this record")
t.Log("grant update on namespace level")
req.NoError(rbacService.Grant(ctx, rbac.AllowRule(testerRole.ID, types.RecordRbacResource(ns.ID, 0, 0), "update")))
t.Log("update record on 2nd module with unchanged values")
mod2rec1, _, err = svc.Update(ctx, mod2rec1)
verifyRecErrSet(t, err)
t.Log("update record on 2nd module with unchanged values; unset record value")
mod2rec1.Values = nil
mod2rec1, _, err = svc.Update(ctx, mod2rec1)
verifyRecErrSet(t, err)
t.Log("link 2nd record to 1st one again")
mod2rec1.Values = mod2rec1.Values.Set(&types.RecordValue{Name: "ref", Value: fmt.Sprintf("%d", mod1rec1.ID)})
mod2rec1, _, err = svc.Update(ctx, mod2rec1)
verifyRecErrSet(t, err)
}
{
t.Log("revoke read on record")
req.NoError(rbacService.Grant(ctx, rbac.DenyRule(testerRole.ID, mod1rec1.RbacResource(), "read")))
t.Log("link 2nd record to 1st one again but w/o permissions; must work, value did not change")
mod2rec1.Values = mod2rec1.Values.Set(&types.RecordValue{Name: "ref", Value: fmt.Sprintf("%d", mod1rec1.ID)})
mod2rec1, _, err = svc.Update(ctx, mod2rec1)
verifyRecErrSet(t, err)
}
}
func TestRecord_searchAccessControl(t *testing.T) {
var (
err error
req = require.New(t)
ctx = context.Background()
rbacService = rbac.NewService(
//zap.NewNop(),
logger.MakeDebugLogger(),
nil,
)
nextIDi uint64 = 1
nextID = func() uint64 {
nextIDi++
return nextIDi
}
modConf = types.ModuleConfig{DAL: types.ModuleConfigDAL{ConnectionID: 1}}
user = &sysTypes.User{ID: nextID()}
ns = &types.Namespace{ID: nextID()}
mod = &types.Module{ID: nextID(), NamespaceID: ns.ID, Name: "mod one", Config: modConf}
strField = &types.ModuleField{ID: nextID(), NamespaceID: ns.ID, ModuleID: mod.ID, Name: "str", Kind: "String"}
svc = makeTestRecordService(t,
rbacService,
user,
ns,
mod,
strField,
)
testerRole = &sysTypes.Role{Name: "tester", ID: nextID()}
rr = make([]*types.Record, 10)
hits []*types.Record
f = types.RecordFilter{
ModuleID: mod.ID,
NamespaceID: ns.ID,
}
// search for module's model to be used as a filter
model = ModelRef(svc, mod.ID)
)
req.NotNil(model)
t.Log("create test records")
for i := 0; i < cap(rr); i++ {
rr[i] = &types.Record{
ID: nextID(),
NamespaceID: ns.ID,
ModuleID: mod.ID,
}
req.NoError(svc.dal.Create(ctx, model.ToFilter(), nil, rr[i]))
}
t.Log("inform rbac service about new roles")
rbacService.UpdateRoles(
rbac.CommonRole.Make(testerRole.ID, testerRole.Name),
)
t.Log("log-in with test user ")
ctx = auth.SetIdentityToContext(ctx, auth.Authenticated(user.ID, testerRole.ID))
req.NoError(rbacService.Grant(ctx, rbac.AllowRule(testerRole.ID, mod.RbacResource(), "records.search")))
t.Log("search for the newly created records; should not find any (all denied)")
f.IncTotal = true
hits, f, err = svc.Find(ctx, f)
req.NoError(err)
req.Len(hits, 0)
req.Equal(uint(0), f.Total)
t.Log("allow read access for two records")
req.NoError(rbacService.Grant(ctx, rbac.AllowRule(testerRole.ID, rr[3].RbacResource(), "read")))
req.NoError(rbacService.Grant(ctx, rbac.AllowRule(testerRole.ID, rr[6].RbacResource(), "read")))
t.Log("search for the newly created records; should find 2 we're allowed to read")
f.IncTotal = true
hits, f, err = svc.Find(ctx, f)
req.NoError(err)
req.Len(hits, 2)
req.Equal(uint(2), f.Total)
}
func TestRecord_contextualRolesAccessControl(t *testing.T) {
var (
err error
req = require.New(t)
ctx = context.Background()
//log = zap.NewNop()
log = logger.MakeDebugLogger()
rbacService = rbac.NewService(log, nil)
nextIDi uint64 = 1
nextID = func() uint64 {
nextIDi++
return nextIDi
}
user = &sysTypes.User{ID: nextID()}
ns = &types.Namespace{ID: nextID()}
mod = &types.Module{
ID: nextID(),
NamespaceID: ns.ID,
Name: "mod one",
Config: types.ModuleConfig{
DAL: types.ModuleConfigDAL{ConnectionID: 1},
},
}
numField = &types.ModuleField{ID: nextID(), NamespaceID: ns.ID, ModuleID: mod.ID, Name: "num", Kind: "String"}
boolField = &types.ModuleField{ID: nextID(), NamespaceID: ns.ID, ModuleID: mod.ID, Name: "yes", Kind: "String"}
svc = makeTestRecordService(t,
rbacService,
log,
user,
ns,
mod,
numField,
boolField,
)
baseRole = &sysTypes.Role{Name: "base", ID: nextID()}
ownerRole = &sysTypes.Role{Name: "owner", ID: nextID()}
truthyRole = &sysTypes.Role{Name: "whenBoolTrue", ID: nextID()}
tttRole = &sysTypes.Role{Name: "whenNum333", ID: nextID()}
rr = make([]*types.Record, 10)
hits []*types.Record
f = types.RecordFilter{
ModuleID: mod.ID,
NamespaceID: ns.ID,
}
// search for module's model to be used as a filter
model = ModelRef(svc, mod.ID)
// setting up rbac context role expression parser
roleCheckFnMaker = func(expression string) func(scope map[string]interface{}) bool {
p := expr.NewParser()
return func(scope map[string]interface{}) bool {
v, _ := expr.NewVars(scope)
if e, err := p.Parse(expression); err != nil {
t.Logf("could not parse expression: %v", err)
} else if c, err := e.Test(ctx, v); err != nil {
t.Logf("could not exec expression: %v", err)
} else {
return c
}
return false
}
}
)
t.Log("create test records")
for i := 0; i < cap(rr); i++ {
rr[i] = &types.Record{
ID: nextID(),
NamespaceID: ns.ID,
ModuleID: mod.ID,
}
rr[i].SetModule(mod)
if i%2 == 0 {
// let's own half of the records
rr[i].OwnedBy = user.ID
}
if i%3 == 0 {
// set 333 to num on every 3rd record
rr[i].Values = rr[i].Values.Set(&types.RecordValue{Name: "num", Value: "333"})
}
if i >= 5 {
// and set true to bool field on the last 5 records
rr[i].Values = rr[i].Values.Set(&types.RecordValue{Name: "yes", Value: "1"})
}
req.NoError(svc.dal.Create(ctx, model.ToFilter(), nil, rr[i]))
}
// result
// i 0 1 2 3 4 5 6 7 8 9
// --------------------------
// owner x x x x x
// bool x x x x x
// 333 x x x x
// --------------------------
// read: x x x x x x x x x (all but one)
t.Log("inform rbac service about new roles")
rbacService.UpdateRoles(
rbac.CommonRole.Make(baseRole.ID, baseRole.Name),
rbac.MakeContextRole(ownerRole.ID, ownerRole.Name, roleCheckFnMaker("resource.ownedBy == userID"), types.RecordResourceType),
rbac.MakeContextRole(truthyRole.ID, truthyRole.Name, roleCheckFnMaker(`has(resource.values, "yes") ? resource.values.yes : false`), types.RecordResourceType),
rbac.MakeContextRole(tttRole.ID, tttRole.Name, roleCheckFnMaker(`has(resource.values, "num") ? resource.values.num == 333 : false`), types.RecordResourceType),
)
req.NoError(rbacService.Grant(ctx, rbac.AllowRule(baseRole.ID, types.ModuleRbacResource(0, 0), "records.search")))
t.Log("log-in with test user")
ctx = auth.SetIdentityToContext(ctx, auth.Authenticated(user.ID, baseRole.ID))
t.Log("expecting not find any (all denied)")
hits, _, err = svc.Find(ctx, f)
req.NoError(err)
req.Len(hits, 0)
t.Log("expecting to find 5 records (owned by us)")
req.NoError(rbacService.Grant(ctx, rbac.AllowRule(ownerRole.ID, types.RecordRbacResource(0, 0, 0), "read")))
hits, _, err = svc.Find(ctx, f)
req.NoError(err)
req.Len(hits, 5)
t.Log("expecting to find 2 records (owned by us and with true value for 'yes' field)")
req.NoError(rbacService.Grant(ctx, rbac.AllowRule(truthyRole.ID, types.RecordRbacResource(0, 0, 0), "read")))
hits, _, err = svc.Find(ctx, f)
req.NoError(err)
req.Len(hits, 8)
t.Log("expecting to find 2 records (owned by us and with true value for 'yes' field + 333 for num)")
req.NoError(rbacService.Grant(ctx, rbac.AllowRule(tttRole.ID, types.RecordRbacResource(0, 0, 0), "read")))
hits, _, err = svc.Find(ctx, f)
req.NoError(err)
req.Len(hits, 9)
}
func TestSetRecordOwner(t *testing.T) {
var (
req = require.New(t)
// uncomment to enable sql conn debugging
//ctx = logger.ContextWithValue(context.Background(), logger.MakeDebugLogger())
ctx = context.Background()
s, err = sqlite.ConnectInMemoryWithDebug(ctx)
)
req.NoError(err)
req.NoError(store.Upgrade(ctx, zap.NewNop(), s))
req.NoError(store.TruncateComposeNamespaces(ctx, s))
req.NoError(store.TruncateComposeModules(ctx, s))
req.NoError(store.TruncateRbacRules(ctx, s))
var (
rbacService = rbac.NewService(
zap.NewNop(),
//logger.MakeDebugLogger(),
nil,
)
ac = &accessControl{rbac: rbacService}
invoker = &sysTypes.User{ID: 1001}
originalOwner = &sysTypes.User{ID: 2001}
alternativeOwner = &sysTypes.User{ID: 2002}
role = &sysTypes.Role{Name: "role-with-ownership-change-permission", ID: 3000}
mod = &types.Module{ID: 3, NamespaceID: 71624}
old, upd *types.Record
)
rbacService.UpdateRoles(
rbac.CommonRole.Make(role.ID, role.Handle),
)
req.NoError(rbacService.Grant(ctx, rbac.AllowRule(role.ID, types.RecordRbacResource(0, 0, 0), "owner.manage")))
req.NoError(rbacService.Grant(ctx, rbac.AllowRule(role.ID, types.ModuleRbacResource(0, 0), "owned-record.create")))
req.NoError(store.CreateUser(ctx, s, invoker, originalOwner, alternativeOwner))
t.Run("invalid input", func(t *testing.T) {
old, upd = nil, nil
verifyRecErrSet(t, SetRecordOwner(ctx, ac, s, old, upd, invoker.ID))
})
t.Run("new record owner is invoker", func(t *testing.T) {
old, upd = nil, &types.Record{ID: 1, NamespaceID: 2, ModuleID: mod.ID}
upd.SetModule(mod)
// this must work, invoker can always set owner to self on a new record
verifyRecErrSet(t, SetRecordOwner(ctx, ac, s, old, upd, invoker.ID))
req.Equal(upd.OwnedBy, invoker.ID)
})
t.Run("deny setting to alternative owner on create", func(t *testing.T) {
old, upd = nil, &types.Record{ID: 1, NamespaceID: 2, ModuleID: 3, OwnedBy: alternativeOwner.ID}
upd.SetModule(mod)
// this must work, invoker can always set owner to self on a new record
if rvse := SetRecordOwner(ctx, ac, s, old, upd, invoker.ID); rvse.IsValid() {
t.Fatalf("expecting error")
}
})
t.Run("deny setting to alternative owner on update", func(t *testing.T) {
old = &types.Record{ID: 1, NamespaceID: 2, ModuleID: 3, OwnedBy: originalOwner.ID}
old.SetModule(mod)
upd = &types.Record{ID: 1, NamespaceID: 2, ModuleID: 3, OwnedBy: alternativeOwner.ID}
upd.SetModule(mod)
// this must work, invoker can always set owner to self on a new record
if rvse := SetRecordOwner(ctx, ac, s, old, upd, invoker.ID); rvse.IsValid() {
t.Fatalf("expecting error")
}
})
t.Run("allow setting to alternative owner on create", func(t *testing.T) {
ctx = auth.SetIdentityToContext(ctx, auth.Authenticated(invoker.ID, role.ID))
old, upd = nil, &types.Record{ID: 1, NamespaceID: 2, ModuleID: 3, OwnedBy: alternativeOwner.ID}
upd.SetModule(mod)
verifyRecErrSet(t, SetRecordOwner(ctx, ac, s, old, upd, invoker.ID))
req.Equal(upd.OwnedBy, alternativeOwner.ID)
})
t.Run("allow setting to alternative owner on update", func(t *testing.T) {
ctx = auth.SetIdentityToContext(ctx, auth.Authenticated(invoker.ID, role.ID))
old = &types.Record{ID: 1, NamespaceID: 2, ModuleID: 3, OwnedBy: originalOwner.ID}
old.SetModule(mod)
upd = &types.Record{ID: 1, NamespaceID: 2, ModuleID: 3, OwnedBy: alternativeOwner.ID}
upd.SetModule(mod)
verifyRecErrSet(t, SetRecordOwner(ctx, ac, s, old, upd, invoker.ID))
req.Equal(upd.OwnedBy, alternativeOwner.ID)
})
t.Run("allow setting to new owner to zerp", func(t *testing.T) {
ctx = auth.SetIdentityToContext(ctx, auth.Authenticated(invoker.ID, role.ID))
old = &types.Record{ID: 1, NamespaceID: 2, ModuleID: 3, OwnedBy: originalOwner.ID}
old.SetModule(mod)
upd = &types.Record{ID: 1, NamespaceID: 2, ModuleID: 3, OwnedBy: 0}
upd.SetModule(mod)
verifyRecErrSet(t, SetRecordOwner(ctx, ac, s, old, upd, invoker.ID))
req.Equal(upd.OwnedBy, invoker.ID)
})
}
func TestRecordReportToDalPipeline(t *testing.T) {
mod := &types.Module{
ID: 10,
NamespaceID: 42,
}
t.Run("no additional metrics", func(t *testing.T) {
pp, _, err := recordReportToDalPipeline(mod, "", "created_at", "")
require.NoError(t, err)
require.Len(t, pp, 2)
agg := pp[1].(*dal.Aggregate)
require.Len(t, agg.OutAttributes, 1)
require.Equal(t, "count", agg.OutAttributes[0].Identifier)
})
t.Run("additional metrics with alias", func(t *testing.T) {
pp, _, err := recordReportToDalPipeline(mod, "MAX(numbers) AS something", "created_at", "")
require.NoError(t, err)
require.Len(t, pp, 2)
agg := pp[1].(*dal.Aggregate)
require.Len(t, agg.OutAttributes, 2)
require.Equal(t, "something", agg.OutAttributes[1].Identifier)
require.Equal(t, "MAX(numbers)", agg.OutAttributes[1].RawExpr)
})
t.Run("additional metrics without alias", func(t *testing.T) {
pp, _, err := recordReportToDalPipeline(mod, "MAX(numbers)", "created_at", "")
require.NoError(t, err)
require.Len(t, pp, 2)
agg := pp[1].(*dal.Aggregate)
require.Len(t, agg.OutAttributes, 2)
require.Equal(t, "MAX(numbers)", agg.OutAttributes[1].Identifier)
require.Equal(t, "MAX(numbers)", agg.OutAttributes[1].RawExpr)
})
}