diff --git a/system/service/role.go b/system/service/role.go index 61f407ecf..a421e7658 100644 --- a/system/service/role.go +++ b/system/service/role.go @@ -837,7 +837,7 @@ func initRoles(ctx context.Context, log *zap.Logger, opt options.RBACOpt, eb eve DefaultRole.SetClosed(tmp...) // Initial RBAC update - if err = updateRbacRoles(ctx, log, ru, bypass, authenticated, anonymous); err != nil { + if err = UpdateRbacRoles(ctx, log, ru, bypass, authenticated, anonymous); err != nil { return } @@ -846,7 +846,7 @@ func initRoles(ctx context.Context, log *zap.Logger, opt options.RBACOpt, eb eve eb.Register( func(_ context.Context, ev eventbus.Event) error { log.Debug("role changed, updating RBAC") - return updateRbacRoles(ctx, log, ru, bypass, authenticated, anonymous) + return UpdateRbacRoles(ctx, log, ru, bypass, authenticated, anonymous) }, eventbus.For("system:role"), eventbus.On("afterUpdate", "afterCreate", "afterDelete"), @@ -855,7 +855,7 @@ func initRoles(ctx context.Context, log *zap.Logger, opt options.RBACOpt, eb eve return nil } -func updateRbacRoles(ctx context.Context, log *zap.Logger, ru rbacRoleUpdater, bypass, authenticated, anonymous map[string]bool) error { +func UpdateRbacRoles(ctx context.Context, log *zap.Logger, ru rbacRoleUpdater, bypass, authenticated, anonymous map[string]bool) error { var ( p = expr.NewParser() f = types.RoleFilter{} diff --git a/tests/compose/main_test.go b/tests/compose/main_test.go index 5bd598236..7aa792f7e 100644 --- a/tests/compose/main_test.go +++ b/tests/compose/main_test.go @@ -111,14 +111,18 @@ func newHelper(t *testing.T) helper { h.cUser.SetRoles(h.roleID) helpers.UpdateRBAC(h.roleID) + h.identityToHelper(h.cUser) + return h +} + +func (h *helper) identityToHelper(u *sysTypes.User) { var err error - h.token, err = auth.DefaultJwtHandler.Generate(context.Background(), h.cUser) + h.cUser = u + h.token, err = auth.DefaultJwtHandler.Generate(context.Background(), u) if err != nil { panic(err) } - - return h } func (h helper) MyRole() uint64 { @@ -184,7 +188,7 @@ func cleanup(t *testing.T) { } func loadScenario(ctx context.Context, s store.Storer, t *testing.T, h helper) { - loadScenarioWithName(ctx, s, t, h, "S"+t.Name()[4:]) + loadScenarioWithName(ctx, s, t, h, t.Name()[5:]) } func loadScenarioWithName(ctx context.Context, s store.Storer, t *testing.T, h helper, scenario string) { diff --git a/tests/compose/0001_namespace_duplicate_test.go b/tests/compose/namespace_duplicate_test.go similarity index 98% rename from tests/compose/0001_namespace_duplicate_test.go rename to tests/compose/namespace_duplicate_test.go index 4e8439c3c..0ec076665 100644 --- a/tests/compose/0001_namespace_duplicate_test.go +++ b/tests/compose/namespace_duplicate_test.go @@ -72,7 +72,7 @@ func findPageByHandle(pp types.PageSet, h string) *types.Page { return nil } -func Test0001_namespace_duplicate(t *testing.T) { +func Test_namespace_duplicate(t *testing.T) { ctx, h, s := setup(t) loadScenario(ctx, defStore, t, h) ns, _, _, _, _, err := fetchEntireNamespace(ctx, s, "ns1") diff --git a/tests/compose/record_access_context_test.go b/tests/compose/record_access_context_test.go new file mode 100644 index 000000000..7615fdc0b --- /dev/null +++ b/tests/compose/record_access_context_test.go @@ -0,0 +1,154 @@ +package compose + +import ( + "context" + "fmt" + "net/http" + "testing" + + "github.com/cortezaproject/corteza-server/compose/types" + "github.com/cortezaproject/corteza-server/pkg/rbac" + "github.com/cortezaproject/corteza-server/store" + "github.com/cortezaproject/corteza-server/system/service" + systemTypes "github.com/cortezaproject/corteza-server/system/types" + "github.com/cortezaproject/corteza-server/tests/helpers" +) + +func crissCrossUserRoles(ctx context.Context, s store.Storer, h helper, uu systemTypes.UserSet, rr systemTypes.RoleSet) (map[string]*systemTypes.User, map[string]*systemTypes.Role) { + ux := make(map[string]*systemTypes.User) + for _, r := range uu { + ux[r.Name] = r + } + + rx := make(map[string]*systemTypes.Role) + for _, r := range rr { + rx[r.Handle] = r + } + + for _, u := range uu { + if _, ok := rx[u.Name]; !ok { + h.a.FailNow(fmt.Sprintf("corresponding role not found for user: r: %s; u: %s", u.Name, u.Handle)) + } + rID := rx[u.Name].ID + h.a.NoError(store.CreateRoleMember(ctx, s, &systemTypes.RoleMember{ + RoleID: rID, + UserID: u.ID, + })) + u.SetRoles(rID) + } + + return ux, rx +} + +func Test_record_access_context(t *testing.T) { + ctx, h, s := setup(t) + loadScenario(ctx, defStore, t, h) + + // setup + rr, _, err := store.SearchRoles(ctx, s, systemTypes.RoleFilter{}) + h.a.NoError(err) + uu, _, err := store.SearchUsers(ctx, s, systemTypes.UserFilter{}) + h.a.NoError(err) + + ux, rx := crissCrossUserRoles(ctx, s, h, uu, rr) + + ns, err := store.LookupComposeNamespaceBySlug(ctx, s, "ns1") + h.a.NoError(err) + + mod, err := store.LookupComposeModuleByNamespaceIDHandle(ctx, s, ns.ID, "mod1") + h.a.NoError(err) + + records, _, err := store.SearchComposeRecords(ctx, s, mod, types.RecordFilter{}) + h.a.NoError(err) + rec := records[0] + + for _, r := range rr { + helpers.Allow(r, types.NamespaceRbacResource(0), "read") + helpers.Allow(r, types.ModuleRbacResource(0, 0), "read") + } + + helpers.DenyMe(h, rec.RbacResource(), "read") + helpers.Allow(rx["owner"], rec.RbacResource(), "read") + helpers.Allow(rx["creator"], rec.RbacResource(), "read") + helpers.Allow(rx["updater"], rec.RbacResource(), "read") + helpers.Allow(rx["deleter"], rec.RbacResource(), "read") + + h.a.NoError(service.UpdateRbacRoles(ctx, testApp.Log, rbac.Global(), nil, nil, nil)) + rbac.Global().Reload(ctx) + + t.Run("generic user with no ctx role", func(t *testing.T) { + h.apiInit(). + Get(fmt.Sprintf("/namespace/%d/module/%d/record/%d", mod.NamespaceID, mod.ID, rec.ID)). + Header("Accept", "application/json"). + Expect(t). + Status(http.StatusOK). + Assert(helpers.AssertError("record.errors.notAllowedToRead")). + End() + }) + + t.Run("user with owner ctx role", func(t *testing.T) { + h.identityToHelper(ux["owner"]) + h.apiInit(). + Get(fmt.Sprintf("/namespace/%d/module/%d/record/%d", mod.NamespaceID, mod.ID, rec.ID)). + Header("Accept", "application/json"). + Expect(t). + Status(http.StatusOK). + Assert(helpers.AssertNoErrors). + End() + }) + + t.Run("user with creator ctx role", func(t *testing.T) { + h.identityToHelper(ux["creator"]) + h.apiInit(). + Get(fmt.Sprintf("/namespace/%d/module/%d/record/%d", mod.NamespaceID, mod.ID, rec.ID)). + Header("Accept", "application/json"). + Expect(t). + Status(http.StatusOK). + Assert(helpers.AssertNoErrors). + End() + }) + + t.Run("user with updater ctx role", func(t *testing.T) { + h.identityToHelper(ux["updater"]) + h.apiInit(). + Get(fmt.Sprintf("/namespace/%d/module/%d/record/%d", mod.NamespaceID, mod.ID, rec.ID)). + Header("Accept", "application/json"). + Expect(t). + Status(http.StatusOK). + Assert(helpers.AssertNoErrors). + End() + }) + + t.Run("user with deleter ctx role", func(t *testing.T) { + h.identityToHelper(ux["deleter"]) + h.apiInit(). + Get(fmt.Sprintf("/namespace/%d/module/%d/record/%d", mod.NamespaceID, mod.ID, rec.ID)). + Header("Accept", "application/json"). + Expect(t). + Status(http.StatusOK). + Assert(helpers.AssertNoErrors). + End() + }) + + t.Run("user with creator ctx role", func(t *testing.T) { + h.identityToHelper(ux["creator"]) + h.apiInit(). + Get(fmt.Sprintf("/namespace/%d/module/%d/record/%d", mod.NamespaceID, mod.ID, rec.ID)). + Header("Accept", "application/json"). + Expect(t). + Status(http.StatusOK). + Assert(helpers.AssertNoErrors). + End() + }) + + t.Run("user with creator ctx role", func(t *testing.T) { + h.identityToHelper(ux["creator"]) + h.apiInit(). + Get(fmt.Sprintf("/namespace/%d/module/%d/record/%d", mod.NamespaceID, mod.ID, rec.ID)). + Header("Accept", "application/json"). + Expect(t). + Status(http.StatusOK). + Assert(helpers.AssertNoErrors). + End() + }) +} diff --git a/tests/compose/testdata/S0001_namespace_duplicate/data_model/1000_namespace.yaml b/tests/compose/testdata/namespace_duplicate/data_model/1000_namespace.yaml similarity index 100% rename from tests/compose/testdata/S0001_namespace_duplicate/data_model/1000_namespace.yaml rename to tests/compose/testdata/namespace_duplicate/data_model/1000_namespace.yaml diff --git a/tests/compose/testdata/S0001_namespace_duplicate/data_model/1100_modules.yaml b/tests/compose/testdata/namespace_duplicate/data_model/1100_modules.yaml similarity index 100% rename from tests/compose/testdata/S0001_namespace_duplicate/data_model/1100_modules.yaml rename to tests/compose/testdata/namespace_duplicate/data_model/1100_modules.yaml diff --git a/tests/compose/testdata/S0001_namespace_duplicate/data_model/1200_pages.yaml b/tests/compose/testdata/namespace_duplicate/data_model/1200_pages.yaml similarity index 100% rename from tests/compose/testdata/S0001_namespace_duplicate/data_model/1200_pages.yaml rename to tests/compose/testdata/namespace_duplicate/data_model/1200_pages.yaml diff --git a/tests/compose/testdata/S0001_namespace_duplicate/data_model/1300_charts.yaml b/tests/compose/testdata/namespace_duplicate/data_model/1300_charts.yaml similarity index 100% rename from tests/compose/testdata/S0001_namespace_duplicate/data_model/1300_charts.yaml rename to tests/compose/testdata/namespace_duplicate/data_model/1300_charts.yaml diff --git a/tests/compose/testdata/record_access_context/data_model/compose.yaml b/tests/compose/testdata/record_access_context/data_model/compose.yaml new file mode 100644 index 000000000..d948bcd67 --- /dev/null +++ b/tests/compose/testdata/record_access_context/data_model/compose.yaml @@ -0,0 +1,30 @@ +namespaces: + ns1: + name: ns1 name + +modules: + mod1: + name: mod1 name + fields: + f1: + label: f1 label + kind: String + required: true + f2: + label: f2 label + kind: Select + options: + options: + - f2 opt 1 + - f2 opt 2 + - f2 opt 3 + +records: + mod1: + - createdBy: user_createdby + updatedBy: user_updatedby + deletedBy: user_deletedby + ownedBy: user_ownedby + values: + f1: "mod1 f1 v1" + f2: "f2 opt 1" diff --git a/tests/compose/testdata/record_access_context/data_model/system.yaml b/tests/compose/testdata/record_access_context/data_model/system.yaml new file mode 100644 index 000000000..38a363471 --- /dev/null +++ b/tests/compose/testdata/record_access_context/data_model/system.yaml @@ -0,0 +1,49 @@ +users: + user_createdby: + email: user_createdby@test.tld + name: creator + + user_updatedby: + email: user_updatedby@test.tld + name: updater + + user_deletedby: + email: user_deletedby@test.tld + name: deleter + + user_ownedby: + email: user_ownedby@test.tld + name: owner + +roles: + owner: + name: Owner + meta: + context: + expr: resource.ownedBy == userID + resourceType: + - corteza::compose:record + + creator: + name: Creator + meta: + context: + expr: resource.createdBy == userID + resourceType: + - corteza::compose:record + + updater: + name: Updater + meta: + context: + expr: resource.updatedBy == userID + resourceType: + - corteza::compose:record + + deleter: + name: Deleter + meta: + context: + expr: resource.deletedBy == userID + resourceType: + - corteza::compose:record diff --git a/tests/helpers/rbac.go b/tests/helpers/rbac.go index 4221c6e7a..f7739ab80 100644 --- a/tests/helpers/rbac.go +++ b/tests/helpers/rbac.go @@ -7,6 +7,7 @@ import ( "github.com/cortezaproject/corteza-server/pkg/cli" "github.com/cortezaproject/corteza-server/pkg/eventbus" "github.com/cortezaproject/corteza-server/pkg/rbac" + "github.com/cortezaproject/corteza-server/system/types" ) type ( @@ -41,12 +42,24 @@ func AllowMe(mrg myRoleGetter, r string, oo ...string) { } } +func Allow(role *types.Role, r string, oo ...string) { + for _, o := range oo { + Grant(rbac.AllowRule(role.ID, r, o)) + } +} + func DenyMe(mrg myRoleGetter, r string, oo ...string) { for _, o := range oo { Grant(rbac.DenyRule(mrg.MyRole(), r, o)) } } +func Deny(role *types.Role, r string, oo ...string) { + for _, o := range oo { + Grant(rbac.DenyRule(role.ID, r, o)) + } +} + func Grant(rr ...*rbac.Rule) { cli.HandleError(rbac.Global().Grant(context.Background(), rr...)) }