1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #import "chrome/browser/mac/keystone_glue.h"
13 #include "base/bind.h"
14 #include "base/location.h"
15 #include "base/logging.h"
16 #include "base/mac/authorization_util.h"
17 #include "base/mac/bundle_locations.h"
18 #include "base/mac/mac_logging.h"
19 #include "base/mac/mac_util.h"
20 #include "base/mac/scoped_nsautorelease_pool.h"
21 #include "base/mac/scoped_nsexception_enabler.h"
22 #include "base/memory/ref_counted.h"
23 #include "base/strings/sys_string_conversions.h"
24 #include "base/threading/worker_pool.h"
25 #import "chrome/browser/mac/keystone_registration.h"
26 #include "chrome/common/chrome_constants.h"
27 #include "chrome/common/chrome_version_info.h"
28 #include "grit/chromium_strings.h"
29 #include "grit/generated_resources.h"
30 #include "ui/base/l10n/l10n_util.h"
31 #include "ui/base/l10n/l10n_util_mac.h"
35 namespace ksr = keystone_registration;
37 // Constants for the brand file (uses an external file so it can survive
38 // updates to Chrome.)
40 #if defined(GOOGLE_CHROME_BUILD)
41 #define kBrandFileName @"Google Chrome Brand.plist";
42 #elif defined(CHROMIUM_BUILD)
43 #define kBrandFileName @"Chromium Brand.plist";
45 #error Unknown branding
48 // These directories are hardcoded in Keystone promotion preflight and the
49 // Keystone install script, so NSSearchPathForDirectoriesInDomains isn't used
50 // since the scripts couldn't use anything like that.
51 NSString* kBrandUserFile = @"~/Library/Google/" kBrandFileName;
52 NSString* kBrandSystemFile = @"/Library/Google/" kBrandFileName;
54 NSString* UserBrandFilePath() {
55 return [kBrandUserFile stringByStandardizingPath];
57 NSString* SystemBrandFilePath() {
58 return [kBrandSystemFile stringByStandardizingPath];
61 // Adaptor for scheduling an Objective-C method call on a |WorkerPool|
63 class PerformBridge : public base::RefCountedThreadSafe<PerformBridge> {
66 // Call |sel| on |target| with |arg| in a WorkerPool thread.
67 // |target| and |arg| are retained, |arg| may be |nil|.
68 static void PostPerform(id target, SEL sel, id arg) {
72 scoped_refptr<PerformBridge> op = new PerformBridge(target, sel, arg);
73 base::WorkerPool::PostTask(
74 FROM_HERE, base::Bind(&PerformBridge::Run, op.get()), true);
77 // Convenience for the no-argument case.
78 static void PostPerform(id target, SEL sel) {
79 PostPerform(target, sel, nil);
83 // Allow RefCountedThreadSafe<> to delete.
84 friend class base::RefCountedThreadSafe<PerformBridge>;
86 PerformBridge(id target, SEL sel, id arg)
87 : target_([target retain]),
94 // Happens on a WorkerPool thread.
96 base::mac::ScopedNSAutoreleasePool pool;
97 [target_ performSelector:sel_ withObject:arg_];
100 base::scoped_nsobject<id> target_;
102 base::scoped_nsobject<id> arg_;
107 @interface KeystoneGlue (Private)
109 // Returns the path to the application's Info.plist file. This returns the
110 // outer application bundle's Info.plist, not the framework's Info.plist.
111 - (NSString*)appInfoPlistPath;
113 // Returns a dictionary containing parameters to be used for a KSRegistration
114 // -registerWithParameters: or -promoteWithParameters:authorization: call.
115 - (NSDictionary*)keystoneParameters;
117 // Called when Keystone registration completes.
118 - (void)registrationComplete:(NSNotification*)notification;
120 // Called periodically to announce activity by pinging the Keystone server.
121 - (void)markActive:(NSTimer*)timer;
123 // Called when an update check or update installation is complete. Posts the
124 // kAutoupdateStatusNotification notification to the default notification
126 - (void)updateStatus:(AutoupdateStatus)status version:(NSString*)version;
128 // Returns the version of the currently-installed application on disk.
129 - (NSString*)currentlyInstalledVersion;
131 // These three methods are used to determine the version of the application
132 // currently installed on disk, compare that to the currently-running version,
133 // decide whether any updates have been installed, and call
134 // -updateStatus:version:.
136 // In order to check the version on disk, the installed application's
137 // Info.plist dictionary must be read; in order to see changes as updates are
138 // applied, the dictionary must be read each time, bypassing any caches such
139 // as the one that NSBundle might be maintaining. Reading files can be a
140 // blocking operation, and blocking operations are to be avoided on the main
141 // thread. I'm not quite sure what jank means, but I bet that a blocked main
142 // thread would cause some of it.
144 // -determineUpdateStatusAsync is called on the main thread to initiate the
145 // operation. It performs initial set-up work that must be done on the main
146 // thread and arranges for -determineUpdateStatus to be called on a work queue
147 // thread managed by WorkerPool.
148 // -determineUpdateStatus then reads the Info.plist, gets the version from the
149 // CFBundleShortVersionString key, and performs
150 // -determineUpdateStatusForVersion: on the main thread.
151 // -determineUpdateStatusForVersion: does the actual comparison of the version
152 // on disk with the running version and calls -updateStatus:version: with the
153 // results of its analysis.
154 - (void)determineUpdateStatusAsync;
155 - (void)determineUpdateStatus;
156 - (void)determineUpdateStatusForVersion:(NSString*)version;
158 // Returns YES if registration_ is definitely on a user ticket. If definitely
159 // on a system ticket, or uncertain of ticket type (due to an older version
160 // of Keystone being used), returns NO.
161 - (BOOL)isUserTicket;
163 // Returns YES if Keystone is definitely installed at the system level,
164 // determined by the presence of an executable ksadmin program at the expected
166 - (BOOL)isSystemKeystone;
168 // Returns YES if on a system ticket but system Keystone is not present.
169 // Returns NO otherwise. The "doomed" condition will result in the
170 // registration framework appearing to have registered Chrome, but no updates
171 // ever actually taking place.
172 - (BOOL)isSystemTicketDoomed;
174 // Called when ticket promotion completes.
175 - (void)promotionComplete:(NSNotification*)notification;
177 // Changes the application's ownership and permissions so that all files are
178 // owned by root:wheel and all files and directories are writable only by
179 // root, but readable and executable as needed by everyone.
180 // -changePermissionsForPromotionAsync is called on the main thread by
181 // -promotionComplete. That routine calls
182 // -changePermissionsForPromotionWithTool: on a work queue thread. When done,
183 // -changePermissionsForPromotionComplete is called on the main thread.
184 - (void)changePermissionsForPromotionAsync;
185 - (void)changePermissionsForPromotionWithTool:(NSString*)toolPath;
186 - (void)changePermissionsForPromotionComplete;
188 // Returns the brand file path to use for Keystone.
189 - (NSString*)brandFilePath;
191 @end // @interface KeystoneGlue (Private)
193 NSString* const kAutoupdateStatusNotification = @"AutoupdateStatusNotification";
194 NSString* const kAutoupdateStatusStatus = @"status";
195 NSString* const kAutoupdateStatusVersion = @"version";
199 NSString* const kChannelKey = @"KSChannelID";
200 NSString* const kBrandKey = @"KSBrandID";
201 NSString* const kVersionKey = @"KSVersion";
205 @implementation KeystoneGlue
207 + (id)defaultKeystoneGlue {
208 static bool sTriedCreatingDefaultKeystoneGlue = false;
209 // TODO(jrg): use base::SingletonObjC<KeystoneGlue>
210 static KeystoneGlue* sDefaultKeystoneGlue = nil; // leaked
212 if (!sTriedCreatingDefaultKeystoneGlue) {
213 sTriedCreatingDefaultKeystoneGlue = true;
215 sDefaultKeystoneGlue = [[KeystoneGlue alloc] init];
216 [sDefaultKeystoneGlue loadParameters];
217 if (![sDefaultKeystoneGlue loadKeystoneRegistration]) {
218 [sDefaultKeystoneGlue release];
219 sDefaultKeystoneGlue = nil;
222 return sDefaultKeystoneGlue;
226 if ((self = [super init])) {
227 NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
229 [center addObserver:self
230 selector:@selector(registrationComplete:)
231 name:ksr::KSRegistrationDidCompleteNotification
234 [center addObserver:self
235 selector:@selector(promotionComplete:)
236 name:ksr::KSRegistrationPromotionDidCompleteNotification
239 [center addObserver:self
240 selector:@selector(checkForUpdateComplete:)
241 name:ksr::KSRegistrationCheckForUpdateNotification
244 [center addObserver:self
245 selector:@selector(installUpdateComplete:)
246 name:ksr::KSRegistrationStartUpdateNotification
254 [productID_ release];
259 [registration_ release];
260 [[NSNotificationCenter defaultCenter] removeObserver:self];
264 - (NSDictionary*)infoDictionary {
265 // Use base::mac::OuterBundle() to get the Chrome app's own bundle identifier
266 // and path, not the framework's. For auto-update, the application is
267 // what's significant here: it's used to locate the outermost part of the
268 // application for the existence checker and other operations that need to
269 // see the entire application bundle.
270 return [base::mac::OuterBundle() infoDictionary];
273 - (void)loadParameters {
274 NSBundle* appBundle = base::mac::OuterBundle();
275 NSDictionary* infoDictionary = [self infoDictionary];
277 NSString* productID = [infoDictionary objectForKey:@"KSProductID"];
278 if (productID == nil) {
279 productID = [appBundle bundleIdentifier];
282 NSString* appPath = [appBundle bundlePath];
283 NSString* url = [infoDictionary objectForKey:@"KSUpdateURL"];
284 NSString* version = [infoDictionary objectForKey:kVersionKey];
286 if (!productID || !appPath || !url || !version) {
287 // If parameters required for Keystone are missing, don't use it.
291 NSString* channel = [infoDictionary objectForKey:kChannelKey];
292 // The stable channel has no tag. If updating to stable, remove the
293 // dev and beta tags since we've been "promoted".
295 channel = ksr::KSRegistrationRemoveExistingTag;
297 productID_ = [productID retain];
298 appPath_ = [appPath retain];
300 version_ = [version retain];
301 channel_ = [channel retain];
304 - (NSString*)brandFilePath {
305 DCHECK(version_ != nil) << "-loadParameters must be called first";
307 if (brandFileType_ == kBrandFileTypeNotDetermined) {
309 NSFileManager* fm = [NSFileManager defaultManager];
310 NSString* userBrandFile = UserBrandFilePath();
311 NSString* systemBrandFile = SystemBrandFilePath();
314 brandFileType_ = kBrandFileTypeNone;
316 // Only the stable channel has a brand code.
317 chrome::VersionInfo::Channel channel = chrome::VersionInfo::GetChannel();
319 if (channel == chrome::VersionInfo::CHANNEL_DEV ||
320 channel == chrome::VersionInfo::CHANNEL_BETA) {
322 // If on the dev or beta channel, this installation may have replaced
323 // an older system-level installation. Check for a user brand file and
324 // nuke it if present. Don't try to remove the system brand file, there
325 // wouldn't be any permission to do so.
327 // Don't do this on the canary channel. The canary can run side-by-side
328 // with another Google Chrome installation whose brand code, if any,
329 // should remain intact.
331 if ([fm fileExistsAtPath:userBrandFile]) {
332 [fm removeItemAtPath:userBrandFile error:NULL];
335 } else if (channel == chrome::VersionInfo::CHANNEL_STABLE) {
337 // If there is a system brand file, use it.
338 if ([fm fileExistsAtPath:systemBrandFile]) {
341 // Use the system file that is there.
342 brandFileType_ = kBrandFileTypeSystem;
344 // Clean up any old user level file.
345 if ([fm fileExistsAtPath:userBrandFile]) {
346 [fm removeItemAtPath:userBrandFile error:NULL];
352 NSDictionary* infoDictionary = [self infoDictionary];
353 NSString* appBundleBrandID = [infoDictionary objectForKey:kBrandKey];
355 NSString* storedBrandID = nil;
356 if ([fm fileExistsAtPath:userBrandFile]) {
357 NSDictionary* storedBrandDict =
358 [NSDictionary dictionaryWithContentsOfFile:userBrandFile];
359 storedBrandID = [storedBrandDict objectForKey:kBrandKey];
362 if ((appBundleBrandID != nil) &&
363 (![storedBrandID isEqualTo:appBundleBrandID])) {
364 // App and store don't match, update store and use it.
365 NSDictionary* storedBrandDict =
366 [NSDictionary dictionaryWithObject:appBundleBrandID
368 // If Keystone hasn't been installed yet, the location the brand file
369 // is written to won't exist, so manually create the directory.
370 NSString *userBrandFileDirectory =
371 [userBrandFile stringByDeletingLastPathComponent];
372 if (![fm fileExistsAtPath:userBrandFileDirectory]) {
373 if (![fm createDirectoryAtPath:userBrandFileDirectory
374 withIntermediateDirectories:YES
377 LOG(ERROR) << "Failed to create the directory for the brand file";
380 if ([storedBrandDict writeToFile:userBrandFile atomically:YES]) {
381 brandFileType_ = kBrandFileTypeUser;
383 } else if (storedBrandID) {
384 // Had stored brand, use it.
385 brandFileType_ = kBrandFileTypeUser;
392 NSString* result = nil;
393 switch (brandFileType_) {
394 case kBrandFileTypeUser:
395 result = UserBrandFilePath();
398 case kBrandFileTypeSystem:
399 result = SystemBrandFilePath();
402 case kBrandFileTypeNotDetermined:
405 case kBrandFileTypeNone:
414 - (BOOL)loadKeystoneRegistration {
415 if (!productID_ || !appPath_ || !url_ || !version_)
418 // Load the KeystoneRegistration framework bundle if present. It lives
419 // inside the framework, so use base::mac::FrameworkBundle();
421 [[base::mac::FrameworkBundle() privateFrameworksPath]
422 stringByAppendingPathComponent:@"KeystoneRegistration.framework"];
423 NSBundle* ksrBundle = [NSBundle bundleWithPath:ksrPath];
426 // Harness the KSRegistration class.
427 Class ksrClass = [ksrBundle classNamed:@"KSRegistration"];
428 KSRegistration* ksr = [ksrClass registrationWithProductID:productID_];
432 registration_ = [ksr retain];
436 - (NSString*)appInfoPlistPath {
437 // NSBundle ought to have a way to access this path directly, but it
439 return [[appPath_ stringByAppendingPathComponent:@"Contents"]
440 stringByAppendingPathComponent:@"Info.plist"];
443 - (NSDictionary*)keystoneParameters {
444 NSNumber* xcType = [NSNumber numberWithInt:ksr::kKSPathExistenceChecker];
445 NSNumber* preserveTTToken = [NSNumber numberWithBool:YES];
446 NSString* appInfoPlistPath = [self appInfoPlistPath];
447 NSString* brandKey = kBrandKey;
448 NSString* brandPath = [self brandFilePath];
450 if ([brandPath length] == 0) {
451 // Brand path and brand key must be cleared together or ksadmin seems
452 // to throw an error.
456 return [NSDictionary dictionaryWithObjectsAndKeys:
457 version_, ksr::KSRegistrationVersionKey,
458 appInfoPlistPath, ksr::KSRegistrationVersionPathKey,
459 kVersionKey, ksr::KSRegistrationVersionKeyKey,
460 xcType, ksr::KSRegistrationExistenceCheckerTypeKey,
461 appPath_, ksr::KSRegistrationExistenceCheckerStringKey,
462 url_, ksr::KSRegistrationServerURLStringKey,
463 preserveTTToken, ksr::KSRegistrationPreserveTrustedTesterTokenKey,
464 channel_, ksr::KSRegistrationTagKey,
465 appInfoPlistPath, ksr::KSRegistrationTagPathKey,
466 kChannelKey, ksr::KSRegistrationTagKeyKey,
467 brandPath, ksr::KSRegistrationBrandPathKey,
468 brandKey, ksr::KSRegistrationBrandKeyKey,
472 - (void)registerWithKeystone {
473 [self updateStatus:kAutoupdateRegistering version:nil];
475 NSDictionary* parameters = [self keystoneParameters];
478 // TODO(shess): Allows Keystone to throw an exception when
479 // /usr/bin/python does not exist (really!).
480 // http://crbug.com/86221 and http://crbug.com/87931
481 base::mac::ScopedNSExceptionEnabler enabler;
482 result = [registration_ registerWithParameters:parameters];
485 [self updateStatus:kAutoupdateRegisterFailed version:nil];
489 // Upon completion, ksr::KSRegistrationDidCompleteNotification will be
490 // posted, and -registrationComplete: will be called.
492 // Mark an active RIGHT NOW; don't wait an hour for the first one.
493 [registration_ setActive];
495 // Set up hourly activity pings.
496 timer_ = [NSTimer scheduledTimerWithTimeInterval:60 * 60 // One hour
498 selector:@selector(markActive:)
499 userInfo:registration_
503 - (void)registrationComplete:(NSNotification*)notification {
504 NSDictionary* userInfo = [notification userInfo];
505 if ([[userInfo objectForKey:ksr::KSRegistrationStatusKey] boolValue]) {
506 if ([self isSystemTicketDoomed]) {
507 [self updateStatus:kAutoupdateNeedsPromotion version:nil];
509 [self updateStatus:kAutoupdateRegistered version:nil];
512 // Dump registration_?
513 [self updateStatus:kAutoupdateRegisterFailed version:nil];
521 - (void)markActive:(NSTimer*)timer {
522 KSRegistration* ksr = [timer userInfo];
526 - (void)checkForUpdate {
527 DCHECK(![self asyncOperationPending]);
529 if (!registration_) {
530 [self updateStatus:kAutoupdateCheckFailed version:nil];
534 [self updateStatus:kAutoupdateChecking version:nil];
536 [registration_ checkForUpdate];
538 // Upon completion, ksr::KSRegistrationCheckForUpdateNotification will be
539 // posted, and -checkForUpdateComplete: will be called.
542 - (void)checkForUpdateComplete:(NSNotification*)notification {
543 NSDictionary* userInfo = [notification userInfo];
545 if ([[userInfo objectForKey:ksr::KSRegistrationUpdateCheckErrorKey]
547 [self updateStatus:kAutoupdateCheckFailed version:nil];
548 } else if ([[userInfo objectForKey:ksr::KSRegistrationStatusKey] boolValue]) {
549 // If an update is known to be available, go straight to
550 // -updateStatus:version:. It doesn't matter what's currently on disk.
551 NSString* version = [userInfo objectForKey:ksr::KSRegistrationVersionKey];
552 [self updateStatus:kAutoupdateAvailable version:version];
554 // If no updates are available, check what's on disk, because an update
555 // may have already been installed. This check happens on another thread,
556 // and -updateStatus:version: will be called on the main thread when done.
557 [self determineUpdateStatusAsync];
561 - (void)installUpdate {
562 DCHECK(![self asyncOperationPending]);
564 if (!registration_) {
565 [self updateStatus:kAutoupdateInstallFailed version:nil];
569 [self updateStatus:kAutoupdateInstalling version:nil];
571 [registration_ startUpdate];
573 // Upon completion, ksr::KSRegistrationStartUpdateNotification will be
574 // posted, and -installUpdateComplete: will be called.
577 - (void)installUpdateComplete:(NSNotification*)notification {
578 NSDictionary* userInfo = [notification userInfo];
580 // http://crbug.com/160308 and b/7517358: when using system Keystone and on
581 // a user ticket, KSUpdateCheckSuccessfulKey will be NO even when an update
582 // was installed correctly, so don't check it. It should be redudnant when
583 // KSUpdateCheckSuccessfullyInstalledKey is checked.
584 if (![[userInfo objectForKey:ksr::KSUpdateCheckSuccessfullyInstalledKey]
586 [self updateStatus:kAutoupdateInstallFailed version:nil];
588 updateSuccessfullyInstalled_ = YES;
590 // Nothing in the notification dictionary reports the version that was
591 // installed. Figure it out based on what's on disk.
592 [self determineUpdateStatusAsync];
596 - (NSString*)currentlyInstalledVersion {
597 NSString* appInfoPlistPath = [self appInfoPlistPath];
598 NSDictionary* infoPlist =
599 [NSDictionary dictionaryWithContentsOfFile:appInfoPlistPath];
600 return [infoPlist objectForKey:@"CFBundleShortVersionString"];
603 // Runs on the main thread.
604 - (void)determineUpdateStatusAsync {
605 DCHECK([NSThread isMainThread]);
607 PerformBridge::PostPerform(self, @selector(determineUpdateStatus));
610 // Runs on a thread managed by WorkerPool.
611 - (void)determineUpdateStatus {
612 DCHECK(![NSThread isMainThread]);
614 NSString* version = [self currentlyInstalledVersion];
616 [self performSelectorOnMainThread:@selector(determineUpdateStatusForVersion:)
621 // Runs on the main thread.
622 - (void)determineUpdateStatusForVersion:(NSString*)version {
623 DCHECK([NSThread isMainThread]);
625 AutoupdateStatus status;
626 if (updateSuccessfullyInstalled_) {
627 // If an update was successfully installed and this object saw it happen,
628 // then don't even bother comparing versions.
629 status = kAutoupdateInstalled;
631 NSString* currentVersion =
632 [NSString stringWithUTF8String:chrome::kChromeVersion];
634 // If the version on disk could not be determined, assume that
635 // whatever's running is current.
636 version = currentVersion;
637 status = kAutoupdateCurrent;
638 } else if ([version isEqualToString:currentVersion]) {
639 status = kAutoupdateCurrent;
641 // If the version on disk doesn't match what's currently running, an
642 // update must have been applied in the background, without this app's
643 // direct participation. Leave updateSuccessfullyInstalled_ alone
644 // because there's no direct knowledge of what actually happened.
645 status = kAutoupdateInstalled;
649 [self updateStatus:status version:version];
652 - (void)updateStatus:(AutoupdateStatus)status version:(NSString*)version {
653 NSNumber* statusNumber = [NSNumber numberWithInt:status];
654 NSMutableDictionary* dictionary =
655 [NSMutableDictionary dictionaryWithObject:statusNumber
656 forKey:kAutoupdateStatusStatus];
658 [dictionary setObject:version forKey:kAutoupdateStatusVersion];
661 NSNotification* notification =
662 [NSNotification notificationWithName:kAutoupdateStatusNotification
664 userInfo:dictionary];
665 recentNotification_.reset([notification retain]);
667 [[NSNotificationCenter defaultCenter] postNotification:notification];
670 - (NSNotification*)recentNotification {
671 return [[recentNotification_ retain] autorelease];
674 - (AutoupdateStatus)recentStatus {
675 NSDictionary* dictionary = [recentNotification_ userInfo];
676 return static_cast<AutoupdateStatus>(
677 [[dictionary objectForKey:kAutoupdateStatusStatus] intValue]);
680 - (BOOL)asyncOperationPending {
681 AutoupdateStatus status = [self recentStatus];
682 return status == kAutoupdateRegistering ||
683 status == kAutoupdateChecking ||
684 status == kAutoupdateInstalling ||
685 status == kAutoupdatePromoting;
688 - (BOOL)isUserTicket {
689 return [registration_ ticketType] == ksr::kKSRegistrationUserTicket;
692 - (BOOL)isSystemKeystone {
694 if (stat("/Library/Google/GoogleSoftwareUpdate/GoogleSoftwareUpdate.bundle/"
695 "Contents/MacOS/ksadmin",
700 if (!(statbuf.st_mode & S_IXUSR)) {
707 - (BOOL)isSystemTicketDoomed {
708 BOOL isSystemTicket = ![self isUserTicket];
709 return isSystemTicket && ![self isSystemKeystone];
712 - (BOOL)isOnReadOnlyFilesystem {
713 const char* appPathC = [appPath_ fileSystemRepresentation];
714 struct statfs statfsBuf;
716 if (statfs(appPathC, &statfsBuf) != 0) {
717 PLOG(ERROR) << "statfs";
718 // Be optimistic about the filesystem's writability.
722 return (statfsBuf.f_flags & MNT_RDONLY) != 0;
725 - (BOOL)needsPromotion {
726 // Don't promote when on a read-only filesystem.
727 if ([self isOnReadOnlyFilesystem]) {
731 // Promotion is required when a system ticket is present but system Keystone
733 if ([self isSystemTicketDoomed]) {
737 // If on a system ticket and system Keystone is present, promotion is not
739 if (![self isUserTicket]) {
743 // Check the outermost bundle directory, the main executable path, and the
744 // framework directory. It may be enough to just look at the outermost
745 // bundle directory, but checking an interior file and directory can be
746 // helpful in case permissions are set differently only on the outermost
747 // directory. An interior file and directory are both checked because some
748 // file operations, such as Snow Leopard's Finder's copy operation when
749 // authenticating, may actually result in different ownership being applied
750 // to files and directories.
751 NSFileManager* fileManager = [NSFileManager defaultManager];
752 NSString* executablePath = [base::mac::OuterBundle() executablePath];
753 NSString* frameworkPath = [base::mac::FrameworkBundle() bundlePath];
754 return ![fileManager isWritableFileAtPath:appPath_] ||
755 ![fileManager isWritableFileAtPath:executablePath] ||
756 ![fileManager isWritableFileAtPath:frameworkPath];
759 - (BOOL)wantsPromotion {
760 if ([self needsPromotion]) {
764 // These are the same unpromotable cases as in -needsPromotion.
765 if ([self isOnReadOnlyFilesystem] || ![self isUserTicket]) {
769 return [appPath_ hasPrefix:@"/Applications/"];
772 - (void)promoteTicket {
773 if ([self asyncOperationPending] || ![self wantsPromotion]) {
774 // Because there are multiple ways of reaching promoteTicket that might
775 // not lock each other out, it may be possible to arrive here while an
776 // asynchronous operation is pending, or even after promotion has already
777 // occurred. Just quietly return without doing anything.
781 NSString* prompt = l10n_util::GetNSStringFWithFixup(
782 IDS_PROMOTE_AUTHENTICATION_PROMPT,
783 l10n_util::GetStringUTF16(IDS_PRODUCT_NAME));
784 base::mac::ScopedAuthorizationRef authorization(
785 base::mac::AuthorizationCreateToRunAsRoot(
786 base::mac::NSToCFCast(prompt)));
787 if (!authorization.get()) {
791 [self promoteTicketWithAuthorization:authorization.release() synchronous:NO];
794 - (void)promoteTicketWithAuthorization:(AuthorizationRef)authorization_arg
795 synchronous:(BOOL)synchronous {
796 base::mac::ScopedAuthorizationRef authorization(authorization_arg);
797 authorization_arg = NULL;
799 if ([self asyncOperationPending]) {
800 // Starting a synchronous operation while an asynchronous one is pending
804 if (!synchronous && ![self wantsPromotion]) {
805 // If operating synchronously, the call came from the installer, which
806 // means that a system ticket is required. Otherwise, only allow
807 // promotion if it's wanted.
811 synchronousPromotion_ = synchronous;
813 [self updateStatus:kAutoupdatePromoting version:nil];
815 // TODO(mark): Remove when able!
817 // keystone_promote_preflight will copy the current brand information out to
818 // the system level so all users can share the data as part of the ticket
821 // It will also ensure that the Keystone system ticket store is in a usable
822 // state for all users on the system. Ideally, Keystone's installer or
823 // another part of Keystone would handle this. The underlying problem is
824 // http://b/2285921, and it causes http://b/2289908, which this workaround
827 // This is run synchronously, which isn't optimal, but
828 // -[KSRegistration promoteWithParameters:authorization:] is currently
829 // synchronous too, and this operation needs to happen before that one.
831 // TODO(mark): Make asynchronous. That only makes sense if the promotion
832 // operation itself is asynchronous too. http://b/2290009. Hopefully,
833 // the Keystone promotion code will just be changed to do what preflight
834 // now does, and then the preflight script can be removed instead.
835 // However, preflight operation (and promotion) should only be asynchronous
836 // if the synchronous parameter is NO.
837 NSString* preflightPath =
838 [base::mac::FrameworkBundle()
839 pathForResource:@"keystone_promote_preflight"
841 const char* preflightPathC = [preflightPath fileSystemRepresentation];
842 const char* userBrandFile = NULL;
843 const char* systemBrandFile = NULL;
844 if (brandFileType_ == kBrandFileTypeUser) {
845 // Running with user level brand file, promote to the system level.
846 userBrandFile = [UserBrandFilePath() fileSystemRepresentation];
847 systemBrandFile = [SystemBrandFilePath() fileSystemRepresentation];
849 const char* arguments[] = {userBrandFile, systemBrandFile, NULL};
852 OSStatus status = base::mac::ExecuteWithPrivilegesAndWait(
855 kAuthorizationFlagDefaults,
859 if (status != errAuthorizationSuccess) {
860 OSSTATUS_LOG(ERROR, status)
861 << "AuthorizationExecuteWithPrivileges preflight";
862 [self updateStatus:kAutoupdatePromoteFailed version:nil];
865 if (exit_status != 0) {
866 LOG(ERROR) << "keystone_promote_preflight status " << exit_status;
867 [self updateStatus:kAutoupdatePromoteFailed version:nil];
871 // Hang on to the AuthorizationRef so that it can be used once promotion is
872 // complete. Do this before asking Keystone to promote the ticket, because
873 // -promotionComplete: may be called from inside the Keystone promotion
875 authorization_.swap(authorization);
877 NSDictionary* parameters = [self keystoneParameters];
879 // If the brand file is user level, update parameters to point to the new
880 // system level file during promotion.
881 if (brandFileType_ == kBrandFileTypeUser) {
882 NSMutableDictionary* temp_parameters =
883 [[parameters mutableCopy] autorelease];
884 [temp_parameters setObject:SystemBrandFilePath()
885 forKey:ksr::KSRegistrationBrandPathKey];
886 parameters = temp_parameters;
889 if (![registration_ promoteWithParameters:parameters
890 authorization:authorization_]) {
891 [self updateStatus:kAutoupdatePromoteFailed version:nil];
892 authorization_.reset();
896 // Upon completion, ksr::KSRegistrationPromotionDidCompleteNotification will
897 // be posted, and -promotionComplete: will be called.
899 // If synchronous, see to it that this happens immediately. Give it a
900 // 10-second deadline.
902 CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, false);
906 - (void)promotionComplete:(NSNotification*)notification {
907 NSDictionary* userInfo = [notification userInfo];
908 if ([[userInfo objectForKey:ksr::KSRegistrationStatusKey] boolValue]) {
909 if (synchronousPromotion_) {
910 // Short-circuit: if performing a synchronous promotion, the promotion
911 // came from the installer, which already set the permissions properly.
912 // Rather than run a duplicate permission-changing operation, jump
913 // straight to "done."
914 [self changePermissionsForPromotionComplete];
916 [self changePermissionsForPromotionAsync];
919 authorization_.reset();
920 [self updateStatus:kAutoupdatePromoteFailed version:nil];
923 if (synchronousPromotion_) {
924 // The run loop doesn't need to wait for this any longer.
925 CFRunLoopRef runLoop = CFRunLoopGetCurrent();
926 CFRunLoopStop(runLoop);
927 CFRunLoopWakeUp(runLoop);
931 - (void)changePermissionsForPromotionAsync {
932 // NSBundle is not documented as being thread-safe. Do NSBundle operations
933 // on the main thread before jumping over to a WorkerPool-managed
934 // thread to run the tool.
935 DCHECK([NSThread isMainThread]);
937 SEL selector = @selector(changePermissionsForPromotionWithTool:);
939 [base::mac::FrameworkBundle()
940 pathForResource:@"keystone_promote_postflight"
943 PerformBridge::PostPerform(self, selector, toolPath);
946 - (void)changePermissionsForPromotionWithTool:(NSString*)toolPath {
947 const char* toolPathC = [toolPath fileSystemRepresentation];
949 const char* appPathC = [appPath_ fileSystemRepresentation];
950 const char* arguments[] = {appPathC, NULL};
953 OSStatus status = base::mac::ExecuteWithPrivilegesAndWait(
956 kAuthorizationFlagDefaults,
960 if (status != errAuthorizationSuccess) {
961 OSSTATUS_LOG(ERROR, status)
962 << "AuthorizationExecuteWithPrivileges postflight";
963 } else if (exit_status != 0) {
964 LOG(ERROR) << "keystone_promote_postflight status " << exit_status;
967 SEL selector = @selector(changePermissionsForPromotionComplete);
968 [self performSelectorOnMainThread:selector
973 - (void)changePermissionsForPromotionComplete {
974 authorization_.reset();
976 [self updateStatus:kAutoupdatePromoted version:nil];
979 - (void)setAppPath:(NSString*)appPath {
980 if (appPath != appPath_) {
982 appPath_ = [appPath copy];
986 @end // @implementation KeystoneGlue
990 std::string BrandCodeInternal() {
991 KeystoneGlue* keystone_glue = [KeystoneGlue defaultKeystoneGlue];
992 NSString* brand_path = [keystone_glue brandFilePath];
994 if (![brand_path length])
995 return std::string();
998 [NSDictionary dictionaryWithContentsOfFile:brand_path];
999 NSString* brand_code =
1000 base::mac::ObjCCast<NSString>([dict objectForKey:kBrandKey]);
1002 return [brand_code UTF8String];
1004 return std::string();
1009 namespace keystone_glue {
1011 std::string BrandCode() {
1012 // |s_brand_code| is leaked.
1013 static std::string* s_brand_code = new std::string(BrandCodeInternal());
1014 return *s_brand_code;
1017 bool KeystoneEnabled() {
1018 return [KeystoneGlue defaultKeystoneGlue] != nil;
1021 string16 CurrentlyInstalledVersion() {
1022 KeystoneGlue* keystoneGlue = [KeystoneGlue defaultKeystoneGlue];
1023 NSString* version = [keystoneGlue currentlyInstalledVersion];
1024 return base::SysNSStringToUTF16(version);
1027 } // namespace keystone_glue