Repository navigation
Fix and finish company sub-accounts (owner checks, Store save, admin owner rules) - #991
Open
KrzysztofPajak wants to merge 7 commits into
Open
KrzysztofPajak wants to merge 7 commits into
KrzysztofPajak wants to merge 7 commits into
Conversation
added 3 commits
October 24, 2026 12:12
Hiding the Subaccounts tab only removed the link: the add, edit and delete actions stayed reachable by URL, and a fresh install hides the tab by default. Every sub-account action now goes through one check, so the setting switches the feature off. The edit command trusted the validator for ownership; it now re-checks it and changes nothing for someone else's, a deleted or a missing account. The edit page answers such an id with a redirect instead of an empty form, the validator gives a missing id and a foreign one the same answer, and the delete endpoint returns the handler's message instead of an empty one. A changed e-mail was stored as typed while sign-in looks it up lowercased, so a sub-account whose owner typed capitals could no longer sign in, and a new password saved together with it failed. The e-mail is stored lowercased and, without usernames, the username follows it. Inactive sub-accounts are marked on the owner's list. Hard-coded messages became resources (also the admin ones used by the next commit).
The Store panel blanked the owner on every customer save, which detached each sub-account a store manager touched. An existing customer now keeps the owner it has; a new one still gets none, and the posted value is never trusted. The admin owner field only checked that the e-mail existed. Sub-accounts stay one level deep now: the owner must be a registered, live customer that is not itself a sub-account, a customer with sub-accounts cannot get an owner, and a customer cannot own itself. The lookup on save uses the same store scope as the validator (it was global, so with per-store identity it could link another store's account), and an owner already linked is kept when posted back unchanged, so legacy data never blocks an unrelated save. The customer form lists an owner's sub-accounts and links a sub-account's owner, read only, on both panels and within each panel's data scope.
Sub-accounts are listed on the storefront and the admin customer form, and the admin checks for them before linking an owner. Without an index each of these scanned the whole customer collection. Fresh installs create it with the other customer indexes, existing ones through a 2.5 migration.
Comment on lines
+31
to
+35
| catch (Exception ex) | ||
| { | ||
| logService.LogError(ex, "UpgradeProcess - CustomerOwnerIndex (2.5)"); | ||
| return false; | ||
| } |
Comment on lines
+390
to
+401
| foreach (var item in subAccounts) | ||
| { | ||
| if (!await _scope.HasAccess(item)) | ||
| continue; | ||
|
|
||
| model.SubAccounts.Add(new CustomerModel.SubAccountModel { | ||
| Id = item.Id, | ||
| Email = item.Email, | ||
| FullName = item.GetFullName(), | ||
| Active = item.Active | ||
| }); | ||
| } |
added 4 commits
October 24, 2026 19:51
- The Store panel replaces the posted owner before saving, so its validator no longer checks the posted value: the answers told a store manager whether another store's e-mail existed, was a sub-account, was registered or had sub-accounts. - An unchanged owner is kept only while the customer stays in its store; a customer moved to another store has its owner looked up there. - A sub-account e-mail differing only in case is repaired on save even when e-mail changes are not allowed. Older edits stored it as typed, and those rows still could not sign in and failed on a password change. - An owner's own deleted sub-account gets the same answer as a missing or foreign one, as the edit page and the command already give.
…tore saves The Store panel blanked the sales employee, vendor and staff store and reset the groups to Registered on every customer save. A store manager fixing a typo in a name took away the customer's sales employee and price groups, and a colleague's Store manager role. An existing customer now keeps all of these as stored; a new one still gets none of them and Registered only. Posted values are never used either way.
My account saved a changed e-mail or username as typed, while sign-in looks both up lowercased, so a customer who typed capitals could no longer sign in. Both are stored lowercased now, a value differing only in case repairs a row older edits stored as typed, and without usernames the username follows the e-mail as registration sets it.
The Store panel is for a store's registered customers. Its scope also took in any registered account of the store, staff included: administrators, vendors, store managers, sales managers and accounts tied to a vendor or a staff store. These are now outside the panel - left out of the customer list in the query, so paging and totals stay right, and refused when opened by id, which covers every customer tab. GetAllCustomers gains two optional filters for this (excluded groups, and accounts tied to a vendor or a staff store); existing callers are unchanged.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Type: bugfix
Issue
Company accounts in GrandNode are the owner / sub-account pair (
Customer.OwnerId): an owner creates sub-accounts for its staff on the storefront (My account → Subaccounts), sees their orders and merchandise returns, and an administrator can link an owner by e-mail on the customer form. Several defects make this unreliable for a business customer:John@Firm.comcan no longer sign in. Saving a new password in the same edit fails, because the password change looks the account up by that e-mail.ApplyPostConstraintsblankedOwneron every save, and the shared update path then clearedOwnerId.ModelState, not the handler's result). Messages were hard-coded English, one of them a raw resource key ("Account is not registered").Me@Shop.COM, or a usernameJohn, could no longer sign in.OwnerId).Solution
No new concept. The existing owner / sub-account model is fixed and finished.
Storefront (
Grand.Web)SubAccountsDenied): an owner only, and the setting really switches the feature off.SubAccountEditCommandHandlerre-checks ownership and changes nothing for a foreign, deleted or missing account. It stores the e-mail lowercased, and without usernames the username follows the e-mail, the same asSubAccountAddCommandHandlercreates it.GetSubAccountHandlerreturnsnullinstead of an empty model, and the controller redirects to the list.SubAccountEditValidatorchecks ownership first and gives a missing id and someone else's id the same answer (no id enumeration, no state disclosure). It no longer throws on an empty e-mail.UpdateCustomerInfoCommandHandler(My account) stores a changed e-mail and username lowercased. A value differing only in case repairs a row stored as typed, and without usernames the username follows the e-mail, as registration sets it.Admin and Store panels (
Grand.Web.AdminShared,Grand.Web.Store)CustomerValidatorkeeps sub-accounts one level deep. The owner must be a registered, live customer that is not itself a sub-account. A customer with sub-accounts cannot get an owner, and a customer cannot own itself. An owner that is already linked stays valid when posted back unchanged, so legacy data never blocks an unrelated save.CustomerViewModelService.ResolveOwnerIduses the same store scope as the validator and keeps the linked owner when it is unchanged.ApplyPostConstraints): an existing customer keeps where it belongs as stored: owner, vendor, staff store, sales employee and groups. A new customer gets none of them and Registered only. Posted values are never used either way, so nothing can be smuggled in.StoreCustomerDataScope: the Store panel covers the store's registered customers only. Staff accounts are left out of its customer list in the query (new optional filters onGetAllCustomersandGetCustomerQuery), so paging and totals stay right. They are also refused when opened by id, which covers every customer tab throughLoadAuthorizedCustomer.HasAccess.Data
OwnerId_1onCustomer: created for fresh installs inInstallationService, and for existing installations byMigrationCustomerOwnerIndex(2.5).DefaultLanguage.xmlandUpgrade/en_250.xml.Patterns followed:
MigrationRecentlyViewedProductCustomerIndexfor the migration, the order-details static tables for the admin card, andIAdminDataScope<Customer>injected into a shared service as inMerchandiseReturnViewModelService.Breaking changes
ICustomerService.GetAllCustomersgains two optional parameters at the end. Source-compatible, but a plugin compiled against the old signature that calls it must be rebuilt.CustomerViewModelServicetakes two more parameters (CustomerConfig,IAdminDataScope<Customer>). DI resolves them. Only code that constructs or subclasses it by hand needs updating.CustomerModelgains display-only properties (OwnerCustomerId,OwnerCustomerEmail,SubAccounts). This is additive, nothing is removed or renamed.Testing
Unit tests (all green on this branch):
AccountControllerSubAccountsTests(10),SubAccountEditCommandHandlerTests(12),SubAccountDeleteCommandHandlerTests(4),GetSubAccountHandlerTests(5),SubAccountEditValidatorTests(7),UpdateCustomerInfoCommandHandlerTests(+8)CustomerValidatorOwnerTests(13),CustomerViewModelServiceTests(+12),StoreCustomerDataScopeTests(+7)GetCustomerQueryHandlerTests(+2)CustomerControllerTests(+3)The whole solution builds with 0 errors.
E2E (Playwright, fresh install with sample data, per-store identity on, 48/48 checks, repeated green runs). Run against the old Store panel behaviour, the owner, sales employee and price group checks fail, so they are not vacuous. To repeat by hand:
/account/subaccounts: you are redirected. A POST to/account/subaccountaddwith a valid token creates nothing.Sub.Two@Firm.COMand a new password in one save. The save succeeds, and the sub-account signs in asSUB.TWO@FIRM.COMwith the new password./account/subaccountssends you to sign in, and a POST to/account/subaccountaddis refused./account/subaccountedit?id=<first owner's sub-account>: you land on your own list and nothing of the account is shown. POST edit and delete for that id: nothing changes, and delete answers "This account is not one of your subaccounts." The same holds for the first owner's own id./Store/Customer/Edit/<sub-account>the owner is shown read only. Change the last name and save: the name is stored, and the owner, sales employee and group are all kept./Store/Customer/Edit/<id>of either redirects to the list. Plain customers are still listed and editable.Owner.New@Firm.COM. It is stored lowercased, and you can sign in typing it in any case.Upgrade path (verified on the E2E database): with the
OwnerId_1index and the 8 new resources removed and the version stamped back to 2.4, the first start ranIndex on Customer owner 2.5together with the other 2.5 migrations. Afterwards the index exists, all 8 resources are imported fromen_250.xml, and the stamp is 2.5 again. Rollback: revert the code. The index is harmless if left behind, or can be dropped withdb.Customer.dropIndex("OwnerId_1").Mutation check: putting back three of the old behaviours one at a time (the hidden tab not blocking, the edit command without its own ownership check, the Store panel blanking the owner) makes 8 tests fail. Restored, all are green.
Review
The PR was reviewed after it was opened (9 findings). Commit 31ff502 fixes four of them:
Not changed, with reasons:
ResolveOwnerIdkeeps the existing owner when the e-mail resolves to nobody. The validator refuses that case on the Admin panel, and on the Store panel keeping the link is the point. Detaching on a lookup miss is the bug this PR removes.SubAccountDeleteanswers a switched-off feature with a redirect, not JSON. This is unchanged from before for non-owners, and the list (with its delete buttons) is not rendered when the feature is off.ICustomerServicefor every caller: it would change a shared service used by every customer flow. The two storefront callers that actually saved the case as typed (sub-account edit and My account) are fixed directly in this PR instead. The admin path already lowercases throughUpdateCustomerInAdminPanel.HasAccess, and two reads by id in the Store panel's save: the query is already scoped (store, sales employee). Owners have a handful of sub-accounts, and the storefront list is unbounded the same way. Worth paging if real data shows large companies.Self-assessment
What I am confident about. Every defect above has a unit test, three of the fixes were mutation-checked, and there is an E2E check on a real install (35/35, run again after the review fixes). Security-relevant paths (foreign ids, a sub-account acting as an owner, a switched-off feature, crafted POSTs with a valid antiforgery token, cross-store disclosure in the Store panel) are covered. Scoping follows the panels' existing
IAdminDataScope, and nothing new is trusted from the posted model.What is weaker.
develop.Deliberately left out (not universal enough to do without a decision, or a separate change):
Score I'd give it: 8.5/10. The fixes are solid and covered on four levels (unit, mutation, E2E, upgrade). Points off for the review's four valid findings, which a more careful first pass should have caught.