use crate::base::{Error, Result};
#[cfg(all(target_os = "macos", feature = "job-bless"))]
use core_foundation::base::Boolean;
use core_foundation::base::{CFTypeRef, TCFType};
use core_foundation::bundle::CFBundleRef;
use core_foundation::dictionary::{CFDictionary, CFDictionaryRef};
#[cfg(all(target_os = "macos", feature = "job-bless"))]
use core_foundation::error::CFError;
#[cfg(all(target_os = "macos", feature = "job-bless"))]
use core_foundation::error::CFErrorRef;
use core_foundation::string::{CFString, CFStringRef};
use security_framework_sys::authorization as sys;
use security_framework_sys::base::errSecConversionError;
use std::ffi::{CStr, CString};
use std::fs::File;
use std::mem::MaybeUninit;
use std::os::raw::c_void;
use std::ptr::addr_of;
use std::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! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Flags: sys::AuthorizationFlags {
const DEFAULTS = sys::kAuthorizationFlagDefaults;
const INTERACTION_ALLOWED = sys::kAuthorizationFlagInteractionAllowed;
const EXTEND_RIGHTS = sys::kAuthorizationFlagExtendRights;
const PARTIAL_RIGHTS = sys::kAuthorizationFlagPartialRights;
const DESTROY_RIGHTS = sys::kAuthorizationFlagDestroyRights;
const PREAUTHORIZE = sys::kAuthorizationFlagPreAuthorize;
}
}
impl Default for Flags {
#[inline(always)]
fn default() -> Flags {
Flags::DEFAULTS
}
}
#[repr(C)]
pub struct AuthorizationItem(sys::AuthorizationItem);
impl AuthorizationItem {
#[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")
}
}
#[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)
}
}
#[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);
}
}
}
#[derive(Debug)]
pub struct AuthorizationItemSetStorage {
names: Vec<CString>,
values: Vec<Option<Vec<u8>>>,
items: Vec<sys::AuthorizationItem>,
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(),
},
}
}
}
#[derive(Debug, Default)]
pub struct AuthorizationItemSetBuilder {
storage: AuthorizationItemSetStorage,
}
impl AuthorizationItemSetBuilder {
#[inline(always)]
#[must_use]
pub fn new() -> AuthorizationItemSetBuilder {
Default::default()
}
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)
}
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)
}
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)
}
#[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
}
}
#[derive(Copy, Clone)]
pub enum RightDefinition<'a> {
FromDictionary(&'a CFDictionary<CFStringRef, CFTypeRef>),
FromExistingRight(&'a str),
}
#[derive(Debug)]
pub struct Authorization {
handle: sys::AuthorizationRef,
free_flags: Flags,
}
impl TryFrom<AuthorizationExternalForm> for Authorization {
type Error = Error;
#[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: Flags::default(),
};
Ok(auth)
}
}
impl Authorization {
#[inline]
#[allow(clippy::should_implement_trait)]
pub fn default() -> Result<Self> {
Self::new(None, None, Default::default())
}
#[allow(clippy::unnecessary_cast)]
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| {
addr_of!(r.set) as *const sys::AuthorizationItemSet
});
let env_ptr = environment.as_ref().map_or(std::ptr::null(), |e| {
addr_of!(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(),
})
}
#[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()
}
#[inline(always)]
pub fn destroy_rights(mut self) {
self.free_flags = Flags::DESTROY_RIGHTS;
}
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)
}
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)
}
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(())
}
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(())
}
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)
}
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() })
}
#[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().flat_map(|a| CString::new(a.as_ref().as_bytes()))
.collect::<Vec<_>>();
self.execute_with_privileges_internal(command.as_ref().as_os_str().as_bytes(), &arguments, flags, false)?;
Ok(())
}
#[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().flat_map(|a| CString::new(a.as_ref().as_bytes()))
.collect::<Vec<_>>();
Ok(self.execute_with_privileges_internal(command.as_ref().as_os_str().as_bytes(), &arguments, flags, true)?.unwrap())
}
#[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(())
}
}
#[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)); }
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::*;
#[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", std::env::var("USER").expect("You must set the USER environment variable"))?
.add_string("password", std::env::var("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 std::env::var_os("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(())
}
#[test]
fn test_modify_authorization_database() -> Result<()> {
if std::env::var_os("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(())
}
#[test]
fn test_execute_with_privileges() -> Result<()> {
if std::env::var_os("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(())
}
}