mirror of
https://github.com/inverse-inc/sogo.git
synced 2026-05-24 04:45:24 +00:00
feat(carddav): fix issue where only personal contacts can be synced on macOS (#1981). When , append the addressbook name as a username suffix to force macOS to treat each book as a distinct account. Usernames must not contain . Install via the provisioning profile. Closes #6033.
This commit is contained in:
@@ -471,6 +471,26 @@ example.
|
||||
|
||||
Defaults to `YES` when unset.
|
||||
|
||||
|S |SOGoCarddavSingleAddressBookProfile
|
||||
|Enables a per-addressbook CardDAV provisioning profile for macOS Contacts.
|
||||
macOS Contacts.app cannot sync more than one addressbook per CardDAV account
|
||||
and silently picks one through its own discovery, ignoring the
|
||||
_CardDAVPrincipalURL_ in the provisioning profile.
|
||||
|
||||
When this flag is set to `YES`, downloading a CardDAV mobile profile from
|
||||
SOGo produces a URL and a username of the form `<user>!<book>`. The `!book`
|
||||
suffix acts as a server-side alias that resolves to the real user but
|
||||
restricts the exposed addressbook to the selected one. The user can then
|
||||
install one profile per addressbook to sync them as separate accounts.
|
||||
|
||||
The flag must only be enabled when no login in your directory contains the
|
||||
character `!`, otherwise legitimate users would be unable to authenticate
|
||||
(the suffix is stripped before the password check). The legacy macOS
|
||||
behaviour, which restricts macOS Contacts to the `personal` addressbook
|
||||
regardless of which one is requested, is disabled when this flag is on.
|
||||
|
||||
Defaults to `NO` when unset.
|
||||
|
||||
|S |SOGoCalendarDAVAccessEnabled
|
||||
|Parameter controlling WebDAV access to the Calendar collections.
|
||||
|
||||
|
||||
+4
-1
@@ -548,8 +548,11 @@
|
||||
davCurrentUserPrincipal = nil;
|
||||
else
|
||||
{
|
||||
/* loginAlias preserves the <user>!<book> alias used by the
|
||||
per-addressbook CardDAV profile, so that macOS Contacts keeps
|
||||
using the alias URL instead of falling back on the canonical one. */
|
||||
usersUrl = [NSString stringWithFormat: @"%@%@/",
|
||||
[self davURLAsString], login];
|
||||
[self davURLAsString], [activeUser loginAlias]];
|
||||
userHREF = davElementWithContent (@"href", XMLNS_WEBDAV, usersUrl);
|
||||
davCurrentUserPrincipal
|
||||
= [davElementWithContent (@"current-user-principal",
|
||||
|
||||
@@ -387,6 +387,10 @@ static BOOL debugLeaks;
|
||||
//Here the user is expected to be name or name@domain.com
|
||||
//However iOS 18.4 sanitize the caldav url and put %40 instead of @
|
||||
login = [login stringByReplacingOccurrencesOfString: @"%40" withString: @"@"];
|
||||
/* When SOGoCarddavSingleAddressBookProfile is on, login may carry a
|
||||
<user>!<book> alias suffix; userWithLogin: strips it internally, but we
|
||||
keep the alias as the folder name so generated hrefs preserve the !book
|
||||
discriminator. */
|
||||
user = [SOGoUser userWithLogin: login roles: nil];
|
||||
}
|
||||
if (user)
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
|
||||
#import <SOGo/NSObject+DAV.h>
|
||||
#import <SOGo/SOGoUser.h>
|
||||
#import <SOGo/SOGoUserFolder.h>
|
||||
#import <SOGo/SOGoUserManager.h>
|
||||
#import <SOGo/SOGoSystemDefaults.h>
|
||||
#import <SOGo/WORequest+SOGo.h>
|
||||
@@ -299,8 +300,27 @@ Class SOGoContactSourceFolderK;
|
||||
- (NSArray *) toManyRelationshipKeys
|
||||
{
|
||||
NSMutableArray *keys;
|
||||
SOGoSystemDefaults *sd;
|
||||
|
||||
if ([[context request] isMacOSXAddressBookApp])
|
||||
sd = [SOGoSystemDefaults sharedSystemDefaults];
|
||||
|
||||
/* Per-addressbook CardDAV profile: when enabled and the parent user folder
|
||||
is reached through a /dav/<user>!<book>/ alias URL, expose only that
|
||||
book. The legacy macOS "personal only" shortcut below is also gated on
|
||||
the flag so the two mechanisms don't overlap. */
|
||||
if ([sd carddavSingleAddressBookProfile])
|
||||
{
|
||||
NSString *parentName;
|
||||
NSRange separator;
|
||||
parentName = [[self lookupUserFolder] nameInContainer];
|
||||
separator = [parentName rangeOfString: @"!"];
|
||||
if (separator.location != NSNotFound)
|
||||
return [NSArray arrayWithObject:
|
||||
[parentName substringFromIndex: separator.location + 1]];
|
||||
}
|
||||
|
||||
if ([[context request] isMacOSXAddressBookApp]
|
||||
&& ![sd carddavSingleAddressBookProfile])
|
||||
keys = [NSMutableArray arrayWithObject: @"personal"];
|
||||
else
|
||||
keys = (NSMutableArray *) [super toManyRelationshipKeys];
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
- (BOOL) checkLogin: (NSString *) _login
|
||||
password: (NSString *) _pwd
|
||||
{
|
||||
NSString *domain;
|
||||
NSString *domain, *checkedLogin;
|
||||
SOGoSystemDefaults *sd;
|
||||
SOGoCASSession *session;
|
||||
SOGoPasswordPolicyError perr;
|
||||
@@ -58,9 +58,21 @@
|
||||
|
||||
domain = nil;
|
||||
perr = PolicyNoError;
|
||||
sd = [SOGoSystemDefaults sharedSystemDefaults];
|
||||
checkedLogin = [_login stringByReplacingString: @"%40" withString: @"@"];
|
||||
|
||||
/* Per-addressbook CardDAV profile: a login of the form <user>!<book> is an
|
||||
alias used to discriminate the URL path; the password check applies to
|
||||
the underlying real user. */
|
||||
if ([sd carddavSingleAddressBookProfile])
|
||||
{
|
||||
NSRange aliasRange = [checkedLogin rangeOfString: @"!"];
|
||||
if (aliasRange.location != NSNotFound)
|
||||
checkedLogin = [checkedLogin substringToIndex: aliasRange.location];
|
||||
}
|
||||
|
||||
rc = ([[SOGoUserManager sharedUserManager]
|
||||
checkLogin: [_login stringByReplacingString: @"%40"
|
||||
withString: @"@"]
|
||||
checkLogin: checkedLogin
|
||||
password: _pwd
|
||||
domain: &domain
|
||||
perr: &perr
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import "SOGoMobileProvision.h"
|
||||
#import "SOGoSystemDefaults.h"
|
||||
#import "SOGoUser.h"
|
||||
#import "NSString+Crypto.h"
|
||||
|
||||
@@ -34,11 +35,12 @@
|
||||
NSURL *serverURL;
|
||||
NSDictionary *provisioning;
|
||||
NSError *error;
|
||||
NSString *payloadType, *prefix, *type;
|
||||
NSString *payloadType, *prefix, *type, *username;
|
||||
NSNumber *port;
|
||||
|
||||
activeUser = [context activeUser];
|
||||
serverURL = [context serverURL];
|
||||
username = [activeUser login];
|
||||
|
||||
if (!context || !path) {
|
||||
[self errorWithFormat: @"Invalid provisioning profile - no parameters"];
|
||||
@@ -55,6 +57,32 @@
|
||||
payloadType = @"com.apple.carddav.account";
|
||||
prefix = @"CardDAV";
|
||||
type = @"Contact";
|
||||
|
||||
/* Per-addressbook CardDAV profile: rewrite the principal URL and the
|
||||
CardDAV username from <user> to <user>!<book>. macOS Contacts
|
||||
strips path-only discriminators when it falls back on its own
|
||||
discovery; embedding the alias in the username keeps the !book
|
||||
tag in every subsequent request, both in the URL and in the Basic
|
||||
Auth header. */
|
||||
if ([[SOGoSystemDefaults sharedSystemDefaults] carddavSingleAddressBookProfile])
|
||||
{
|
||||
NSArray *parts;
|
||||
parts = [path componentsSeparatedByString: @"/"];
|
||||
if ([parts count] >= 7
|
||||
&& [[parts objectAtIndex: 4] isEqualToString: @"Contacts"])
|
||||
{
|
||||
NSString *aliasLogin;
|
||||
NSMutableArray *rewritten;
|
||||
aliasLogin = [NSString stringWithFormat: @"%@!%@",
|
||||
[parts objectAtIndex: 3],
|
||||
[parts objectAtIndex: 5]];
|
||||
rewritten = [parts mutableCopy];
|
||||
[rewritten replaceObjectAtIndex: 3 withObject: aliasLogin];
|
||||
path = [rewritten componentsJoinedByString: @"/"];
|
||||
[rewritten release];
|
||||
username = aliasLogin;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -74,7 +102,7 @@
|
||||
port, [NSString stringWithFormat:@"%@%@", prefix, @"Port"],
|
||||
path, [NSString stringWithFormat:@"%@%@", prefix, @"PrincipalURL"],
|
||||
[NSNumber numberWithBool:[[serverURL scheme] isEqualToString:@"https"]], [NSString stringWithFormat:@"%@%@", prefix, @"UseSSL"],
|
||||
[activeUser login], [NSString stringWithFormat:@"%@%@", prefix, @"Username"],
|
||||
username, [NSString stringWithFormat:@"%@%@", prefix, @"Username"],
|
||||
[NSString stringWithFormat: @"SOGo %@ provisioning", prefix], @"PayloadDescription",
|
||||
[NSString stringWithFormat:@"%@ %@", type, name], @"PayloadDisplayName",
|
||||
[NSString stringWithFormat:@"%@.%@.apple.%@", [serverURL host], [name asMD5String], [prefix lowercaseString]], @"PayloadIdentifier",
|
||||
|
||||
@@ -1257,7 +1257,10 @@
|
||||
davCurrentUserPrincipal = nil;
|
||||
else
|
||||
{
|
||||
s = [NSString stringWithFormat: @"/SOGo/dav/%@", login];
|
||||
/* loginAlias preserves the <user>!<book> alias used by the
|
||||
per-addressbook CardDAV profile, so that macOS Contacts keeps
|
||||
using the alias URL instead of falling back on the canonical one. */
|
||||
s = [NSString stringWithFormat: @"/SOGo/dav/%@", [activeUser loginAlias]];
|
||||
userHREF = davElementWithContent (@"href", XMLNS_WEBDAV, s);
|
||||
davCurrentUserPrincipal
|
||||
= [davElementWithContent (@"current-user-principal",
|
||||
|
||||
@@ -62,6 +62,7 @@ static const NSString *kDisableSharingCalendar = @"Calendar";
|
||||
- (BOOL) isCalendarDAVAccessEnabled;
|
||||
- (BOOL) isCalendarJitsiLinkEnabled;
|
||||
- (BOOL) isAddressBookDAVAccessEnabled;
|
||||
- (BOOL) carddavSingleAddressBookProfile;
|
||||
|
||||
- (BOOL) enableEMailAlarms;
|
||||
|
||||
|
||||
@@ -588,6 +588,11 @@ _injectConfigurationFromFile (NSMutableDictionary *defaultsDict,
|
||||
return [self boolForKey: @"SOGoAddressBookDAVAccessEnabled"];
|
||||
}
|
||||
|
||||
- (BOOL) carddavSingleAddressBookProfile
|
||||
{
|
||||
return [self boolForKey: @"SOGoCarddavSingleAddressBookProfile"];
|
||||
}
|
||||
|
||||
- (BOOL) enableEMailAlarms
|
||||
{
|
||||
return [self boolForKey: @"SOGoEnableEMailAlarms"];
|
||||
|
||||
@@ -63,6 +63,10 @@
|
||||
SOGoUserFolder *homeFolder;
|
||||
NSString *currentPassword;
|
||||
NSString *loginInDomain;
|
||||
/* The login as it appeared at lookup time, before any alias stripping
|
||||
(per-addressbook CardDAV profile uses <user>!<book> aliases). Equal
|
||||
to [self login] for regular users. */
|
||||
NSString *loginAlias;
|
||||
//NSString *language;
|
||||
NSArray *allEmails;
|
||||
NSMutableArray *mailAccounts;
|
||||
@@ -88,6 +92,7 @@
|
||||
- (NSString *) currentPassword;
|
||||
|
||||
- (NSString *) loginInDomain;
|
||||
- (NSString *) loginAlias;
|
||||
|
||||
/* properties */
|
||||
- (NSString *) domain;
|
||||
|
||||
@@ -144,11 +144,26 @@
|
||||
realUID = nil;
|
||||
domain = nil;
|
||||
|
||||
ASSIGN (loginAlias, newLogin);
|
||||
|
||||
if ([newLogin isEqualToString: @"anonymous"] || [newLogin isEqualToString: @"freebusy"])
|
||||
realUID = newLogin;
|
||||
else
|
||||
{
|
||||
sd = [SOGoSystemDefaults sharedSystemDefaults];
|
||||
|
||||
/* Per-addressbook CardDAV profile: strip the alias suffix so that a login
|
||||
of the form <user>!<book> resolves to the real user <user>. The book
|
||||
part is consumed elsewhere (Main/SOGo.m, SOGoContactFolders) to filter
|
||||
the addressbook listing. The original alias is kept in loginAlias so
|
||||
that DAV property hrefs (current-user-principal, etc.) preserve it. */
|
||||
if ([sd carddavSingleAddressBookProfile])
|
||||
{
|
||||
NSRange aliasRange = [newLogin rangeOfString: @"!"];
|
||||
if (aliasRange.location != NSNotFound)
|
||||
newLogin = [newLogin substringToIndex: aliasRange.location];
|
||||
}
|
||||
|
||||
if ([sd enableDomainBasedUID] || [[sd loginDomains] count] > 0)
|
||||
{
|
||||
r = [newLogin rangeOfString: @"@" options: NSBackwardsSearch];
|
||||
@@ -243,9 +258,15 @@
|
||||
[currentPassword release];
|
||||
[cn release];
|
||||
[loginInDomain release];
|
||||
[loginAlias release];
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
- (NSString *) loginAlias
|
||||
{
|
||||
return [loginAlias length] ? loginAlias : self->login;
|
||||
}
|
||||
|
||||
- (void) setPrimaryRoles: (NSArray *) newRoles
|
||||
{
|
||||
ASSIGN (roles, newRoles);
|
||||
|
||||
@@ -44,6 +44,8 @@
|
||||
@class SOGoContactFolders;
|
||||
|
||||
@interface SOGoUserFolder : SOGoFolder
|
||||
{
|
||||
}
|
||||
|
||||
/* ownership */
|
||||
|
||||
|
||||
@@ -102,11 +102,14 @@
|
||||
ownerUser = [SOGoUser userWithLogin: nameInContainer roles: nil];
|
||||
login = [ownerUser login];
|
||||
[self setOwner: login];
|
||||
if (![nameInContainer isEqualToString: login])
|
||||
if (![nameInContainer isEqualToString: login]
|
||||
&& [nameInContainer rangeOfString: @"!"].location == NSNotFound)
|
||||
// In case the user domain is specified in the URL but not in the user
|
||||
// login name, we remove it (user@domain => user).
|
||||
// This happens when SOGoLoginDomains is defined but
|
||||
// SOGoEnableDomainBasedUID is disabled.
|
||||
// The !book alias used by the per-addressbook CardDAV profile must be
|
||||
// preserved here so generated hrefs keep the discriminator.
|
||||
ASSIGN(nameInContainer, login);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user