//! Authorization Services support. use std::{ convert::{TryFrom, TryInto}, ffi::{CStr, CString}, fs::File, marker::PhantomData, mem::MaybeUninit, os::raw::c_void, ptr::addr_of, }; #[cfg(all(target_os = "macos", feature = "job-bless"))] use core_foundation::{ base::Boolean, error::{CFError, CFErrorRef}, }; use core_foundation::{ base::{CFTypeRef, TCFType}, bundle::CFBundleRef, dictionary::{CFDictionary, CFDictionaryRef}, string::{CFString, CFStringRef}, }; use security_framework_sys::{authorization as sys, base::errSecConversionError}; use sys::AuthorizationExternalForm; /// # Potential improvements /// /// * When generic specialization stabilizes prevent copying from `CString` /// arguments. /// * `AuthorizationCopyRightsAsync` /// * Provide constants for well known item names use crate::base::Error; /// # Potential improvements /// /// * When generic specialization stabilizes prevent copying from `CString` /// arguments. /// * `AuthorizationCopyRightsAsync` /// * Provide constants for well known item names use crate::base::Result; macro_rules! optional_str_to_cfref { ($string:ident) => {{ $string .map(CFString::new) .map_or(std::ptr::null(), |cfs| cfs.as_concrete_TypeRef()) }}; } macro_rules! cstring_or_err { ($x:expr) => {{ CString::new($x).map_err(|_| Error::from_code(errSecConversionError)) }}; } bitflags::bitflags! { /// The flags used to specify authorization options. #[derive(Debug, Clone)] pub struct Flags: sys::AuthorizationFlags { /// An empty flag set that you use as a placeholder when you don't want /// any of the other flags. const DEFAULTS = sys::kAuthorizationFlagDefaults; /// A flag that permits user interaction as needed. const INTERACTION_ALLOWED = sys::kAuthorizationFlagInteractionAllowed; /// A flag that permits the Security Server to attempt to grant the /// rights requested. const EXTEND_RIGHTS = sys::kAuthorizationFlagExtendRights; /// A flag that permits the Security Server to grant rights on an /// individual basis. const PARTIAL_RIGHTS = sys::kAuthorizationFlagPartialRights; /// A flag that instructs the Security Server to revoke authorization. const DESTROY_RIGHTS = sys::kAuthorizationFlagDestroyRights; /// A flag that instructs the Security Server to preauthorize the rights /// requested. const PREAUTHORIZE = sys::kAuthorizationFlagPreAuthorize; } } impl Default for Flags { #[inline(always)] fn default() -> Flags { Flags::DEFAULTS } } /// Information about an authorization right or the environment. #[repr(C)] pub struct AuthorizationItem(sys::AuthorizationItem); impl AuthorizationItem { /// The required name of the authorization right or environment data. /// /// If `name` isn't convertible to a `CString` it will return /// Err(errSecConversionError). #[must_use] pub fn name(&self) -> &str { unsafe { CStr::from_ptr(self.0.name) .to_str() .expect("AuthorizationItem::name failed to convert &str to CStr") } } /// The information pertaining to the name field. Do not rely on NULL /// termination of string data. #[inline] #[must_use] pub fn value(&self) -> Option<&[u8]> { if self.0.value.is_null() { return None; } let value = unsafe { std::slice::from_raw_parts(self.0.value as *const u8, self.0.valueLength) }; Some(value) } } /// A set of authorization items returned and owned by the Security Server. #[derive(Debug)] #[repr(C)] pub struct AuthorizationItemSet<'a> { inner: *const sys::AuthorizationItemSet, phantom: PhantomData<&'a sys::AuthorizationItemSet>, } impl<'a> Drop for AuthorizationItemSet<'a> { #[inline] fn drop(&mut self) { unsafe { sys::AuthorizationFreeItemSet(self.inner as *mut sys::AuthorizationItemSet); } } } /// Used by `AuthorizationItemSetBuilder` to store data pointed to by /// `sys::AuthorizationItemSet`. #[derive(Debug)] pub struct AuthorizationItemSetStorage { /// The layout of this is a little awkward because of the requirements of /// Apple's APIs. `items` contains pointers to data owned by `names` and /// `values`, so we must not modify them once `items` has been set up. names: Vec, values: Vec>>, items: Vec, /// Must not be given to APIs which would attempt to modify it. /// /// See `AuthorizationItemSet` for sets owned by the Security Server which /// are writable. pub set: sys::AuthorizationItemSet, } impl Default for AuthorizationItemSetStorage { #[inline] fn default() -> Self { AuthorizationItemSetStorage { names: Vec::new(), values: Vec::new(), items: Vec::new(), set: sys::AuthorizationItemSet { count: 0, items: std::ptr::null_mut(), }, } } } /// A convenience `AuthorizationItemSetBuilder` builder which enabled you to use /// rust types. All names and values passed in will be copied. #[derive(Debug, Default)] pub struct AuthorizationItemSetBuilder { storage: AuthorizationItemSetStorage, } // Stores AuthorizationItems contiguously, and their items separately impl AuthorizationItemSetBuilder { /// Creates a new `AuthorizationItemSetStore`, which simplifies creating /// owned vectors of `AuthorizationItem`s. #[inline(always)] #[must_use] pub fn new() -> AuthorizationItemSetBuilder { Default::default() } /// Adds an `AuthorizationItem` with the name set to a right and an empty /// value. /// /// If `name` isn't convertible to a `CString` it will return /// Err(errSecConversionError). pub fn add_right>>(mut self, name: N) -> Result { self.storage.names.push(cstring_or_err!(name)?); self.storage.values.push(None); Ok(self) } /// Adds an `AuthorizationItem` with arbitrary data. /// /// If `name` isn't convertible to a `CString` it will return /// Err(errSecConversionError). pub fn add_data(mut self, name: N, value: V) -> Result where N: Into>, V: Into>, { self.storage.names.push(cstring_or_err!(name)?); self.storage.values.push(Some(value.into())); Ok(self) } /// Adds an `AuthorizationItem` with NULL terminated string data. /// /// If `name` or `value` isn't convertible to a `CString` it will return /// Err(errSecConversionError). pub fn add_string(mut self, name: N, value: V) -> Result where N: Into>, V: Into>, { self.storage.names.push(cstring_or_err!(name)?); self.storage .values .push(Some(cstring_or_err!(value)?.to_bytes().to_vec())); Ok(self) } /// Creates the `sys::AuthorizationItemSet`, and gives you ownership of the /// data it points to. #[must_use] pub fn build(mut self) -> AuthorizationItemSetStorage { self.storage.items = self .storage .names .iter() .zip(self.storage.values.iter()) .map(|(n, v)| sys::AuthorizationItem { name: n.as_ptr(), value: v .as_ref() .map_or(std::ptr::null_mut(), |v| v.as_ptr() as *mut c_void), valueLength: v.as_ref().map_or(0, |v| v.len()), flags: 0, }) .collect(); self.storage.set = sys::AuthorizationItemSet { count: self.storage.items.len() as u32, items: self.storage.items.as_ptr() as *mut sys::AuthorizationItem, }; self.storage } } /// Used by `Authorization::set_item` to define the rules of he right. #[derive(Copy, Clone)] pub enum RightDefinition<'a> { /// The dictionary will contain the keys and values that define the rules. FromDictionary(&'a CFDictionary), /// The specified right's rules will be duplicated. FromExistingRight(&'a str), } /// A wrapper around `AuthorizationCreate` and functions which operate on an /// `AuthorizationRef`. #[derive(Debug)] pub struct Authorization { handle: sys::AuthorizationRef, free_flags: Flags, } impl TryFrom for Authorization { type Error = Error; /// Internalizes the external representation of an authorization reference. #[cold] fn try_from(external_form: AuthorizationExternalForm) -> Result { let mut handle = MaybeUninit::::uninit(); let status = unsafe { sys::AuthorizationCreateFromExternalForm(&external_form, handle.as_mut_ptr()) }; if status != sys::errAuthorizationSuccess { return Err(Error::from_code(status)); } let auth = Authorization { handle: unsafe { handle.assume_init() }, free_flags: Default::default(), }; Ok(auth) } } impl Authorization { /// Creates an authorization object which has no environment or associated /// rights. #[allow(clippy::should_implement_trait)] #[inline] pub fn default() -> Result { Self::new(None, None, Default::default()) } /// Creates an authorization reference and provides an option to authorize /// or preauthorize rights. /// /// `rights` should be the names of the rights you want to create. /// /// `environment` is used when authorizing or pre-authorizing rights. Not /// used in OS X v10.2 and earlier. In macOS 10.3 and later, you can pass /// icon or prompt data to be used in the authentication dialog box. In /// macOS 10.4 and later, you can also pass a user name and password in /// order to authorize a user without user interaction. pub fn new( // FIXME: this should have been by reference rights: Option, environment: Option, flags: Flags, ) -> Result { let rights_ptr = rights .as_ref() .map_or(std::ptr::null(), |r| addr_of!(r.set) as *const _); let env_ptr = environment .as_ref() .map_or(std::ptr::null(), |e| addr_of!(e.set) as *const _); let mut handle = MaybeUninit::::uninit(); let status = unsafe { sys::AuthorizationCreate(rights_ptr, env_ptr, flags.bits(), handle.as_mut_ptr()) }; if status != sys::errAuthorizationSuccess { return Err(Error::from_code(status)); } Ok(Authorization { handle: unsafe { handle.assume_init() }, free_flags: Default::default(), }) } /// Internalizes the external representation of an authorization reference. #[deprecated(since = "2.0.1", note = "Please use the TryFrom trait instead")] pub fn from_external_form(external_form: sys::AuthorizationExternalForm) -> Result { external_form.try_into() } /// By default the rights acquired will be retained by the Security Server. /// Use this to ensure they are destroyed and to prevent shared rights' /// continued used by other processes. #[inline(always)] pub fn destroy_rights(mut self) { self.free_flags = Flags::DESTROY_RIGHTS; } /// Retrieve's the right's definition as a dictionary. Use `right_exists` /// if you want to avoid retrieving the dictionary. /// /// `name` can be a wildcard right name. /// /// If `name` isn't convertible to a `CString` it will return /// Err(errSecConversionError). pub fn get_right>>(name: T) -> Result> { let name = cstring_or_err!(name)?; let mut dict = MaybeUninit::::uninit(); let status = unsafe { sys::AuthorizationRightGet(name.as_ptr(), dict.as_mut_ptr()) }; if status != sys::errAuthorizationSuccess { return Err(Error::from_code(status)); } let dict = unsafe { CFDictionary::wrap_under_create_rule(dict.assume_init()) }; Ok(dict) } /// Checks if a right exists within the policy database. This is the same as /// `get_right`, but avoids a dictionary allocation. /// /// If `name` isn't convertible to a `CString` it will return /// Err(errSecConversionError). pub fn right_exists>>(name: T) -> Result { let name = cstring_or_err!(name)?; let status = unsafe { sys::AuthorizationRightGet(name.as_ptr(), std::ptr::null_mut()) }; Ok(status == sys::errAuthorizationSuccess) } /// Removes a right from the policy database. /// /// `name` cannot be a wildcard right name. /// /// If `name` isn't convertible to a `CString` it will return /// Err(errSecConversionError). pub fn remove_right>>(&self, name: T) -> Result<()> { let name = cstring_or_err!(name)?; let status = unsafe { sys::AuthorizationRightRemove(self.handle, name.as_ptr()) }; if status != sys::errAuthorizationSuccess { return Err(Error::from_code(status)); } Ok(()) } /// Creates or updates a right entry in the policy database. Your process /// must have a code signature in order to be able to add rights to the /// authorization database. /// /// `name` cannot be a wildcard right. /// /// `definition` can be either a `CFDictionaryRef` containing keys defining /// the rules or a `CFStringRef` representing the name of another right /// whose rules you wish to duplicate. /// /// `description` is a key which can be used to look up localized /// descriptions. /// /// `bundle` will be used to get localizations from if not the main bundle. /// /// `localeTableName` will be used to get localizations if provided. /// /// If `name` isn't convertible to a `CString` it will return /// Err(errSecConversionError). pub fn set_right>>( &self, name: T, definition: RightDefinition<'_>, description: Option<&str>, bundle: Option, locale: Option<&str>, ) -> Result<()> { let name = cstring_or_err!(name)?; let definition_cfstring: CFString; let definition_ref = match definition { RightDefinition::FromDictionary(def) => def.as_CFTypeRef(), RightDefinition::FromExistingRight(def) => { definition_cfstring = CFString::new(def); definition_cfstring.as_CFTypeRef() } }; let status = unsafe { sys::AuthorizationRightSet( self.handle, name.as_ptr(), definition_ref, optional_str_to_cfref!(description), bundle.unwrap_or(std::ptr::null_mut()), optional_str_to_cfref!(locale), ) }; if status != sys::errAuthorizationSuccess { return Err(Error::from_code(status)); } Ok(()) } /// An authorization plugin can store the results of an authentication /// operation by calling the `SetContextValue` function. You can then /// retrieve this supporting data, such as the user name. /// /// `tag` should specify the type of data the Security Server should return. /// If `None`, all available information is retrieved. /// /// If `tag` isn't convertible to a `CString` it will return /// Err(errSecConversionError). pub fn copy_info>>(&self, tag: Option) -> Result> { let tag_with_nul: CString; let tag_ptr = match tag { Some(tag) => { tag_with_nul = cstring_or_err!(tag)?; tag_with_nul.as_ptr() } None => std::ptr::null(), }; let mut inner = MaybeUninit::<*mut sys::AuthorizationItemSet>::uninit(); let status = unsafe { sys::AuthorizationCopyInfo(self.handle, tag_ptr, inner.as_mut_ptr()) }; if status != sys::errAuthorizationSuccess { return Err(Error::from(status)); } let set = AuthorizationItemSet { inner: unsafe { inner.assume_init() }, phantom: PhantomData, }; Ok(set) } /// Creates an external representation of an authorization reference so that /// you can transmit it between processes. pub fn make_external_form(&self) -> Result { let mut external_form = MaybeUninit::::uninit(); let status = unsafe { sys::AuthorizationMakeExternalForm(self.handle, external_form.as_mut_ptr()) }; if status != sys::errAuthorizationSuccess { return Err(Error::from(status)); } Ok(unsafe { external_form.assume_init() }) } /// Runs an executable tool with root privileges. /// Discards executable's output #[cfg(target_os = "macos")] #[inline(always)] pub fn execute_with_privileges( &self, command: P, arguments: I, flags: Flags, ) -> Result<()> where P: AsRef, I: IntoIterator, S: AsRef, { use std::os::unix::ffi::OsStrExt; let arguments = arguments .into_iter() .flat_map(|a| CString::new(a.as_ref().as_bytes())) .collect::>(); self.execute_with_privileges_internal( command.as_ref().as_os_str().as_bytes(), &arguments, flags, false, )?; Ok(()) } /// Runs an executable tool with root privileges, /// and returns a `File` handle to its communication pipe #[cfg(target_os = "macos")] #[inline(always)] pub fn execute_with_privileges_piped( &self, command: P, arguments: I, flags: Flags, ) -> Result where P: AsRef, I: IntoIterator, S: AsRef, { use std::os::unix::ffi::OsStrExt; let arguments = arguments .into_iter() .flat_map(|a| CString::new(a.as_ref().as_bytes())) .collect::>(); Ok(self .execute_with_privileges_internal( command.as_ref().as_os_str().as_bytes(), &arguments, flags, true, )? .unwrap()) } /// Submits the executable for the given label as a `launchd` job. #[cfg(all(target_os = "macos", feature = "job-bless"))] pub fn job_bless(&self, label: &str) -> Result<(), CFError> { #[link(name = "ServiceManagement", kind = "framework")] extern "C" { static kSMDomainSystemLaunchd: CFStringRef; fn SMJobBless( domain: CFStringRef, executableLabel: CFStringRef, auth: sys::AuthorizationRef, error: *mut CFErrorRef, ) -> Boolean; } unsafe { let mut error = std::ptr::null_mut(); SMJobBless( kSMDomainSystemLaunchd, CFString::new(label).as_concrete_TypeRef(), self.handle, &mut error, ); if !error.is_null() { return Err(CFError::wrap_under_create_rule(error)); } Ok(()) } } // Runs an executable tool with root privileges. #[cfg(target_os = "macos")] fn execute_with_privileges_internal( &self, command: &[u8], arguments: &[CString], flags: Flags, make_pipe: bool, ) -> Result> { use std::os::unix::io::{FromRawFd, RawFd}; let c_cmd = cstring_or_err!(command)?; let mut c_args = arguments .iter() .map(|a| a.as_ptr() as _) .collect::>(); c_args.push(std::ptr::null_mut()); let mut pipe: *mut libc::FILE = std::ptr::null_mut(); let status = unsafe { sys::AuthorizationExecuteWithPrivileges( self.handle, c_cmd.as_ptr(), flags.bits(), c_args.as_ptr(), if make_pipe { &mut pipe } else { std::ptr::null_mut() }, ) }; crate::cvt(status)?; Ok(if make_pipe { if pipe.is_null() { return Err(Error::from_code(32)); // EPIPE? } Some(unsafe { File::from_raw_fd(libc::fileno(pipe) as RawFd) }) } else { None }) } } impl Drop for Authorization { #[inline] fn drop(&mut self) { unsafe { sys::AuthorizationFree(self.handle, self.free_flags.bits()); } } } #[cfg(test)] mod tests { use core_foundation::string::CFString; use super::*; #[test] fn test_create_default_authorization() { Authorization::default().unwrap(); } #[test] fn test_create_allowed_authorization() -> Result<()> { let rights = AuthorizationItemSetBuilder::new() .add_right("system.hdd.smart")? .add_right("system.login.done")? .build(); Authorization::new(Some(rights), None, Flags::EXTEND_RIGHTS).unwrap(); Ok(()) } #[test] fn test_create_then_destroy_allowed_authorization() -> Result<()> { let rights = AuthorizationItemSetBuilder::new() .add_right("system.hdd.smart")? .add_right("system.login.done")? .build(); let auth = Authorization::new(Some(rights), None, Flags::EXTEND_RIGHTS).unwrap(); auth.destroy_rights(); Ok(()) } #[test] fn test_create_authorization_requiring_interaction() -> Result<()> { let rights = AuthorizationItemSetBuilder::new() .add_right("system.privilege.admin")? .build(); let error = Authorization::new(Some(rights), None, Flags::EXTEND_RIGHTS).unwrap_err(); assert_eq!(error.code(), sys::errAuthorizationInteractionNotAllowed); Ok(()) } fn create_credentials_env() -> Result { #![allow(clippy::option_env_unwrap)] let set = AuthorizationItemSetBuilder::new() .add_string( "username", option_env!("USER").expect("You must set the USER environment variable"), )? .add_string( "password", option_env!("PASSWORD").expect("You must set the PASSWORD environment variable"), )? .build(); Ok(set) } #[test] fn test_create_authorization_with_bad_credentials() -> Result<()> { let rights = AuthorizationItemSetBuilder::new() .add_right("system.privilege.admin")? .build(); let env = AuthorizationItemSetBuilder::new() .add_string("username", "Tim Apple")? .add_string("password", "butterfly")? .build(); let error = Authorization::new(Some(rights), Some(env), Flags::INTERACTION_ALLOWED).unwrap_err(); assert_eq!(error.code(), sys::errAuthorizationDenied); Ok(()) } #[test] fn test_create_authorization_with_credentials() -> Result<()> { if option_env!("PASSWORD").is_none() { return Ok(()); } let rights = AuthorizationItemSetBuilder::new() .add_right("system.privilege.admin")? .build(); let env = create_credentials_env()?; Authorization::new(Some(rights), Some(env), Flags::EXTEND_RIGHTS).unwrap(); Ok(()) } #[test] fn test_query_authorization_database() -> Result<()> { assert!(Authorization::right_exists("system.hdd.smart")?); assert!(!Authorization::right_exists("EMPTY")?); let dict = Authorization::get_right("system.hdd.smart").unwrap(); let key = CFString::from_static_string("class"); assert!(dict.contains_key(&key)); let invalid_key = CFString::from_static_string("EMPTY"); assert!(!dict.contains_key(&invalid_key)); Ok(()) } /// This test will only pass if its process has a valid code signature. #[test] fn test_modify_authorization_database() -> Result<()> { if option_env!("PASSWORD").is_none() { return Ok(()); } let rights = AuthorizationItemSetBuilder::new() .add_right("config.modify.")? .build(); let env = create_credentials_env()?; let auth = Authorization::new(Some(rights), Some(env), Flags::EXTEND_RIGHTS).unwrap(); assert!(!Authorization::right_exists("TEST_RIGHT")?); auth.set_right( "TEST_RIGHT", RightDefinition::FromExistingRight("system.hdd.smart"), None, None, None, ) .unwrap(); assert!(Authorization::right_exists("TEST_RIGHT")?); auth.remove_right("TEST_RIGHT").unwrap(); assert!(!Authorization::right_exists("TEST_RIGHT")?); Ok(()) } /// This test will succeed if authorization popup is approved. #[test] fn test_execute_with_privileges() -> Result<()> { if option_env!("PASSWORD").is_none() { return Ok(()); } let rights = AuthorizationItemSetBuilder::new() .add_right("system.privilege.admin")? .build(); let auth = Authorization::new( Some(rights), None, Flags::DEFAULTS | Flags::INTERACTION_ALLOWED | Flags::PREAUTHORIZE | Flags::EXTEND_RIGHTS, )?; let file = auth.execute_with_privileges_piped("/bin/ls", ["/"], Flags::DEFAULTS)?; use std::io::{ BufRead, {self}, }; for line in io::BufReader::new(file).lines() { let _ = line.unwrap(); } Ok(()) } }