Skip to content

fix: system address book card restriction - #63392

Open
SebastianKrupinski wants to merge 1 commit into
masterfrom
fix/system-address-book
Open

SebastianKrupinski wants to merge 1 commit into
masterfrom
fix/system-address-book

Conversation

@SebastianKrupinski

@SebastianKrupinski SebastianKrupinski commented Aug 18, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • added logic to the system address book to only enumerate users from a users group

Checklist

AI (if applicable)

  • The content of this PR was partly or fully generated using AI

@SebastianKrupinski

Copy link
Copy Markdown
Contributor Author

/backport to stable34

@SebastianKrupinski

Copy link
Copy Markdown
Contributor Author

/backport to stable33

@SebastianKrupinski

Copy link
Copy Markdown
Contributor Author

/backport to stable32

@nextcloud-bot nextcloud-bot mentioned this pull request Aug 20, 2026
@skjnldsv skjnldsv mentioned this pull request Aug 25, 2026
@SebastianKrupinski
SebastianKrupinski requested a review from a team as a code owner August 31, 2026 19:18
@SebastianKrupinski
SebastianKrupinski requested review from Altahrim, icewind1991, leftybournes and salmart-dev and removed request for a team August 31, 2026 19:18
@joshtrichards joshtrichards added bug feature: carddav Related to CardDAV internals labels Sep 1, 2026
if (!is_array($excludedGroupIds) || $excludedGroupIds === []) {
return false;
}
return array_intersect($this->groupManager->getUserGroupIds($user), $excludedGroupIds) !== [];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: the logic here and a similar precedent one in ContactsController is applying the group exclusion in a completely different way than the setting itself advertises: in all other places groups are removed from the user's group list so that the checks about group membership only act on groups that are not in the exclude list.
I guess a common use-case is to have "everyone" groups, but exclude them from counting when checking for group membership and keep the filtering working. With the logic above and the configuration I mentioned, everyone would see everyone else.

Is this the desired outcome?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey! Sorry, just saw your comment.

You are correct, I copied the logic from calendar, and it was broken in calendar.

Fixing now

Signed-off-by: SebastianKrupinski <krupinskis05@gmail.com>
@SebastianKrupinski

This comment was marked as resolved.

@DerDreschner

Copy link
Copy Markdown
Member

/backport to stable35

@SebastianKrupinski

Copy link
Copy Markdown
Contributor Author

@ChristophWurst @kesselb @hamza221 +1 please

@kesselb

kesselb commented Oct 27, 2026

Copy link
Copy Markdown
Contributor

Looking at #63392 with #65812 in mind, I think we should extract the enumeration settings to a own helper in this PR.

class SystemAddressbookAccess {
	public function __construct(
		private IShareManager $shareManager,
		private IGroupManager $groupManager,
	) {
	}

	/** @return list<string>|null group IDs whose members $viewer may see, [] for own card only, null if unrestricted */
	private function getVisibleGroupIds(IUser $viewer): ?array {
		if ($viewer->getBackendClassName() === 'Guests'
			|| !$this->shareManager->allowEnumeration()
			|| ($this->shareManager->limitEnumerationToPhone() && !$this->shareManager->limitEnumerationToGroups())) {
			return [];
		}
		if (!$this->shareManager->limitEnumerationToGroups() && !$this->shareManager->shareWithGroupMembersOnly()) {
			return null;
		}
		return array_values(array_diff(
			$this->groupManager->getUserGroupIds($viewer),
			$this->shareManager->shareWithGroupMembersOnlyExcludeGroupsList(),
		));
	}

	public function canSee(IUser $viewer, IUser $target): bool { /* own → true, guest target → false, null → true, else group intersect */ }

	/** @return list<string>|null */
	public function getVisibleCardUris(IUser $viewer): ?array { /* own + non-guest members of getVisibleGroupIds() */ }
}

@kesselb

kesselb commented Oct 27, 2026

Copy link
Copy Markdown
Contributor

Federation

  • Trusted-server sync breaks with "only share with group members" (SystemAddressbook.php:117, :166)
    Trusted servers authenticate through FedAuth (apps/federation/lib/DAV/FedAuth.php), which sets no session user. The new || $restrictToOwnGroups branch runs before isFederation(). With a null user it throws Forbidden in getChild() and returns [] in getMultipleChildren(). getChanges() then moves every added or modified URI to deleted, so the remote server drops all federated contacts.
    The enumeration restrictions already behave like this, but this setting is far more common. Suggestion: handle the federation request before the restriction branches, and add a test for federation with the setting enabled.

Config

  • Use the IShareManager helpers instead of reading the keys (SystemAddressbook.php:61-64, :100-103, :155-158, :276)
    IShareManager already has allowEnumeration(), limitEnumerationToGroups(), limitEnumerationToPhone(), shareWithGroupMembersOnly() and shareWithGroupMembersOnlyExcludeGroupsList(). The last one already returns [] when the restriction is off, so the $ignoreExcludedGroups parameter isn't needed.
    This also avoids an AppConfigTypeConflictException: getValueBool()/getValueString() throw if a key was stored with a different explicit type (e.g. occ config:app:set ... --type=string), and the system address book would return 500. These keys aren't in the core lexicon, UserPlugin reads them as strings, and the share manager reads them through getAppValue() without type checks.

Performance

  • getChild() loads every member of every group to check one card (SystemAddressbook.php:171)
    The old loop returned on the first match. Now a user in a 20k-member group causes 20k IUser objects and getCardUri() calls on every single-card GET. Resolving the target from the URI (Backend:uid.vcf) and intersecting getUserGroupIds(), as ContactsStore does, costs O(groups) instead.
  • getMultipleChildren() enumerates all group members to keep a few paths (SystemAddressbook.php:122)
    A multiget for 5 cards loads every member of every group the user is in. Checking only the requested paths makes the cost depend on the request size instead of the group size.

Structure

  • Rule reads copied into three methods (SystemAddressbook.php:61, :100, :155)
    The same four reads appear in getChildren(), getMultipleChildren() and getChild(), and every test re-mocks exactly(4) calls. One decision method would remove the triplication. fix(contactsinteraction): skip recent contacts visible in system addr… #65812 needs the same rule in contactsinteraction, so this could become a small service in dav that both PRs use:
  • Naming of getCardsForUsersGroups() (SystemAddressbook.php:273)
    $ignoreExcludedGroups = true actually turns the exclude list on, so it reads backwards. The method also returns card URIs, not cards.

Housekeeping

  • Commit subject has no scope
    fix: system address book card restriction could be fix(dav): restrict system address book to own groups.

This branch has not been deployed

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

Labels

3. to review Waiting for reviews backport-request bug feature: carddav Related to CardDAV internals

Projects

Status: 🏗️ In progress

Development

Successfully merging this pull request may close these issues.

6 participants