Skip to content

regression: honor the store-owner encoding written up to 1.20 - #95

Open
r4nc0r wants to merge 2 commits into
grommunio:masterfrom
r4nc0r:fix/storeowner-legacy-encoding
Open

regression: honor the store-owner encoding written up to 1.20#95
r4nc0r wants to merge 2 commits into
grommunio:masterfrom
r4nc0r:fix/storeowner-legacy-encoding

Conversation

@r4nc0r

@r4nc0r r4nc0r commented Sep 2, 2026

Copy link
Copy Markdown

The problem

After upgrading to 1.21 the store-owner list of every shared mailbox comes up empty, while the users concerned still have store ownership as far as gromox is concerned and can still open the mailbox. Removing and re-adding through the UI appears to work but leaves the old grant in place.

storeAccess has encoded "store owner" in the IPM_SUBTREE ACL three different ways, each generation reading back exactly what it wrote, so the change is invisible until an upgrade:

commit first release writes / reads
8b22f25 1.3 (2021-10-04) GROMOXSTOREOWNER = 0x2000
d8557b3 FOLDEROWNER = 0x100
bb64403 1.21 STOREACCESS = FOLDEROWNER|FOLDERCONTACT|FOLDERVISIBLE = 0x700

No migration exists for the rows the first generation left behind, and gromox grants store ownership from a literal 0x2000 on its own: get_mbox_perm (exch/exmdb/store.cpp) ors the stored rights in verbatim, in addition to deriving ownership from FOLDEROWNER on IPM_SUBTREE. Those grants are therefore still in force, and STOREACCESS matches none of their bits. Concretely, for an ACE of 0x3800:

  • getUserStoreAccess omits it, so an administrator auditing a shared mailbox is told nobody has access when somebody does;
  • deleteUserStoreAccess clears 0x700 only, so a revoke returns success and changes nothing;
  • setUserStoreAccessMulti cannot revoke by omission either, for the same reason.

Reproduced on gromox 3.10.25.m1692b65, one grantee on IPM_SUBTREE of a shared mailbox, read back with grommunio-admin exmdb <mailbox> folder permissions 0x9:

step UI stored ACE
after upgrade not listed 0x3800 (freebusysimple,freebusydetailed,storeowner)
set store owner listed 0x3f00
remove everything not listed 0x3800

The delta is exactly 0x700; the 0x3800 residue is never touched and the row is never removed.

The change

Two commits, separable.

1. Honor the store-owner encoding written up to 1.20. Match GROMOXSTOREOWNER as well when listing, clear it when revoking, and drop it when granting so a row is normalized on first write. Because setFolderMember/setFolderMembers already delete a row once no rights remain, revoking a grant that carries nothing else now removes the entry outright rather than leaving a husk.

2. List store owners by folderowner, not by any STOREACCESS bit. STOREACCESS is the right mask to write but the wrong one to match on: an entry carrying only FOLDERCONTACT or FOLDERVISIBLE was reported as a store owner despite having no such rights, which is the shape a client-set folder ACL routinely has. This is the over-reporting half of #93. The rest of that report, a client-granted entry keeping its working rights on IPM_SUBTREE after the store-owner grant is revoked, is deliberately left alone: those bits were not granted through this endpoint, and dropping them is a separate decision.

Masks after the change: writes use STOREACCESS, listing uses STOREOWNER_EFFECTIVE (FOLDEROWNER|GROMOXSTOREOWNER, i.e. what gromox itself would answer), revoking uses STOREACCESS_ANY (STOREACCESS|GROMOXSTOREOWNER).

Testing

pyexmdb is not available in my environment, so the endpoints were not exercised against a live store. What was checked instead: the ACL primitives were reimplemented from libexmdbpp src/queries.cpp (setFolderMember, setFolderMembers) and the masks imported from the patched tools/constants.py, then the invariant "listed by getUserStoreAccess if and only if gromox considers the user a store owner" was asserted over the call sequences this patch actually emits:

case rights after listed gromox owner
legacy 0x3800, listed? 0x3800 yes yes
legacy + grant 0x1f00 yes yes
legacy + revoke 0x1800 no no
bare 0x2000 + revoke row deleted no no
modern 0x700 + revoke row deleted no no
legacy dropped from bulk PUT 0x1800 no no
legacy kept in bulk PUT 0x1f00 yes yes
client-granted owner 0x7fb 0x7fb yes yes
contact+visible 0x600 0x600 no no
reviewer 0x401 0x401 no no

The two rows that disagreed before the change are the legacy ones, and 0x600 is the row that commit 2 fixes. A live confirmation on a real store would still be worth having before release.

Note on affected deployments

Because the first encoding shipped in 1.3, any installation upgraded from <=1.20 to >=1.21 is affected for every store owner granted before the upgrade. This patch makes those grants visible and revocable again, and normalizes each row the first time it is written, but it does not rewrite rows on its own. Affected rows can be listed with:

grommunio-admin exmdb <mailbox> folder permissions 0x9

Anything printing storeowner without folderowner was written by <=1.20. Note that grommunio-admin user <mailbox> storeowner list used the same filter as the API and so could not be used to find these either; it is fixed by the same commits.

A related gap this patch does not address: UserSecondaryStores is maintained only by the current endpoints, so mailboxes whose owners were granted under the old encoding are likely missing rows there as well.

Store ownership has been encoded three ways in the IPM_SUBTREE ACL:
GROMOXSTOREOWNER (0x2000) from 1.3 up to 1.20, FOLDEROWNER alone in
d8557b3, and STOREACCESS (0x700) since bb64403. Each generation read
back what it wrote, so the change only becomes visible on upgrade, and
no migration exists for the rows the first one left behind.

gromox grants store ownership from a literal 0x2000 on its own, because
get_mbox_perm ors the stored rights in verbatim in addition to deriving
ownership from FOLDEROWNER on IPM_SUBTREE. Those old grants are thus
still in force, while STOREACCESS matches none of their bits:

  - getUserStoreAccess omits them, so the list of a shared mailbox comes
    up empty although the users still have access and can open it
  - deleteUserStoreAccess clears 0x700 only, so a revoke reports success
    and leaves ownership in place
  - setUserStoreAccessMulti likewise cannot revoke by omission

Match on GROMOXSTOREOWNER as well when listing, clear it when revoking,
and drop it when granting so a row is normalized on first write. Since
setFolderMember(s) deletes a row once no rights remain, revoking a grant
that carries nothing else now removes the entry outright.
STOREACCESS is the right mask to write, but the wrong one to match on:
gromox derives store ownership from FOLDEROWNER on IPM_SUBTREE, so an
entry carrying only FOLDERCONTACT or FOLDERVISIBLE was reported as a
store owner while having no such rights. A folder ACL set by a client
routinely has that shape, which is why the list could show entries that
could not be removed through it.

Match FOLDEROWNER (or the legacy bit) instead, so the list says what
gromox would answer.

This covers the over-reporting half of grommunio#93. The remaining part of that
report, a client-granted entry keeping its working rights on IPM_SUBTREE
after the store-owner grant is revoked, is deliberately left alone here:
those bits were not granted through this endpoint, and dropping them is
a separate decision.
@jengelh

jengelh commented Sep 8, 2026

Copy link
Copy Markdown
Member

we can do an automated one-time replacement, cf. grommunio/gromox@8e0dd96

@jengelh

jengelh commented Sep 8, 2026

Copy link
Copy Markdown
Member

All the tests (e.g. x & ...) should look at only 0x100, not 0x700, because that one bit ultimately controls the grant.

Setting 0x600 is not strictly needed for the store superuser bit. 0x600 is mimicing gromox's permission_adjust(uint32_t v)'s logic that is meant for regular folder permission grants.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants