Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1286)

Unified Diff: chrome/browser/cocoa/keystone_glue.mm

Issue 437053: In-application Keystone ticket promotion (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src/
Patch Set: '' Created 11 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « chrome/browser/cocoa/keystone_glue.h ('k') | chrome/browser/cocoa/keystone_glue_unittest.mm » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: chrome/browser/cocoa/keystone_glue.mm
===================================================================
--- chrome/browser/cocoa/keystone_glue.mm (revision 32993)
+++ chrome/browser/cocoa/keystone_glue.mm (working copy)
@@ -2,19 +2,41 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-#import "chrome/app/keystone_glue.h"
+#import "chrome/browser/cocoa/keystone_glue.h"
+#include <sys/param.h>
+#include <sys/mount.h>
+
+#include <vector>
+
+#import "app/l10n_util_mac.h"
#include "base/logging.h"
#include "base/mac_util.h"
#import "base/worker_pool_mac.h"
+#include "chrome/browser/cocoa/authorization_util.h"
#include "chrome/common/chrome_constants.h"
+#include "grit/chromium_strings.h"
+#include "grit/generated_resources.h"
namespace {
// Provide declarations of the Keystone registration bits needed here. From
// KSRegistration.h.
-typedef enum { kKSPathExistenceChecker } KSExistenceCheckerType;
+typedef enum {
+ kKSPathExistenceChecker,
+} KSExistenceCheckerType;
+typedef enum {
+ kKSRegistrationUserTicket,
+ kKSRegistrationSystemTicket,
+ kKSRegistrationDontKnowWhatKindOfTicket,
+} KSRegistrationTicketType;
+
+NSString *KSRegistrationDidCompleteNotification =
+ @"KSRegistrationDidCompleteNotification";
+NSString *KSRegistrationPromotionDidCompleteNotification =
+ @"KSRegistrationPromotionDidCompleteNotification";
+
NSString *KSRegistrationCheckForUpdateNotification =
@"KSRegistrationCheckForUpdateNotification";
NSString *KSRegistrationStatusKey = @"Status";
@@ -42,14 +64,26 @@
preserveTTToken:(BOOL)preserveToken
tag:(NSString*)tag;
+- (BOOL)promoteWithVersion:(NSString*)version
+ existenceCheckerType:(KSExistenceCheckerType)xctype
+ existenceCheckerString:(NSString*)xc
+ serverURLString:(NSString*)serverURLString
+ preserveTTToken:(BOOL)preserveToken
+ tag:(NSString*)tag
+ authorization:(AuthorizationRef)authorization;
+
- (void)setActive;
- (void)checkForUpdate;
- (void)startUpdate;
+- (KSRegistrationTicketType)ticketType;
@end // @interface KSRegistration
@interface KeystoneGlue(Private)
+// Called when Keystone registration completes.
+- (void)registrationComplete:(NSNotification*)notification;
+
// Called periodically to announce activity by pinging the Keystone server.
- (void)markActive:(NSTimer*)timer;
@@ -73,18 +107,37 @@
//
// -determineUpdateStatusAsync is called on the main thread to initiate the
// operation. It performs initial set-up work that must be done on the main
-// thread and arranges for -determineUpdateStatusAtPath: to be called on a
-// work queue thread managed by NSOperationQueue.
-// -determineUpdateStatusAtPath: then reads the Info.plist, gets the version
-// from the CFBundleShortVersionString key, and performs
+// thread and arranges for -determineUpdateStatus to be called on a work queue
+// thread managed by NSOperationQueue.
+// -determineUpdateStatus then reads the Info.plist, gets the version from the
+// CFBundleShortVersionString key, and performs
// -determineUpdateStatusForVersion: on the main thread.
// -determineUpdateStatusForVersion: does the actual comparison of the version
// on disk with the running version and calls -updateStatus:version: with the
// results of its analysis.
- (void)determineUpdateStatusAsync;
-- (void)determineUpdateStatusAtPath:(NSString*)appPath;
+- (void)determineUpdateStatus;
- (void)determineUpdateStatusForVersion:(NSString*)version;
+// Returns YES if registration_ is definitely on a user ticket. If definitely
+// on a system ticket, or uncertain of ticket type (due to an older version
+// of Keystone being used), returns NO.
+- (BOOL)isUserTicket;
+
+// Called when ticket promotion completes.
+- (void)promotionComplete:(NSNotification*)notification;
+
+// Changes the application's ownership and permissions so that all files are
+// owned by root:wheel and all files and directories are writable only by
+// root, but readable and executable as needed by everyone.
+// -changePermissionsForPromotionAsync is called on the main thread by
+// -promotionComplete. That routine calls
+// -changePermissionsForPromotionWithTool: on a work queue thread. When done,
+// -changePermissionsForPromotionComplete is called on the main thread.
+- (void)changePermissionsForPromotionAsync;
+- (void)changePermissionsForPromotionWithTool:(NSString*)toolPath;
+- (void)changePermissionsForPromotionComplete;
+
@end // @interface KeystoneGlue(Private)
const NSString* const kAutoupdateStatusNotification =
@@ -94,11 +147,14 @@
@implementation KeystoneGlue
-// TODO(jrg): use base::SingletonObjC<KeystoneGlue>
-static KeystoneGlue* sDefaultKeystoneGlue = nil; // leaked
++ (id)defaultKeystoneGlue {
+ static bool sTriedCreatingDefaultKeystoneGlue = false;
+ // TODO(jrg): use base::SingletonObjC<KeystoneGlue>
+ static KeystoneGlue* sDefaultKeystoneGlue = nil; // leaked
-+ (id)defaultKeystoneGlue {
- if (!sDefaultKeystoneGlue) {
+ if (!sTriedCreatingDefaultKeystoneGlue) {
+ sTriedCreatingDefaultKeystoneGlue = true;
+
sDefaultKeystoneGlue = [[KeystoneGlue alloc] init];
[sDefaultKeystoneGlue loadParameters];
if (![sDefaultKeystoneGlue loadKeystoneRegistration]) {
@@ -114,6 +170,16 @@
NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
[center addObserver:self
+ selector:@selector(registrationComplete:)
+ name:KSRegistrationDidCompleteNotification
+ object:nil];
+
+ [center addObserver:self
+ selector:@selector(promotionComplete:)
+ name:KSRegistrationPromotionDidCompleteNotification
+ object:nil];
+
+ [center addObserver:self
selector:@selector(checkForUpdateComplete:)
name:KSRegistrationCheckForUpdateNotification
object:nil];
@@ -128,8 +194,9 @@
}
- (void)dealloc {
+ [productID_ release];
+ [appPath_ release];
[url_ release];
- [productID_ release];
[version_ release];
[channel_ release];
[registration_ release];
@@ -144,15 +211,24 @@
- (void)loadParameters {
NSDictionary* infoDictionary = [self infoDictionary];
+
+ // Use [NSBundle mainBundle] to get the application's own bundle identifier
+ // and path, not the framework's. For auto-update, the application is
+ // what's significant here: it's used to locate the outermost part of the
+ // application for the existence checker and other operations that need to
+ // see the entire application bundle.
+ NSBundle* appBundle = [NSBundle mainBundle];
+
+ NSString* productID = [infoDictionary objectForKey:@"KSProductID"];
+ if (productID == nil) {
+ productID = [appBundle bundleIdentifier];
+ }
+
+ NSString* appPath = [appBundle bundlePath];
NSString* url = [infoDictionary objectForKey:@"KSUpdateURL"];
- NSString* product = [infoDictionary objectForKey:@"KSProductID"];
- if (product == nil) {
- // Use [NSBundle mainBundle] to fall back to the app's own bundle
- // identifier, not the app framework's.
- product = [[NSBundle mainBundle] bundleIdentifier];
- }
NSString* version = [infoDictionary objectForKey:@"KSVersion"];
- if (!product || !url || !version) {
+
+ if (!productID || !appPath || !url || !version) {
// If parameters required for Keystone are missing, don't use it.
return;
}
@@ -163,14 +239,15 @@
if (channel == nil)
channel = KSRegistrationRemoveExistingTag;
+ productID_ = [productID retain];
+ appPath_ = [appPath retain];
url_ = [url retain];
- productID_ = [product retain];
version_ = [version retain];
channel_ = [channel retain];
}
- (BOOL)loadKeystoneRegistration {
- if (!productID_ || !url_ || !version_)
+ if (!productID_ || !appPath_ || !url_ || !version_)
return NO;
// Load the KeystoneRegistration framework bundle if present. It lives
@@ -192,16 +269,21 @@
}
- (void)registerWithKeystone {
- // The existence checks should use the path to the app bundle, not the
- // app framework bundle, so use [NSBundle mainBundle] instead of
- // mac_util::MainBundle().
- [registration_ registerWithVersion:version_
- existenceCheckerType:kKSPathExistenceChecker
- existenceCheckerString:[[NSBundle mainBundle] bundlePath]
- serverURLString:url_
- preserveTTToken:YES
- tag:channel_];
+ [self updateStatus:kAutoupdateRegistering version:nil];
+ if (![registration_ registerWithVersion:version_
+ existenceCheckerType:kKSPathExistenceChecker
+ existenceCheckerString:appPath_
+ serverURLString:url_
+ preserveTTToken:YES
+ tag:channel_]) {
+ [self updateStatus:kAutoupdateRegisterFailed version:nil];
+ return;
+ }
+
+ // Upon completion, KSRegistrationDidCompleteNotification will be posted,
+ // and -registrationComplete: will be called.
+
// Mark an active RIGHT NOW; don't wait an hour for the first one.
[registration_ setActive];
@@ -213,6 +295,16 @@
repeats:YES];
}
+- (void)registrationComplete:(NSNotification*)notification {
+ NSDictionary* userInfo = [notification userInfo];
+ if ([[userInfo objectForKey:KSRegistrationStatusKey] boolValue]) {
+ [self updateStatus:kAutoupdateRegistered version:nil];
+ } else {
+ // Dump registration_?
+ [self updateStatus:kAutoupdateRegisterFailed version:nil];
+ }
+}
+
- (void)stopTimer {
[timer_ invalidate];
}
@@ -290,28 +382,24 @@
// Runs on the main thread.
- (void)determineUpdateStatusAsync {
- // NSBundle is not documented as being thread-safe. Do NSBundle operations
- // on the main thread before jumping over to a NSOperationQueue-managed
- // thread to do blocking file input.
DCHECK([NSThread isMainThread]);
- SEL selector = @selector(determineUpdateStatusAtPath:);
- NSString* appPath = [[NSBundle mainBundle] bundlePath];
+ SEL selector = @selector(determineUpdateStatus);
NSInvocationOperation* operation =
[[[NSInvocationOperation alloc] initWithTarget:self
selector:selector
- object:appPath] autorelease];
+ object:nil] autorelease];
NSOperationQueue* operationQueue = [WorkerPoolObjC sharedOperationQueue];
[operationQueue addOperation:operation];
}
// Runs on a thread managed by NSOperationQueue.
-- (void)determineUpdateStatusAtPath:(NSString*)appPath {
+- (void)determineUpdateStatus {
DCHECK(![NSThread isMainThread]);
NSString* appInfoPlistPath =
- [[appPath stringByAppendingPathComponent:@"Contents"]
+ [[appPath_ stringByAppendingPathComponent:@"Contents"]
stringByAppendingPathComponent:@"Info.plist"];
NSDictionary* infoPlist =
[NSDictionary dictionaryWithContentsOfFile:appInfoPlistPath];
@@ -383,7 +471,258 @@
- (BOOL)asyncOperationPending {
AutoupdateStatus status = [self recentStatus];
- return status == kAutoupdateChecking || status == kAutoupdateInstalling;
+ return status == kAutoupdateRegistering ||
+ status == kAutoupdateChecking ||
+ status == kAutoupdateInstalling ||
+ status == kAutoupdatePromoting;
}
+- (BOOL)isUserTicket {
+ return [registration_ ticketType] == kKSRegistrationUserTicket;
+}
+
+- (BOOL)isOnReadOnlyFilesystem {
+ const char* appPathC = [appPath_ fileSystemRepresentation];
+ struct statfs statfsBuf;
+
+ if (statfs(appPathC, &statfsBuf) != 0) {
+ PLOG(ERROR) << "statfs";
+ // Be optimistic about the filesystem's writability.
+ return NO;
+ }
+
+ return (statfsBuf.f_flags & MNT_RDONLY) != 0;
+}
+
+- (BOOL)needsPromotion {
+ if (![self isUserTicket] || [self isOnReadOnlyFilesystem]) {
+ return NO;
+ }
+
+ // Check the outermost bundle directory, the main executable path, and the
+ // framework directory. It may be enough to just look at the outermost
+ // bundle directory, but checking an interior file and directory can be
+ // helpful in case permissions are set differently only on the outermost
+ // directory. An interior file and directory are both checked because some
+ // file operations, such as Snow Leopard's Finder's copy operation when
+ // authenticating, may actually result in different ownership being applied
+ // to files and directories.
+ NSFileManager* fileManager = [NSFileManager defaultManager];
+ NSString* executablePath = [[NSBundle mainBundle] executablePath];
+ NSString* frameworkPath = [mac_util::MainAppBundle() bundlePath];
+ return ![fileManager isWritableFileAtPath:appPath_] ||
+ ![fileManager isWritableFileAtPath:executablePath] ||
+ ![fileManager isWritableFileAtPath:frameworkPath];
+}
+
+- (BOOL)wantsPromotion {
+ // -needsPromotion checks these too, but this method doesn't necessarily
+ // return NO just becuase -needsPromotion returns NO, so another check is
+ // needed here.
+ if (![self isUserTicket] || [self isOnReadOnlyFilesystem]) {
+ return NO;
+ }
+
+ if ([self needsPromotion]) {
+ return YES;
+ }
+
+ return [appPath_ hasPrefix:@"/Applications/"];
+}
+
+- (void)promoteTicket {
+ if ([self asyncOperationPending] || ![self wantsPromotion]) {
+ // Because there are multiple ways of reaching promoteTicket that might
+ // not lock each other out, it may be possible to arrive here while an
+ // asynchronous operation is pending, or even after promotion has already
+ // occurred. Just quietly return without doing anything.
+ return;
+ }
+
+ // Create an empty AuthorizationRef.
+ scoped_AuthorizationRef authorization;
+ OSStatus status = AuthorizationCreate(NULL,
+ kAuthorizationEmptyEnvironment,
+ kAuthorizationFlagDefaults,
+ &authorization);
+ if (status != errAuthorizationSuccess) {
+ LOG(ERROR) << "AuthorizationCreate: " << status;
+ return;
+ }
+
+ // Specify the "system.privilege.admin" right, which allows
+ // AuthorizationExecuteWithPrivileges to run commands as root.
+ AuthorizationItem rightItems[] = {
+ {kAuthorizationRightExecute, 0, NULL, 0}
+ };
+ AuthorizationRights rights = {arraysize(rightItems), rightItems};
+
+ // product_logo_32.png is used instead of app.icns because Authorization
+ // Services requires an image that NSImage can read.
+ NSString* iconPath =
+ [mac_util::MainAppBundle() pathForResource:@"product_logo_32"
+ ofType:@"png"];
+ const char* iconPathC = [iconPath fileSystemRepresentation];
+ size_t iconPathLength = iconPathC ? strlen(iconPathC) : 0;
+
+ // The OS will append " Type an administrator's name and password to allow
+ // <CFBundleDisplayName> to make changes."
+ NSString* prompt = l10n_util::GetNSStringFWithFixup(
+ IDS_PROMOTE_AUTHENTICATION_PROMPT,
+ l10n_util::GetStringUTF16(IDS_PRODUCT_NAME));
+ const char* promptC = [prompt UTF8String];
+ size_t promptLength = promptC ? strlen(promptC) : 0;
+
+ AuthorizationItem environmentItems[] = {
+ {kAuthorizationEnvironmentIcon, iconPathLength, (void*)iconPathC, 0},
+ {kAuthorizationEnvironmentPrompt, promptLength, (void*)promptC, 0}
+ };
+
+ AuthorizationEnvironment environment = {arraysize(environmentItems),
+ environmentItems};
+
+ AuthorizationFlags flags = kAuthorizationFlagDefaults |
+ kAuthorizationFlagInteractionAllowed |
+ kAuthorizationFlagExtendRights |
+ kAuthorizationFlagPreAuthorize;
+
+ status = AuthorizationCopyRights(authorization,
+ &rights,
+ &environment,
+ flags,
+ NULL);
+ if (status != errAuthorizationSuccess) {
+ if (status != errAuthorizationCanceled) {
+ LOG(ERROR) << "AuthorizationCopyRights: " << status;
+ }
+ return;
+ }
+
+ [self updateStatus:kAutoupdatePromoting version:nil];
+
+ // TODO(mark): Remove when able!
+ //
+ // keystone_promote_preflight is hopefully temporary. It's here to ensure
+ // that the Keystone system ticket store is in a usable state for all users
+ // on the system. Ideally, Keystone's installer or another part of Keystone
+ // would handle this. The underlying problem is http://b/2285921, and it
+ // causes http://b/2289908, which this workaround addresses.
+ //
+ // This is run synchronously, which isn't optimal, but
+ // -[KSRegistration promoteWithVersion:...] is currently synchronous too,
+ // and this operation needs to happen before that one.
+ //
+ // TODO(mark): Make asynchronous. That only makes sense if the promotion
+ // operation itself is asynchronous too. http://b/2290009. Hopefully,
+ // the Keystone promotion code will just be changed to do what preflight
+ // now does, and then the preflight script can be removed instead.
+ NSString* preflightPath =
+ [mac_util::MainAppBundle() pathForResource:@"keystone_promote_preflight"
+ ofType:@"sh"];
+ const char* preflightPathC = [preflightPath fileSystemRepresentation];
+ const char* arguments[] = {NULL};
+
+ int exit_status;
+ status = authorization_util::ExecuteWithPrivilegesAndWait(
+ authorization,
+ preflightPathC,
+ kAuthorizationFlagDefaults,
+ arguments,
+ NULL, // pipe
+ &exit_status);
+ if (status != errAuthorizationSuccess) {
+ LOG(ERROR) << "AuthorizationExecuteWithPrivileges preflight: " << status;
+ [self updateStatus:kAutoupdatePromoteFailed version:nil];
+ return;
+ }
+ if (exit_status != 0) {
+ LOG(ERROR) << "keystone_promote_preflight status " << exit_status;
+ [self updateStatus:kAutoupdatePromoteFailed version:nil];
+ return;
+ }
+
+ // Hang on to the AuthorizationRef so that it can be used once promotion is
+ // complete. Do this before asking Keystone to promote the ticket, because
+ // -promotionComplete: may be called from inside the Keystone promotion
+ // call.
+ authorization_.swap(authorization);
+
+ if (![registration_ promoteWithVersion:version_
+ existenceCheckerType:kKSPathExistenceChecker
+ existenceCheckerString:appPath_
+ serverURLString:url_
+ preserveTTToken:YES
+ tag:channel_
+ authorization:authorization_]) {
+ [self updateStatus:kAutoupdatePromoteFailed version:nil];
+ authorization_.reset();
+ return;
+ }
+
+ // Upon completion, KSRegistrationPromotionDidCompleteNotification will be
+ // posted, and -promotionComplete: will be called.
+}
+
+- (void)promotionComplete:(NSNotification*)notification {
+ NSDictionary* userInfo = [notification userInfo];
+ if ([[userInfo objectForKey:KSRegistrationStatusKey] boolValue]) {
+ [self changePermissionsForPromotionAsync];
+ } else {
+ authorization_.reset();
+ [self updateStatus:kAutoupdatePromoteFailed version:nil];
+ }
+}
+
+- (void)changePermissionsForPromotionAsync {
+ // NSBundle is not documented as being thread-safe. Do NSBundle operations
+ // on the main thread before jumping over to a NSOperationQueue-managed
+ // thread to run the tool.
+ DCHECK([NSThread isMainThread]);
+
+ SEL selector = @selector(changePermissionsForPromotionWithTool:);
+ NSString* toolPath =
+ [mac_util::MainAppBundle() pathForResource:@"keystone_promote_postflight"
+ ofType:@"sh"];
+
+ NSInvocationOperation* operation =
+ [[[NSInvocationOperation alloc] initWithTarget:self
+ selector:selector
+ object:toolPath] autorelease];
+
+ NSOperationQueue* operationQueue = [WorkerPoolObjC sharedOperationQueue];
+ [operationQueue addOperation:operation];
+}
+
+- (void)changePermissionsForPromotionWithTool:(NSString*)toolPath {
+ const char* toolPathC = [toolPath fileSystemRepresentation];
+
+ const char* appPathC = [appPath_ fileSystemRepresentation];
+ const char* arguments[] = {appPathC, NULL};
+
+ int exit_status;
+ OSStatus status = authorization_util::ExecuteWithPrivilegesAndWait(
+ authorization_,
+ toolPathC,
+ kAuthorizationFlagDefaults,
+ arguments,
+ NULL, // pipe
+ &exit_status);
+ if (status != errAuthorizationSuccess) {
+ LOG(ERROR) << "AuthorizationExecuteWithPrivileges postflight: " << status;
+ } else if (exit_status != 0) {
+ LOG(ERROR) << "keystone_promote_postflight status " << exit_status;
+ }
+
+ SEL selector = @selector(changePermissionsForPromotionComplete);
+ [self performSelectorOnMainThread:selector
+ withObject:nil
+ waitUntilDone:NO];
+}
+
+- (void)changePermissionsForPromotionComplete {
+ authorization_.reset();
+
+ [self updateStatus:kAutoupdatePromoted version:nil];
+}
+
@end // @implementation KeystoneGlue
« no previous file with comments | « chrome/browser/cocoa/keystone_glue.h ('k') | chrome/browser/cocoa/keystone_glue_unittest.mm » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698