/* File: BetterAuthorizationSampleLib.h Contains: Interface to reusable code for privileged helper tools. Written by: DTS Copyright: Copyright (c) 2007 Apple Inc. All Rights Reserved. Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple, Inc. ("Apple") in consideration of your agreement to the following terms, and your use, installation, modification or redistribution of this Apple software constitutes acceptance of these terms. If you do not agree with these terms, please do not use, install, modify or redistribute this Apple software. In consideration of your agreement to abide by the following terms, and subject to these terms, Apple grants you a personal, non-exclusive license, under Apple's copyrights in this original Apple software (the "Apple Software"), to use, reproduce, modify and redistribute the Apple Software, with or without modifications, in source and/or binary forms; provided that if you redistribute the Apple Software in its entirety and without modifications, you must retain this notice and the following text and disclaimers in all such redistributions of the Apple Software. Neither the name, trademarks, service marks or logos of Apple, Inc. may be used to endorse or promote products derived from the Apple Software without specific prior written permission from Apple. Except as expressly stated in this notice, no other rights or licenses, express or implied, are granted by Apple herein, including but not limited to any patent rights that may be infringed by your derivative works or by other works in which the Apple Software may be incorporated. The Apple Software is provided by Apple on an "AS IS" basis. APPLE MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef _BetterAuthorizationSampleLIB_H #define _BetterAuthorizationSampleLIB_H #include #include #include #ifdef __cplusplus extern "C" { #endif ///////////////////////////////////////////////////////////////// /* This header has extensive HeaderDoc comments. To see these comments in a more felicitous form, you can generate HTML from the HeaderDoc comments using the following command: $ headerdoc2html BetterAuthorizationSampleLib.h $ open BetterAuthorizationSampleLib/index.html */ /*! @header BetterAuthorizationSampleLib @abstract Reusable library for creating helper tools that perform privileged operations on behalf of your application. @discussion BetterAuthorizationSampleLib allows you to perform privileged operations in a helper tool. In this model, your application runs with standard privileges and, when it needs to do a privileged operation, it makes a request to the helper tool. The helper tool uses Authorization Services to ensure that the user is authorized to perform that operation. BetterAuthorizationSampleLib takes care of all of the mechanics of installing the helper tool and communicating with it. Specifically, it has routines that your application can call to: 1. send requests to a helper tool (BASExecuteRequestInHelperTool) 2. install the helper tool if it's not installed, or fix an installation if it's broken (BASDiagnoseFailure and BASFixFailure) BetterAuthorizationSampleLib also helps you implement the helper tool. Specifically, you call the routine BASHelperToolMain in the main entry point for your helper tool, passing it an array of command callbacks (of type BASCommandProc). BASHelperToolMain will take care of all the details of communication with the application and only call your callback to execute the actual command. A command consists of request and response CFDictionaries (or, equivalently, NSDictionaries). BetterAuthorizationSampleLib defines three special keys for these dictionaries: 1. kBASCommandKey -- In the request dictionary, this is the name of the command. Its value is a string that uniquely identifies the command within your program. 2. kBASErrorKey -- In the response dictionary, this is the error result for the request. Its value is an OSStatus-style error code. 3. kBASDescriptorArrayKey -- In the response dictionary, if present, this is an array of file descriptors being returned from the helper tool. You can use any other key to represent addition parameters (or return values) for the command. The only constraints that BetterAuthorizationSampleLib applies to these extra parameters is that they must be serialisable as a CFPropertyList. BetterAuthorizationSampleLib requires that you tell it about the list of commands that you support. Each command is represented by a command specification (BASCommandSpec). The command specification includes the following information: 1. The name of the command. This is the same as the kBASCommandKey value in the request dictionary. 2. The authorization right associated with the command. BetterAuthorizationSampleLib uses this to ensure that the user is authorized to use the command before it calls your command callback in the privileged helper tool. 3. Information to create the command's authorization right specification in the policy database. The is used by the BASSetDefaultRules function. Finally, BetterAuthorizationSampleLib includes a number of utilities routines to help wrangle error codes (BASErrnoToOSStatus, BASOSStatusToErrno, and BASGetErrorFromResponse) and file descriptors (BASCloseDescriptorArray). */ ///////////////////////////////////////////////////////////////// #pragma mark ***** Command Description /*! @struct BASCommandSpec @abstract Describes a privileged operation to BetterAuthorizationSampleLib. @discussion Both the application and the tool must tell BetterAuthorizationSampleLib about the operations (that is, commands) that they support. They do this by passing in an array of BASCommandSpec structures. Each element describes one command. The array is terminated by a command whose commandName field is NULL. In general the application and tool should use the same array definition. However, there are cases where these might be out of sync. For example, if you have an older version of the application talking to a newer version of the tool, the tool might know about more commands than the application (and thus provide a longer array), and that's OK. @field commandName A identifier for this command. This can be any string that is unique within the context of your programs. A NULL value in this field terminates the array. The length of the command name must not be greater than 1024 UTF-16 values. @field rightName This is the name of the authorization right associated with the command. This can be NULL if you don't want any right associated with the command. If it's not NULL, BetterAuthorizationSampleLib will acquire that right before allowing the command to execute. @field rightDefaultRule This is the name of an authorization rule that should be used in the default right specification for the right. To see a full list of these rules, look at the "rules" dictionary within the policy database (currently "/etc/authorization"). Common values include "default" (which requires that the user hold credentials that authenticate them as an admin user) and "allow" (which will let anyone acquire the right). This must be NULL if (and only if) rightName is NULL. @field rightDescriptionKey This is a key used to form a custom prompt for the right. The value of this string should be a key into a .strings file whose name you supply to BASSetDefaultRules. When BetterAuthorizationSampleLib creates the right specification, it uses this key to get all of the localised prompt strings for the right. This must be NULL if rightName is NULL. Otherwise, this may be NULL if you don't want a custom prompt for your right. @field userData This field is is for the benefit of the client; BetterAuthorizationSampleLib does not use it in any way. */ struct BASCommandSpec { const char * commandName; const char * rightName; const char * rightDefaultRule; const char * rightDescriptionKey; const void * userData; }; typedef struct BASCommandSpec BASCommandSpec; ///////////////////////////////////////////////////////////////// #pragma mark ***** Request/Response Keys // Standard keys for the request dictionary /*! @define kBASCommandKey @abstract Key for the command string within the request dictionary. @discussion Within a request, this key must reference a string that is the name of the command to execute. This must match one of the commands in the BASCommandSpec array. The length of a command name must not be greater than 1024 UTF-16 values. */ #define kBASCommandKey "com.apple.dts.BetterAuthorizationSample.command" // CFString // Standard keys for the response dictionary /*! @define kBASErrorKey @abstract Key for the error result within the response dictionary. @discussion Within a response, this key must reference a number that is the error result for the response, interpreted as an OSStatus. */ #define kBASErrorKey "com.apple.dts.BetterAuthorizationSample.error" // CFNumber /*! @define kBASDescriptorArrayKey @abstract Key for a file descriptor array within the response dictionary. @discussion Within a response, this key, if present, must reference an array of numbers, which are the file descriptors being returned with the response. The numbers are interpreted as ints. */ #define kBASDescriptorArrayKey "com.apple.dts.BetterAuthorizationSample.descriptors" // CFArray of CFNumber /*! @define kBASTestString @abstract Key for a file descriptor array within the response dictionary. @discussion Within a response, this key, if present, must reference an array of numbers, which are the file descriptors being returned with the response. The numbers are interpreted as ints. */ #define kBASTestString "com.apple.dts.BetterAuthorizationSample.testString" // CFArray of CFNumber ///////////////////////////////////////////////////////////////// #pragma mark ***** Helper Tool Routines /*! @functiongroup Helper Tool Routines */ /*! @typedef BASCommandProc @abstract Command processing callback. @discussion When your helper tool calls BASHelperToolMain, it passes in a pointer to an array of callback functions of this type. When BASHelperToolMain receives a valid command, it calls one of these function so that your program-specific code can process the request. BAS guarantees that the effective, save and real user IDs (EUID, SUID, RUID) will all be zero at this point (that is, you're "running as root"). By the time this callback is called, BASHelperToolMain has already verified that this is a known command. It also acquires the authorization right associated with the command, if any. However, it does nothing to validate the other parameters in the request. These parameters come from a non-privileged source and you should verify them carefully. Your implementation should get any input parameters from the request and place any output parameters in the response. It can also put an array of file descriptors into the response using the kBASDescriptorArrayKey key. If an error occurs, you should just return an appropriate error code. BASHelperToolMain will ensure that this gets placed in the response. You should attempt to fail before adding any file descriptors to the response, or remove them once you know that you're going to fail. If you put file descriptors into the response and then return an error, those descriptors will still be passed back to the client. It's likely the client isn't expecting this. Calls to this function will be serialised; that is, once your callback is running, BASHelperToolMain won't call you again until you return. Your callback should avoid blocking for long periods of time. If you block for too long, the BAS watchdog will kill the entire helper tool process. This callback runs in a daemon context; you must avoid doing things that require the user's context. For example, launching a GUI application would be bad. See Technote 2083 "Daemons and Agents" for more information about execution contexts. @param auth This is a reference to the authorization instance associated with the original application that made the request. This will never be NULL. @param userData This is the value from the userData field of the corresponding entry in the BASCommandSpec array that you passed to BASHelperToolMain. @param request This dictionary contains the request. It will have, at a bare minimum, a kBASCommandKey item whose value matches one of the commands in the BASCommandSpec array you passed to BASHelperToolMain. It may also have other, command-specific parameters. This will never be NULL. @param response This is a dictionary into which you can place the response. It will start out empty, and you can add any results you please to it. If you need to return file descriptors, place them in an array and place that array in the response using the kBASDescriptorArrayKey key. There's no need to set the error result in the response. BASHelperToolMain will do that for you. However, if you do set a value for the kBASErrorKey key, that value will take precedence; in this case, the function result is ignored. This will never be NULL. @param asl A reference to the ASL client handle for logging. This may be NULL. However, ASL handles a NULL input, so you don't need to conditionalise your code. @param aslMsg A reference to a ASL message template for logging. This may be NULL. However, ASL handles a NULL input, so you don't need to conditionalise your code. */ typedef OSStatus (*BASCommandProc)( AuthorizationRef auth, const void * userData, CFDictionaryRef request, CFMutableDictionaryRef response, aslclient asl, aslmsg aslMsg ); /*! @function BASHelperToolMain @abstract Entry point for a privileged helper tool. @discussion You should call this function from the main function of your helper tool. It takes care of all of the details of receiving and processing commands. It will call you back (via one of the commandProcs callbacks) when a valid request arrives. This function assumes acts like a replacement for main. Thus, it assumes that it owns various process-wide resources (like SIGALRM and the disposition of SIGPIPE). You should not use those resources, either in your main function or in your callback function. Also, you should not call this function on a thread, or start any other threads in the process. Finally, this function has a habit of exiting the entire process if something goes wrong. You should not expect the function to always return. This function does not clean up after itself. When this function returns, you are expected to exit. If the function result is noErr, the command processing loop quit in an expected manner (typically because of an idle timeout). Otherwise it quit because of an error. @param commands An array that describes the commands that you implement, and their associated rights. The array is terminated by a command with a NULL name. There must be at least one valid command. @param commandProcs An array of callback routines that are called when a valid request arrives. The array is expected to perform the operation associated with the corresponding command and set up the response values, if any. The array is terminated by a NULL pointer. IMPORTANT: The array must have exactly the same number of entries as the commands array. @result An integer representing EXIT_SUCCESS or EXIT_FAILURE. */ extern int BASHelperToolMain( const BASCommandSpec commands[], const BASCommandProc commandProcs[] ); ///////////////////////////////////////////////////////////////// #pragma mark ***** Application Routines /*! @functiongroup Application Routines */ /*! @function BASSetDefaultRules @abstract Creates default right specifications in the policy database. @discussion This routine ensures that the policy database (currently "/etc/authorization") contains right specifications for all of the rights that you use (as specified by the commands array). This has two important consequences: 1. It makes the rights that you use visible to the system administrator. All they have to do is run your program once and they can see your default right specifications in the policy database. 2. It means that, when the privileged helper tool tries to acquire the right, it will use your specification of the right (as modified by the system administrator) rather than the default right specification. You must call this function before calling BASExecuteRequestInHelperTool. Typically you would call it at application startup time, or lazily, immediately before calling BASExecuteRequestInHelperTool. @param auth A reference to your program's authorization instance; you typically get this by calling AuthorizationCreate. This must not be NULL. @param commands An array that describes the commands that you implement, and their associated rights. There must be at least one valid command. @param bundleID The bundle identifier for your program. This must not be NULL. @param descriptionStringTableName The name of the .strings file from which to fetch the localised custom prompts for the rights in the commands array (if any). A NULL value is equivalent to passing "Localizable" (that is, it gets the prompts from "Localizable.strings"). For example, imagine you have a command for which you require a custom prompt. You should put the custom prompt in a .strings file, let's call it "AuthPrompts.strings". You should then pass "AuthPrompts" to this parameter and put the key that gets the prompt into the rightDescriptionKey of the command. */ extern void BASSetDefaultRules( AuthorizationRef auth, const BASCommandSpec commands[], CFStringRef bundleID, CFStringRef descriptionStringTableName ); /*! @function BASExecuteRequestInHelperTool @abstract Executes a request in the privileged helper tool, returning the response. @discussion This routine synchronously executes a request in the privileged helper tool and returns the response. If the function returns an error, the IPC between your application and the helper tool failed. Unfortunately it's not possible to tell whether this failure occurred while sending the request or receiving the response, thus it's not possible to know whether the privileged operation was done or not. If the functions returns no error, the IPC between your application and the helper tool was successful. However, the command may still have failed. You must get the error value from the response (typically using BASGetErrorFromResponse) to see if the command succeeded or not. On success the response dictionary may contain a value for the kBASDescriptorArrayKey key. If so, that will be a non-empty CFArray of CFNumbers, each of which can be accessed as an int. Each value is a descriptor that is being returned to you from the helper tool. You are responsible for closing these descriptors when you're done with them. @param auth A reference to your program's authorization instance; you typically get this by calling AuthorizationCreate. This must not be NULL. @param commands An array that describes the commands that you implement, and their associated rights. There must be at least one valid command. @param bundleID The bundle identifier for your program. This must not be NULL. @param request A dictionary describing the requested operation. This must, at least, contain a string value for the kBASCommandKey. Furthermore, this string must match one of the commands in the array. The dictionary may also contain other values. These are passed to the helper tool unintepreted. All values must be serialisable using the CFPropertyList API. This must not be NULL. @param response This must not be NULL. On entry, *response must be NULL. On success, *response will not be NULL. On error, *response will be NULL. On success, you are responsible for disposing of *response. You are also responsible for closing any descriptors returned in the response. @result An OSStatus code (see BASErrnoToOSStatus and BASOSStatusToErrno). */ extern OSStatus BASExecuteRequestInHelperTool( AuthorizationRef auth, const BASCommandSpec commands[], CFStringRef bundleID, CFDictionaryRef request, CFDictionaryRef * response ); /*! @enum BASFailCode @abstract Indicates why a request failed. @discussion If BASExecuteRequestInHelperTool fails with an error (indicating an IPC failure), you can call BASDiagnoseFailure to determine what went wrong. BASDiagnoseFailure will return the value of this type that best describes the failure. @constant kBASFailUnknown Indicates that BASDiagnoseFailure could not accurately determine the cause of the failure. @constant kBASFailDisabled The request failed because the helper tool is installed but disabled. @constant kBASFailPartiallyInstalled The request failed because the helper tool is only partially installed. @constant kBASFailNotInstalled The request failed because the helper tool is not installed at all. @constant kBASFailNeedsUpdate The request failed because the helper tool is installed but out of date. BASDiagnoseFailure will never return this value. However, if you detect that the helper tool is out of date (typically by sending it a "get version" request) you can pass this value to BASFixFailure to force it to update the tool. */ enum { kBASFailUnknown, kBASFailDisabled, kBASFailPartiallyInstalled, kBASFailNotInstalled, kBASFailNeedsUpdate }; typedef uint32_t BASFailCode; /*! @function BASDiagnoseFailure @abstract Determines the cause of a failed request. @discussion If BASExecuteRequestInHelperTool fails with an error (indicating an IPC failure), you can call this routine to determine what went wrong. It returns a BASFailCode value indicating the cause of the failure. You should use this value to tell the user what's going on and what you intend to do about it. Once you get the user's consent, you can call BASFixFailure to fix the problem. For example, if this function result is kBASFailDisabled, you could put up the dialog saying: My privileged helper tool is disabled. Would you like to enable it? This operation may require you to authorize as an admin user. [Cancel] [[Enable]] On the other hand, if this function result is kBASFailNotInstalled, the dialog might be: My privileged helper tool is not installed. Would you like to install it? This operation may require you to authorize as an admin user. [Cancel] [[Install]] BASDiagnoseFailure will never return kBASFailNeedsUpdate. It's your responsibility to detect version conflicts (a good way to do this is by sending a "get version" request to the helper tool). However, once you've detected a version conflict, you can pass kBASFailNeedsUpdate to BASFixFailure to get it to install the latest version of your helper tool. If you call this routine when everything is working properly, you're likely to get a result of kBASFailUnknown. @param auth A reference to your program's authorization instance; you typically get this by calling AuthorizationCreate. This must not be NULL. @param bundleID The bundle identifier for your program. This must not be NULL. @result A BASFailCode value indicating the cause of the failure. This will never be kBASFailNeedsUpdate. */ extern BASFailCode BASDiagnoseFailure( AuthorizationRef auth, CFStringRef bundleID ); /*! @function BASFixFailure @abstract Installs, or reinstalls, the privileged helper tool. @discussion This routine installs or reinstalls the privileged helper tool. Typically you call this in response to an IPC failure talking to the tool. You first diagnose the failure using BASDiagnoseFailure and then call this routine to fix the failure by installing (or reinstalling) the tool. Because the helper tool is privileged, installing it is a privileged operation. This routine will do its work by calling AuthorizationExecuteWithPrivileges, which is likely to prompt the user for an admin name and password. @param auth A reference to your program's authorization instance; you typically get this by calling AuthorizationCreate. This must not be NULL. @param bundleID The bundle identifier for your program. This must not be NULL. @param installToolName The name of the install tool within your bundle. You should place the tool in the executable directory within the bundle. Specifically, the tool must be available by passing this name to CFBundleCopyAuxiliaryExecutableURL. This must not be NULL. @param helperToolName The name of the helper tool within your bundle. You should place the tool in the executable directory within the bundle. Specifically, the tool must be available by passing this name to CFBundleCopyAuxiliaryExecutableURL. This must not be NULL. @param failCode A value indicating the type of failure that's occurred. In most cases you get this value by calling BASDiagnoseFailure. @result An OSStatus code (see BASErrnoToOSStatus and BASOSStatusToErrno). */ extern OSStatus BASFixFailure( AuthorizationRef auth, CFStringRef bundleID, CFStringRef installToolName, CFStringRef helperToolName, BASFailCode failCode ); ///////////////////////////////////////////////////////////////// #pragma mark ***** Utility Routines /*! @functiongroup Utilities */ /*! @function BASErrnoToOSStatus @abstract Convert an errno value to an OSStatus value. @discussion All errno values have accepted alternatives in the errSecErrnoBase OSStatus range, and this routine does the conversion. For example, ENOENT becomes errSecErrnoBase + ENOENT. Any value that's not recognised just gets passed through unmodified. A value of 0 becomes noErr. For more information about errSecErrnoBase, see DTS Q&A 1499 . @param errNum The errno value to convert. @result An OSStatus code representing the errno equivalent. */ extern OSStatus BASErrnoToOSStatus(int errNum); /*! @function BASOSStatusToErrno @abstract Convert an OSStatus value to an errno value. @discussion This function converts some specific OSStatus values (Open Transport and errSecErrnoBase ranges) to their corresponding errno values. It more-or-less undoes the conversion done by BASErrnoToOSStatus, including a pass through for unrecognised values. It's worth noting that there are many more defined OSStatus error codes than errno error codes, so you're more likely to encounter a passed through value when going in this direction. A value of noErr becomes 0. For more information about errSecErrnoBase, see DTS Q&A 1499 . @param errNum The OSStatus value to convert. @result An integer code representing the OSStatus equivalent. */ extern int BASOSStatusToErrno(OSStatus errNum); /*! @function BASGetErrorFromResponse @abstract Extracts the error status from a helper tool response. @discussion This function extracts the error status from a helper tool response. Specifically, its uses the kBASErrorKey key to get a CFNumber and it gets the resulting value from that number. @param response A helper tool response, typically acquired by calling BASExecuteRequestInHelperTool. This must not be NULL @result An OSStatus code (see BASErrnoToOSStatus and BASOSStatusToErrno). */ extern OSStatus BASGetErrorFromResponse(CFDictionaryRef response); /*! @function BASCloseDescriptorArray @abstract Closes all of the file descriptors referenced by a CFArray. @discussion Given a CFArray of CFNumbers, treat each number as a file descriptor and close it. The most common reason to use this routine is that you've executed, using BASExecuteRequestInHelperTool, a request that returns a response with embedded file descriptors, and you want to close those descriptors. In that case, you typically call this as: BASCloseDescriptorArray( CFDictionaryGetValue(response, CFSTR(kBASDescriptorArrayKey)) ); @param descArray The array containing the descriptors to close. This may be NULL, in which case the routine does nothing. */ extern void BASCloseDescriptorArray( CFArrayRef descArray ); ///////////////////////////////////////////////////////////////// #pragma mark ***** Utility Routines // The following definitions are exported purely for the convenience of the // install tool ("BetterAuthorizationSampleLibInstallTool.c"). You must not // use them in your own code. #if !defined(BAS_PRIVATE) #define BAS_PRIVATE 0 #endif #if BAS_PRIVATE // Hard-wired file system paths for the launchd property list file and // the privileged helper tool. In all cases, %s is a placeholder // for the bundle ID (in file system representation). #define kBASPlistPathFormat "/Library/LaunchDaemons/%s.plist" #define kBASToolDirPath "/Library/PrivilegedHelperTools" // KEEP IN SYNC! #define kBASToolPathFormat "/Library/PrivilegedHelperTools/%s" // KEEP IN SYNC! // Commands strings for the install tool. #define kBASInstallToolInstallCommand "install" #define kBASInstallToolEnableCommand "enable" // Magic values used to bracket the process ID returned by the install tool. #define kBASAntiZombiePIDToken1 "cricket<" #define kBASAntiZombiePIDToken2 ">bat" // Magic value used to indicate success or failure from the install tool. #define kBASInstallToolSuccess "oK" #define kBASInstallToolFailure "FailUrE %d" #endif #ifdef __cplusplus } #endif #endif