|
IntroductionMost developers who create applications for Mac OS X only consider authorization issues when the system's privileges model prevents them from performing their work. Using Authorization Services to implement this sort of application (called a system-restricted application in the Authorization Services documentation) is well understood, and there is a sample program, BetterAuthorizationSample, that demonstrate this clearly. However, Authorization Services is designed for more than just this. When you write a Mac OS X application, even if it has no system-restricted functionality, you should consider whether you can take advantage of Authorization Services. Specifically, you should think about whether a system administrator might want to restrict some features of your application to some subset of users. Consider the following examples:
It's important to stress that the system privileges model does not prevent these sorts of activity. For example, as far as the CD driver is concerned, any user is allowed to burn discs. However, you can use Authorization Services to add an authorization layer to your application, which is then known as a self-restricted application. The ability to create a self-restricted application has always been part of Authorization Services. However, prior to Mac OS X version 10.3, there was no way to do this and maintain the simple out-of-box experience (that is, no password dialogs) expected by those users who administer their own system. Mac OS X version 10.3 introduced new Authorization Services routines that give you the best of both worlds.
The next eight sections explain how to achieve this ideal solution on Mac OS X version 10.3 and later. The last section, Supporting Earlier Systems, explains how to achieve a similar solution on earlier versions of Mac OS X. Supporting Mac OS X version 10.3 and LaterThe following sections explain how you can use Authorization Services on Mac OS X version 10.3 and later to implement a self-restricted application which never displays a password dialog unless the system administrator configures it to do so. The first step is to define your custom authorization right. Next you must modify your application to acquire that right. To understand why this sometimes brings up an authorization dialog, you must understand the authorization policy database. Once you understand the database you can modify it to include a right specification for your custom right, either manually or programmatically. Finally, you can learn about two common gotchas when using this technique, namely, how to support a localizable prompt for your custom right and how to work around a bug in Authorization Services. Define Your Authorization RightThe first step is to work out which user-level operations a system administrator might want to restrict and define a right name for each of these authorized operations. Right names form a hierarchical namespace using reverse DNS notation. In this example I'll use the right name shown in Listing 1, which lists the company name first Listing 1: Defining a right name. const char kXYZRightName[] = "com.apple.dts.SelfRestrictedSample.xyz"; Typically you define one right per authorized operation. For example, a CD burning application might define Acquiring the RightThe next step is to modify your the code so that it acquires the right before doing the operation. An example of this is shown in Listing 2. The basic idea is to call Listing 2: Obtaining the right. extern OSStatus AcquireRight(const char *rightName) // This routine calls Authorization Services to acquire // the specified right. { OSStatus err; static const AuthorizationFlags kFlags = kAuthorizationFlagInteractionAllowed | kAuthorizationFlagExtendRights; AuthorizationItem kActionRight = { rightName, 0, 0, 0 }; AuthorizationRights kRights = { 1, &kActionRight }; assert(gAuthorization != NULL); // Request the application-specific right. err = AuthorizationCopyRights( gAuthorization, // authorization &kRights, // rights NULL, // environment kFlags, // flags NULL // authorizedRights ); return err; } Note: In Listing 2, A Tour of the Authorization Policy DatabaseIf you modify your code as described above and then test it thoroughly, you'll find that under some circumstances it brings up a dialog asking you to enter an administrator user name and password. One easy way to reproduce this is to create a non-administrator user, log in as that user, then run your application and do the authorized operation. If you're shipping a consumer application, this authentication dialog is probably not what you want. However, before you try to prevent the dialog being displayed, you need to understand why it's displayed. Authorization on Mac OS X is controlled by a policy database. In current versions of Mac OS X, this database is held in WARNING: The location and format of the policy database is subject to change. Do not code any of this knowledge into an application that you expect to be binary compatible with future releases of Mac OS X. In fact, the format of the policy database has changed with Mac OS X version 10.3. This section uses the new format, although it should be relatively easy for to you to map the concepts described here on to the old format. To understand why Authorization Services brings up dialog when you ask for a custom right, you have to understand a little about how the policy database works. Listing 3 shows an extract from the policy database as installed by Mac OS X version 10.3. Listing 3: A policy database extract. <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC [...]> <plist version="1.0"> <dict> [...] <key>rights</key> <dict> <key>system.device.dvd.setregion.initial</key> <dict> <key>class</key> <string>user</string> <key>comment</key> <string>Used by the dvd player to set the regioncode the first time. Note that changed the region code after it has been set requires a different right (system.device.dvd.setregion.change) Credentials remain valid indefinitely after they've been obtained. An acquired credential is shared amongst all clients.</string> <key>group</key> <string>admin</string> <key>mechanisms</key> <array> <string>builtin:authenticate</string> </array> <key>shared</key> <true/> </dict> [...] <key>config.add.</key> <dict> <key>class</key> <string>allow</string> <key>comment</key> <string>wildcard right for adding rights. Anyone is allowed to add any (non-wildcard) rights</string> </dict> [...] <key></key> <dict> <key>class</key> <string>rule</string> <key>comment</key> <string>All other rights will be matched by this rule. Credentials remain valid 5 minutes after they've been obtained. An acquired credential is shared amongst all clients. </string> <key>rule</key> <string>default</string> </dict> [...] </dict> <key>rules</key> <dict> [...] </dict> </dict> </plist> The database consists of two dictionaries,
When your program asks for a right, Authorization Services executes the following algorithm.
Once it has found the appropriate right specification, Authorization Services evaluates the specification to decide whether to grant the right. In some cases this is easy (in the example in Listing 3 The presence of the default right has implications for self-restricted programs that use custom rights. Consider the code in Listing 1 and Listing 2. The requested right, Note: You may notice that the code Listing 2 does not always bring up a password dialog. This is because of the way authorization credentials are shared. See DTS QA 1277 Security Credentials for more information about this. Modifying the Policy DatabaseThe obvious way to avoid this password dialog is to add a new right specification to the policy database that specifically allows anyone to acquire your custom right. For a system administrator this is easy. They can simply open the file with a text editor (or indeed Property List Editor), make the appropriate changes, and then save the changes back. For example, to add a right specification that allows anyone to acquire the right from Listing 1, you can simply add the text in Listing 4 to the policy database. Listing 4: A 'always allow' right specification for the right from Listing 1. [...] <key>com.apple.dts.SelfRestrictedSample.xyz</key> <dict> <key>rule</key> <string>allow</string> </dict> [...] Note: The policy database file is only writable by root. You can edit it using a Terminal-based text editor run via sudo, or with a GUI text editor that prompts you for an administrator password when you attempt to save privileged files (such as modern versions of BBEdit). This approach is appropriate if you expect your customers to have a skilled system administrator handy (for example, if your application is sold for use in university laboratories). However, it's not appropriate for programs sold to the majority of Mac users, who do not have a dedicated system administrator. Furthermore, programmatically modifying the policy database file directly is not an option because, as mentioned earlier, the location and format of the database is subject to change. The solution to this conundrum is the new Authorization database API added in Mac OS X version 10.3. Adding the Right ProgrammaticallyThe code in Listing 5 shows how you can add a right specification to the policy database using the Authorization Services routines released with Mac OS X version 10.3. The code is fairly straightforward. It first calls Listing 5: Adding a right programmatically. static OSStatus SetupRight( AuthorizationRef authRef, const char * rightName, CFStringRef rightRule, CFStringRef rightPrompt ) // Checks whether a right exists in the authorization database // and, if not, creates the right and sets up its initial value. { OSStatus err; // Check whether our right is already defined. err = AuthorizationRightGet(rightName, NULL); if (err == noErr) { // A right already exists, either set up in advance by // the system administrator or because this is the second // time we've run. Either way, there's nothing more for // us to do. } else if (err == errAuthorizationDenied) { // The right is not already defined. Let's create a // right definition based on the rule specified by the // caller (in the rightRule parameter). This might be // kAuthorizationRuleClassAllow (which allows anyone to // acquire the right) or // kAuthorizationRuleAuthenticateAsAdmin (which requires // the user to authenticate as an admin user) // or some other value from "AuthorizationDB.h". The // system administrator can modify this right as they // see fit. err = AuthorizationRightSet( authRef, // authRef rightName, // rightName rightRule, // rightDefinition rightPrompt, // descriptionKey NULL, // bundle, NULL indicates main NULL // localeTableName, ); // NULL indicates // "Localizable.strings" // The ability to add a right is, itself, governed by a non-NULLdescriptionKey // right. If we can't get that right, we'll get an error // from the above routine. We don't want that error // stopping the application from launching, so we // swallow the error. if (err != noErr) { #if ! defined(NDEBUG) fprintf( stderr, "Could not create default right (%ld)\n", err ); #endif err = noErr; } } return err; } extern OSStatus SetupAuthorization(void) // Called as the application starts up. Creates a connection // to Authorization Services and then makes sure that our // right (kActionRightName) is defined. { OSStatus err; // Connect to Authorization Services. err = AuthorizationCreate(NULL, NULL, 0, &gAuthorization); // Set up our rights. if (err == noErr) { err = SetupRight( gAuthorization, kAlphaRightName, CFSTR(kAuthorizationRuleClassAllow), CFSTR("YOU MUST BE AUTHORIZED TO DO XYZ") ); } [...] return err; } This sequence ensures that the right specification is added to the policy database so the system administrator, if any, can then look through the policy database to examine the right specification created by your application, and set its value appropriately. Listing 6 shows how that right specification might look. Listing 6: The right specification added by the code from Listing 5. [...] <key>com.apple.dts.SelfRestrictedSample.xyz</key> <dict> <key>default-prompt</key> <dict> <key></key> <string>YOU MUST BE AUTHORIZED TO DO XYZ</string> </dict> <key>rule</key> <string>allow</string> </dict> [...]
LocalizationWhen you call
The The When you call For example, if your application has no localizations the code in Listing 5 generates the right specification in Listing 6. However, if you have two localizations, English (en) and Australian English (en_AU), the code in Listing 5 will generate the right specification shown in Listing 7. Listing 7: The localized right specification added by the code from Listing 5. [...] <key>com.apple.dts.SelfRestrictedSample.xyz</key> <dict> <key>default-prompt</key> <dict> <key></key> <string>YOU MUST BE AUTHORIZED TO DO XYZ</string> <key>en</key> <string>You must be authorized to do xyz.</string> <key>en_AU</key> <string>Strewth! You must be authorised to do xyz.</string> </dict> <key>rule</key> <string>allow</string> </dict> [...] With this right specification you will see that, when prompted to authorize, a user who prefers English over Australian English will see the message "You must be authorized to do xyz" while a user who prefers Australian English over English will see "Strewth! You must be authorised to do xyz". For Australians it's very important that every sentence contains an expletive (and that "authorised" be spelt with an 's'). IMPORTANT: For the right specification shown in Listing 7 the authorization dialog will never appear because the rule always allows the right to be granted. You can see the authorization dialog by editing the authorization policy database and changing Note: You can tell the system that you prefer Australian English over English by reordering the items in the Language panel of the International panel of System Preferences. You must first enable Australian English in the dialog displayed by clicking the "Edit List" button. When choosing a prompt it's important that you use an entire sentence. The string displayed in the authorization dialog is composed of a number of distinct sentences, and your prompt string must fit into that model. Finally, you should be aware that, currently, Authorization Services does not support localization synonyms (r. 3430642). Thus, the folder for the localization in your bundle must be "en.lproj", not "English.lproj". Bundle Reference Count ProblemsThere is one final gotcha associated with This bug is fixed in Mac OS X 10.4 and later. However, you can still work around the bug by artificially incrementing the reference count of the bundle. Listing 8 shows the recommended code for this workaround. Listing 8: Recommended workaround for reference count bug. static OSStatus AuthorizationRightSetWithWorkaround( AuthorizationRef authRef, const char * rightName, CFTypeRef rightDefinition, CFStringRef descriptionKey, CFBundleRef bundle, CFStringRef localeTableName ) // The AuthorizationRightSet routine has a bug where it // releases the bundle parameter that you pass in (or the // main bundle if you pass NULL). If you do pass NULL and // call AuthorizationRightSet multiple times, eventually the // main bundle's reference count will hit zero and you crash. // // This routine works around the bug by doing an extra retain // on the bundle. It should also work correctly when the bug // is fixed. // // Note that this technique is not thread safe, so it's // probably a good idea to restrict your use of it to // application startup time, where the threading environment // is very simple. { OSStatus err; CFBundleRef clientBundle; CFIndex originalRetainCount; // Get the effective bundle. if (bundle == NULL) { clientBundle = CFBundleGetMainBundle(); } else { clientBundle = bundle; } assert(clientBundle != NULL); // Remember the original retain count and retain it. We force // a retain because if the retain count was 1 and the bug still // exists, the next call might decrement the count to 0, which // would free the object. originalRetainCount = CFGetRetainCount(clientBundle); CFRetain(clientBundle); // Call through to Authorization Services. err = AuthorizationRightSet( authRef, rightName, rightDefinition, descriptionKey, clientBundle, localeTableName ); // If the retain count is now magically back to its original // value, we've encountered the bug and we print a message. // Otherwise the bug must've been fixed and we just balance // our retain with a release. if ( CFGetRetainCount(clientBundle) == originalRetainCount ) { fprintf( stderr, "AuthForAll: Working around <rdar://problems/3446163>\n" ); } else { CFRelease(clientBundle); } return err; } Supporting Earlier SystemsThe Authorization Services routines described in the previous sections do not help if your application needs to run on earlier versions of Mac OS X. There are two strategies that you can use if you need compatibility with earlier systems.
SummaryAuthorization Services allows you to create a self-restricted application, wherein a system administrator can control which users are allowed to access which features. With Mac OS X version 10.3 you can implement this without compromising the out-of-box experience for your typical customers. It's easy to adopt Authorization Services and doing so will make the system administrators of the world very happy. ReferencesDocument Revision History
Posted: 2008-01-30 |
|