blob: 50f91cb918962f7be9dc5c65c69770c9d8e3705f [file] [log] [blame]
//! Authorization Services support.
/// # Potential improvements
///
/// * When generic specialization stabilizes prevent copying from CString
/// arguments.
/// * AuthorizationCopyRightsAsync
/// * Provide constants for well known item names
use crate::base::{Error, Result};
use core_foundation::base::{CFTypeRef, TCFType};
use core_foundation::bundle::CFBundleRef;
use core_foundation::dictionary::{CFDictionary, CFDictionaryRef};
use core_foundation::string::{CFString, CFStringRef};
use security_framework_sys::authorization as sys;
use security_framework_sys::base::errSecConversionError;
use std::mem::MaybeUninit;
use std::os::raw::c_void;
use std::{
convert::TryFrom,
ffi::{CStr, CString},
fs::File,
};
use std::{convert::TryInto, marker::PhantomData};
use sys::AuthorizationExternalForm;
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.
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 convertable to a `CString` it will return
/// Err(errSecConversionError).
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.
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<CString>,
values: Vec<Option<Vec<u8>>>,
items: Vec<sys::AuthorizationItem>,
/// 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)]
pub fn new() -> AuthorizationItemSetBuilder {
Default::default()
}
/// Adds an AuthorizationItem with the name set to a right and an empty
/// value.
///
/// If `name` isn't convertable to a `CString` it will return
/// Err(errSecConversionError).
pub fn add_right<N: Into<Vec<u8>>>(mut self, name: N) -> Result<Self> {
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 convertable to a `CString` it will return
/// Err(errSecConversionError).
pub fn add_data<N, V>(mut self, name: N, value: V) -> Result<Self>
where
N: Into<Vec<u8>>,
V: Into<Vec<u8>>,
{
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 convertable to a `CString` it will return
/// Err(errSecConversionError).
pub fn add_string<N, V>(mut self, name: N, value: V) -> Result<Self>
where
N: Into<Vec<u8>>,
V: Into<Vec<u8>>,
{
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.
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.
pub enum RightDefinition<'a> {
/// The dictionary will contain the keys and values that define the rules.
FromDictionary(&'a CFDictionary<CFStringRef, CFTypeRef>),
/// 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<AuthorizationExternalForm> for Authorization {
type Error = Error;
/// Internalizes the external representation of an authorization reference.
#[cold]
fn try_from(external_form: AuthorizationExternalForm) -> Result<Self> {
let mut handle = MaybeUninit::<sys::AuthorizationRef>::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<'a> Authorization {
/// Creates an authorization object which has no environment or associated
/// rights.
#[inline]
pub fn default() -> Result<Self> {
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 preauthorizing 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(
rights: Option<AuthorizationItemSetStorage>,
environment: Option<AuthorizationItemSetStorage>,
flags: Flags,
) -> Result<Self> {
let rights_ptr = rights
.as_ref()
.map_or(std::ptr::null(), |r| &r.set as *const sys::AuthorizationItemSet);
let env_ptr = environment
.as_ref()
.map_or(std::ptr::null(), |e| &e.set as *const sys::AuthorizationItemSet);
let mut handle = MaybeUninit::<sys::AuthorizationRef>::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<Self> {
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 convertable to a `CString` it will return
/// Err(errSecConversionError).
pub fn get_right<T: Into<Vec<u8>>>(name: T) -> Result<CFDictionary<CFString, CFTypeRef>> {
let name = cstring_or_err!(name)?;
let mut dict = MaybeUninit::<CFDictionaryRef>::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 convertable to a `CString` it will return
/// Err(errSecConversionError).
pub fn right_exists<T: Into<Vec<u8>>>(name: T) -> Result<bool> {
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 convertable to a `CString` it will return
/// Err(errSecConversionError).
pub fn remove_right<T: Into<Vec<u8>>>(&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 duplicaate.
///
/// `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 convertable to a `CString` it will return
/// Err(errSecConversionError).
pub fn set_right<T: Into<Vec<u8>>>(
&self,
name: T,
definition: RightDefinition<'_>,
description: Option<&str>,
bundle: Option<CFBundleRef>,
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 retreieved.
///
/// If `tag` isn't convertable to a `CString` it will return
/// Err(errSecConversionError).
pub fn copy_info<T: Into<Vec<u8>>>(&self, tag: Option<T>) -> Result<AuthorizationItemSet<'_>> {
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<sys::AuthorizationExternalForm> {
let mut external_form = MaybeUninit::<sys::AuthorizationExternalForm>::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<P, S, I>(
&self,
command: P,
arguments: I,
flags: Flags,
) -> Result<()>
where
P: AsRef<std::path::Path>,
I: IntoIterator<Item = S>,
S: AsRef<std::ffi::OsStr>,
{
use std::os::unix::ffi::OsStrExt;
let arguments = arguments
.into_iter()
.map(|a| CString::new(a.as_ref().as_bytes()))
.flatten()
.collect::<Vec<_>>();
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<P, S, I>(
&self,
command: P,
arguments: I,
flags: Flags,
) -> Result<File>
where
P: AsRef<std::path::Path>,
I: IntoIterator<Item = S>,
S: AsRef<std::ffi::OsStr>,
{
use std::os::unix::ffi::OsStrExt;
let arguments = arguments
.into_iter()
.map(|a| CString::new(a.as_ref().as_bytes()))
.flatten()
.collect::<Vec<_>>();
Ok(self
.execute_with_privileges_internal(
command.as_ref().as_os_str().as_bytes(),
&arguments,
flags,
true,
)?
.unwrap())
}
// 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<Option<File>> {
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::<Vec<_>>();
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 super::*;
use core_foundation::string::CFString;
#[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<AuthorizationItemSetStorage> {
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 varible"),
)?
.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::{self, BufRead};
for line in io::BufReader::new(file).lines() {
let _ = line.unwrap();
}
Ok(())
}
}