diff --git a/compose/service/module.go b/compose/service/module.go index c3960b6ef..8337d70b9 100644 --- a/compose/service/module.go +++ b/compose/service/module.go @@ -17,6 +17,7 @@ import ( "github.com/cortezaproject/corteza-server/pkg/handle" "github.com/cortezaproject/corteza-server/pkg/label" "github.com/cortezaproject/corteza-server/pkg/locale" + "github.com/cortezaproject/corteza-server/pkg/slice" "github.com/cortezaproject/corteza-server/store" ) @@ -63,6 +64,19 @@ const ( moduleFieldsChanged moduleChanges = 4 ) +var ( + systemFields = slice.ToStringBoolMap([]string{ + "recordID", + "ownedBy", + "createdBy", + "createdAt", + "updatedBy", + "updatedAt", + "deletedBy", + "deletedAt", + }) +) + func Module() *module { return &module{ ac: DefaultAccessControl, @@ -217,6 +231,16 @@ func (svc module) Create(ctx context.Context, new *types.Module) (*types.Module, return ModuleErrInvalidHandle() } + for _, f := range new.Fields { + if systemFields[f.Name] { + return ModuleErrFieldNameReserved() + } + } + + if err != nil { + + } + if ns, err = loadNamespace(ctx, s, new.NamespaceID); err != nil { return err } @@ -574,6 +598,13 @@ func updateModuleFields(ctx context.Context, s store.Storer, new, old *types.Mod f.NamespaceID = new.NamespaceID } + if systemFields[f.Name] && !old.Fields.HasName(f.Name) { + // make sure we're backward compatible, or better: + // if, by some weird case, someone managed to get invalid field name into + // the store, we'll turn a blind eye. + return ModuleErrFieldNameReserved() + } + if f.ModuleID != new.ID { return fmt.Errorf("module id of field %q does not match the module", f.Name) } diff --git a/compose/service/module_actions.gen.go b/compose/service/module_actions.gen.go index f696e28a4..900c49e75 100644 --- a/compose/service/module_actions.gen.go +++ b/compose/service/module_actions.gen.go @@ -628,7 +628,7 @@ func ModuleErrNameNotUnique(mm ...*moduleActionProps) *errors.Error { errors.Meta("resource", "compose:module"), // action log entry; no formatting, it will be applied inside recordAction fn. - errors.Meta(moduleLogMetaKey{}, "used duplicate username ({{module.name}}) for module"), + errors.Meta(moduleLogMetaKey{}, "used duplicate name ({{module.name}}) for module"), errors.Meta(modulePropsMetaKey{}, p), // translation namespace & key @@ -644,6 +644,40 @@ func ModuleErrNameNotUnique(mm ...*moduleActionProps) *errors.Error { return e } +// ModuleErrFieldNameReserved returns "compose:module.fieldNameReserved" as *errors.Error +// +// +// This function is auto-generated. +// +func ModuleErrFieldNameReserved(mm ...*moduleActionProps) *errors.Error { + var p = &moduleActionProps{} + if len(mm) > 0 { + p = mm[0] + } + + var e = errors.New( + errors.KindInternal, + + p.Format("field name is reserved for system fields", nil), + + errors.Meta("type", "fieldNameReserved"), + errors.Meta("resource", "compose:module"), + + errors.Meta(modulePropsMetaKey{}, p), + + // translation namespace & key + errors.Meta(locale.ErrorMetaNamespace{}, "compose"), + errors.Meta(locale.ErrorMetaKey{}, "module.errors.fieldNameReserved"), + + errors.StackSkip(1), + ) + + if len(mm) > 0 { + } + + return e +} + // ModuleErrStaleData returns "compose:module.staleData" as *errors.Error // // diff --git a/compose/service/module_actions.yaml b/compose/service/module_actions.yaml index 1dfbcb0c8..09492a6ff 100644 --- a/compose/service/module_actions.yaml +++ b/compose/service/module_actions.yaml @@ -71,9 +71,12 @@ errors: - error: nameNotUnique message: "name not unique" - log: "used duplicate username ({{module.name}}) for module" + log: "used duplicate name ({{module.name}}) for module" severity: warning + - error: fieldNameReserved + message: "field name is reserved for system fields" + - error: staleData message: "stale data" severity: warning diff --git a/tests/compose/module_test.go b/tests/compose/module_test.go index f2500b27d..a68d9a350 100644 --- a/tests/compose/module_test.go +++ b/tests/compose/module_test.go @@ -266,6 +266,57 @@ func TestModuleFieldsUpdate(t *testing.T) { h.a.Equal(m.Fields[1].Kind, "DateTime") } +func TestModuleUpdateWithReservedFieldName(t *testing.T) { + h := newHelper(t) + h.clearModules() + + helpers.AllowMe(h, types.NamespaceRbacResource(0), "read", "modules.search") + ns := h.makeNamespace("some-namespace") + m := h.makeModule(ns, "some-module", &types.ModuleField{ID: id.Next(), Kind: "String", Name: "ownedBy"}) + helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "update") + + f := m.Fields[0] + fjs := fmt.Sprintf(`{ "name": "%s", "fields": [{ "fieldID": "%d", "name": "ownedBy", "kind": "Number" }]}`, m.Name, f.ID) + h.apiInit(). + Post(fmt.Sprintf("/namespace/%d/module/%d", ns.ID, m.ID)). + Header("Accept", "application/json"). + JSON(fjs). + Expect(t). + Status(http.StatusOK). + Assert(helpers.AssertNoErrors). + End() + + f = m.Fields[0] + fjs = fmt.Sprintf(`{ "name": "%s", "fields": [{ "fieldID": "%d", "name": "updatedBy", "kind": "Number" }]}`, m.Name, f.ID) + h.apiInit(). + Post(fmt.Sprintf("/namespace/%d/module/%d", ns.ID, m.ID)). + Header("Accept", "application/json"). + JSON(fjs). + Expect(t). + Status(http.StatusOK). + Assert(helpers.AssertError("module.errors.fieldNameReserved")). + End() +} + +func TestModuleCreateWithReservedFieldName(t *testing.T) { + h := newHelper(t) + h.clearModules() + + helpers.AllowMe(h, types.NamespaceRbacResource(0), "read", "modules.search") + ns := h.makeNamespace("some-namespace") + helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "update") + + fjs := fmt.Sprintf(`{ "name": "foo", "fields": [{ "name": "ownedBy", "kind": "Number" }]}`) + h.apiInit(). + Post(fmt.Sprintf("/namespace/%d/module/", ns.ID)). + Header("Accept", "application/json"). + JSON(fjs). + Expect(t). + Status(http.StatusOK). + Assert(helpers.AssertError("module.errors.fieldNameReserved")). + End() +} + func TestModuleFieldsUpdate_defaults(t *testing.T) { h := newHelper(t) h.clearModules()