|
IntroductionIn some circumstances it is necessary to run code as the user logs in, and to guarantee that the code runs to completion before the login can continue. For example, if you're developing an administrative product that wants to reset the user's preferences to a particular state, you have to do that after the home directory is mounted (otherwise, your product wouldn't work for FileVault users or for users with network home directories) but before any applications have had a chance to run and read those preferences. This technote describes the recommended approach for running code that's coordinated with the login process. It starts with an overview of the techniques that have been used for this in the past, but which are fundamentally flawed (Close But No Cigar). It then describes the recommended technique, namely, creating an authorization plug-in (Do The Right Thing). Finally, it concludes with a discussion of some of the gotchas associated with this technique (Danger Will Robinson). IMPORTANT: Formal support for authorization plug-ins was added in Mac OS X 10.4. If you need to do this sort of thing on earlier systems, please contact Developer Technical Support. IMPORTANT: This technote only covers GUI logins; it does not cover other types of logins (for example, SSH logins or AppleShare logins). Note: This technote only discusses login; it does not cover issues related to logout. Specifically, while the authorization plug-in technique described in this technote is a good alternative for the login hook, there is currently no equivalent replacement for the logout hook (r. 4905756) . It's important to understand when an authorization plug-in is not appropriate. Two common cases are:
Close But No CigarHistorically developers have used a number of techniques for running code at login time, and most of them are inadequate for various reasons. This section describes those techniques and their problems. Login HookThe login hook is documented in System Startup Programming Topics. It has a number of drawbacks and is generally considered to be deprecated. Specifically:
DaemonVarious developers have tried to do this sort of thing from a daemon (a launchd daemon or a startup item). There are a number of problems with using a daemon for this sort of thing.
See Technical Note TN2083, 'Daemons and Agents' for more information about daemons. AgentVarious developers have tried to use an agent (a launchd agent, a login item, or global login item) to do this sort of thing. This technique is unsuitable for a number of reasons.
See Technical Note TN2083, 'Daemons and Agents' for more information about agents. Do The Right ThingThe recommended approach for running code that's coordinated with the login process is to create an authorization plug-in. IMPORTANT: To understand the rest of this section you will need a basic understanding of Authorization Services. If you've not used Authorization Services before, I suggest you read the Authorization Concepts chapter of Performing Privileged Operations With Authorization Services. When the user logs in, <key>system.login.console</key> <dict> <key>class</key> <string>evaluate-mechanisms</string> <key>comment</key> <string>Login mechanism based rule. Not for general use, yet.</string> <key>mechanisms</key> <array> <string>builtin:smartcard-sniffer,privileged</string> <string>loginwindow:login</string> <string>builtin:reset-password,privileged</string> <string>builtin:auto-login,privileged</string> <string>builtin:authenticate,privileged</string> <string>HomeDirMechanism:login,privileged</string> <string>HomeDirMechanism:status</string> <string>MCXMechanism:login</string> <string>loginwindow:success</string> <string>loginwindow:done</string> </array> </dict> WARNING: The location of the authorization database is subject to change. To ensure long-term binary compatibility you must access the database using the authorization database API. Note: The exact definition of the When It's possible to write an authorization plug-in that implements a custom mechanism, and then add the name of that mechanism to this array and have it called at login time. This happens for both manual and automatic logins. The position of your mechanism in the array determines when it will be run. The default right specification for
WARNING: Regardless of the position of your mechanism in this array, your code will run in an unusual environment, with numerous gotchas. Danger Will Robinson describes these gotchas in more detail. Getting StartedThe best place to start when developing an authorization plug-in is Sample Code 'NullAuthPlugin'. Grab the sample and start customizing! The remainder of this section describes some of the issues that you might encounter while creating your plug-in. Always Mount A Scratch MonkeyWARNING: If your authorization plug-in is not found, or the mechanism it implements fails, you will not be able to log in to the system via the GUI. The easiest way to avoid being bitten by this is to enable SSH logins on your machine (Remote Login in the Sharing panel of System Preferences). That way, if anything goes wrong, you can log in via SSH to debug and correct it. If you run into problems and you haven't enabled SSH (or you don't have access to another machine to use as the SSH client), you can fix things by booting the machine in single user mode. Regardless, I strongly recommend that you make a backup of your authorization database before doing any work with authorization plug-ins. Listing 2 shows how to do this on current systems. $ # Backup the authorization database $ sudo cp /etc/authorization /etc/authorization-orig $ # Restore from the backup $ sudo cp /etc/authorization-orig /etc/authorization Fast User SwitchingIn many cases you do not need to restart the machine to test your authorization plug-in. Instead, you can enable fast user switching (in the Account panel of System Preferences) and switch directly to the login window. From there, log in as a different user. Your authorization plug-in's mechanisms will be invoked in much the same way as they would be at startup time. DebuggingThe mechanics of debugging an authorization plug-in can be challenging. For example:
You can also add copious logging to your plug-in and debug it by trolling through the logs. Sample Code 'NullAuthPlugin' shows how to do that using syslog; a more modern example would use ASL. For an example of how to use ASL, see Sample Code 'SampleD'. Installing Your Plug-inOn Mac OS X 10.5 and later, third party authorization plug-ins should be installed in Once you have installed your plug-in, you must activate it by modifying the authorization database. During development you can do this by editing the authorization database file (currently Listing 3 shows an example of how to do this. The top-level routine, #include <assert.h> #include <CoreServices/CoreServices.h> #include <Security/Security.h> static void InsertMechanismRelativeToHomeDirMechanism( CFMutableArrayRef mechanisms, CFStringRef mechanismStr, Boolean beforeHomeDirMechanism ) // Adds the mechanism specified mechanismStr to the mechanisms array. // If beforeHomeDirMechanism is true, mechanismStr is added immediately // before the first instance of "HomeDirMechanism"; otherwise it is added // after the last instance. { CFIndex mechanismCount; CFIndex mechanismIndex; CFIndex insertionIndex; Boolean isHomeDirMechanism; CFStringRef mechanism; assert(mechanisms != NULL); assert(mechanismStr != NULL); mechanismCount = CFArrayGetCount(mechanisms); insertionIndex = mechanismCount; // add after last entry by default for (mechanismIndex = 0; mechanismIndex < mechanismCount; mechanismIndex++) { mechanism = CFArrayGetValueAtIndex(mechanisms, mechanismIndex); isHomeDirMechanism = ( (mechanism != NULL) && (CFGetTypeID(mechanism) == CFStringGetTypeID()) && CFStringHasPrefix(mechanism, CFSTR("HomeDirMechanism:")) ); if (isHomeDirMechanism) { if (beforeHomeDirMechanism) { insertionIndex = mechanismIndex; break; } else { insertionIndex = mechanismIndex + 1; } } } CFArrayInsertValueAtIndex(mechanisms, insertionIndex, mechanismStr); } static OSStatus AddMechanismToConsoleLoginRight( AuthorizationRef authRef, CFStringRef authPluginName, CFStringRef mechanismID, Boolean privileged, Boolean beforeHomeDirMechanism ) // Adds the mechanism specified authPluginName and mechanismID to the // "mechanisms" array of the "system.login.console" right definition. // If privileged is true, the mechanism runs as root. If // beforeHomeDirMechanism is true, mechanismStr is added immediately // before the first instance of "HomeDirMechanism"; otherwise it is added // after the last instance. { OSStatus err; CFStringRef mechanismStr; CFDictionaryRef rightDict; CFStringRef authClass; CFArrayRef authMechanisms; CFMutableArrayRef newMechanisms; CFMutableDictionaryRef newRightDict; static const char * kConsoleLoginRightName = "system.login.console"; assert(authRef != NULL); assert(authPluginName != NULL); assert(mechanismID != NULL); mechanismStr = NULL; rightDict = NULL; newRightDict = NULL; newMechanisms = NULL; // Construct a correctly formatted mechanism string. err = noErr; mechanismStr = CFStringCreateWithFormat( NULL, NULL, CFSTR("%@:%@%@"), authPluginName, mechanismID, (privileged ? CFSTR(",privileged") : CFSTR("")) ); if (mechanismStr == NULL) { err = coreFoundationUnknownErr; } // Get the right definition and check the class is "evaluate-mechanisms". if (err == noErr) { err = AuthorizationRightGet(kConsoleLoginRightName, &rightDict); } if (err == noErr) { authClass = (CFStringRef) CFDictionaryGetValue(rightDict, CFSTR("class")); if ( (authClass == NULL) || (CFGetTypeID(authClass) != CFStringGetTypeID()) ) { err = coreFoundationUnknownErr; } else if ( ! CFEqual(authClass, CFSTR("evaluate-mechanisms")) ) { err = errAuthorizationInternal; } } // Get the mechanisms array and check whether our mechanism is already present. if (err == noErr) { authMechanisms = (CFArrayRef) CFDictionaryGetValue( rightDict, CFSTR("mechanisms") ); if ( (authMechanisms == NULL) || (CFGetTypeID(authMechanisms) != CFArrayGetTypeID()) ) { err = coreFoundationUnknownErr; } } if ( (err == noErr) && ! CFArrayContainsValue( authMechanisms, CFRangeMake(0, CFArrayGetCount(authMechanisms)), mechanismStr ) ) { // If it's not, add our mechanism and write back the right definition. newMechanisms = CFArrayCreateMutableCopy(NULL, 0, authMechanisms); if (newMechanisms == NULL) { err = coreFoundationUnknownErr; } if (err == noErr) { InsertMechanismRelativeToHomeDirMechanism( newMechanisms, mechanismStr, beforeHomeDirMechanism ); newRightDict = CFDictionaryCreateMutableCopy(NULL, 0, rightDict); if (newRightDict == NULL) { err = coreFoundationUnknownErr; } if (err == noErr) { CFDictionarySetValue( newRightDict, CFSTR("mechanisms"), newMechanisms ); err = AuthorizationRightSet( authRef, kConsoleLoginRightName, newRightDict, NULL, NULL, NULL ); } } } // Clean up. if (newRightDict != NULL) { CFRelease(newRightDict); } if (newMechanisms != NULL) { CFRelease(newMechanisms); } if (rightDict != NULL) { CFRelease(rightDict); } if (mechanismStr != NULL) { CFRelease(mechanismStr); } return err; } You do not need to restart the system for it to recognize the new authorization plug-in or your changes to the authorization database. Context IssuesAuthorization mechanisms run in a very unusual context, the specific values of which depend on whether you install your mechanisms as privileged or not. Table 1 is a brief summary of the context inherited by each type of authorization mechanism. Note: For more information about the context issues covered by this table, see Technical Note TN2083, 'Daemons and Agents'. Table 1 : Authorization mechanism context
Notes:
In many cases it may be necessary for your plug-in to implement a pair of cooperating authorization mechanisms, one that runs privileged and one that runs non-privileged. For example, if you're writing an authorization plug-in that completely resets the user's home directory on login, you may need two mechanisms:
This is because a privileged mechanism can't display UI and a non-privileged mechanism runs as UID 92, and thus can't modify the user's home directory. If you need to create two or more authorization mechanisms, you can use authorization auxiliary information to communicate between them, as described in the next section. Authorization Auxiliary InformationAuthorization Services maintains two dictionaries of auxiliary information that it passes from one plug-in to the next. These are the context and the hints discussed in detail in the Authorization Plug-in Reference. There are two common uses for this auxiliary information:
As an example of point 2, if you're developing an authorization plug-in that resets the user's preferences, you will need to know the user ID of the user who is logging in (so you can switch to that user when writing to the file system) and the path to their home directory. You can get these from the "uid" and "home" context values, respectively. Listing 4 shows how you can get the "uid" value from the authorization context. static uid_t GetUIDFromContext( const AuthorizationCallbacks * authServerFuncs, AuthorizationEngineRef engine ) // Returns the "uid" value from the authorization context, or // -2 (nobody) if the value is not present or can't be fetched. // authServerFuncs is the value passed to your plug-in's // AuthorizationPluginCreate routine. engine is the value // passed to your MechanismCreate routine. { OSStatus err; uid_t result; AuthorizationContextFlags junkFlags; const AuthorizationValue * value; assert(authServerFuncs != NULL); assert(engine != NULL); result = (uid_t) -2; err = authServerFuncs->GetContextValue(engine, "uid", &junkFlags, &value); if ( (err == noErr) && (value->length == sizeof(uid_t)) ) { result = * (const uid_t *) value->data; } else { // [... log the failure ...] } return result; } Danger Will RobinsonThe following sections describe a number of non-obvious pitfalls associated with creating an authorization plug-in. Allowing Home Directory AccessIf you're writing an authorization mechanism that wants to access the user's home directory, you must consider file system permissions. Specifically, non-privileged authorization mechanisms run as effective user ID (EUID) 92 ( On the other hand privileged authorization mechanisms run as EUID 0 ( You can get the user ID of the user logging in from the authorization context (see Authorization Auxiliary Information). You can change the EUID using Preventing Home Directory AccessIf you're writing an authorization mechanism that runs before The framework that causes most problems in this respect is Core Foundation. Core Foundation tries to access the user's home directory to determine their default text encoding (stored in the file IMPORTANT: Setting To maximize your chance of ongoing binary compatibility, you should try to limit yourself to low-level frameworks. Specifically, frameworks that are daemon-safe should be more-or-less safe in the context. On the other hand frameworks that are not daemon-safe are almost certainly going to cause problems. See Technical Note TN2083, 'Daemons and Agents' for a list of daemon-safe frameworks. Mach Bootstrap Namespace IssuesYour authorization mechanism inherits a very unusual Mach bootstrap namespace. The namespace is ultimately destined to become the GUI per-session bootstrap namespace for the user logging in. However, while the login is in progress, the availability of Mach-based services can vary in non-obvious ways. WARNING: Your authorization plug-in must not register services in its bootstrap namespace, nor hold a reference to the namespace that persists beyond the execution of your authorization mechanisms. Both of these activities will yield unspecified results. Note: For detailed information about Mach bootstrap namespaces, see Technical Note TN2083, 'Daemons and Agents'. Table 2 shows how you can access Mach services registered in specific namespaces on Mac OS X 10.5 and later. Table 2 : Accessing Mach services from an authorization mechanism on Mac OS X 10.5
Notes:
Table 3 shows the same information for Mac OS X 10.4.x. Table 3 : Accessing Mach services from an authorization mechanism on Mac OS X 10.4.x
Notes:
Be Nice To Your HostYour authorization plug-in is loaded and executed within a system process (see Context Issues for the details). You should attempt to be as nice to your host process as possible. Specifically:
One process-wide value that deserves special attention is the effective user ID (EUID). There are circumstances where your authorization plug-in should switch the EUID to that of the user logging in. In these circumstances you should avoid using #include <assert.h> #include <errno.h> #include <fcntl.h> #include <pthread.h> #include <unistd.h> #include <sys/stat.h> #include <sys/kauth.h> static int CreateFileAsUserGroup(const char *path, uid_t uid, gid_t gid) // Creates a file and returns the file descriptor. On error, returns // -1 and errno is set to an error value. { int err; int fd; int junk; fd = -1; err = pthread_setugid_np(uid, gid); if (err == 0) { fd = open(path, O_CREAT | O_EXCL | O_RDWR, S_IRWXU); err = errno; // preserve errno across the pthread_setugid_np junk = pthread_setugid_np(KAUTH_UID_NONE, KAUTH_GID_NONE); assert(junk == 0); errno = err; } return fd; } Note: While Host Death IssuesThe system process that hosts your authorization plug-in will typically terminate at the end of the login process, which means that your authorization plug-in will not continue to execute during the login session. If you wish to maintain a presence during the login session, you should use some other mechanism (typically a GUI launchd agent or a global login item). WARNING: Do not attempt to maintain a presence during the login session by forking from within your authorization plug-in. The resulting process will inherit an unusual execution context which is likely to lead to long-term binary compatibility problems. For more information about launchd agents and login items, see Technical Note TN2083, 'Daemons and Agents' Further Reading
Document Revision History
Posted: 2008-09-16 |
|