mirror of
https://github.com/awfixers-stuff/src.git
synced 2026-03-27 04:55:59 +00:00
create src
This commit is contained in:
1274
src-sec/CHANGELOG.md
Normal file
1274
src-sec/CHANGELOG.md
Normal file
File diff suppressed because it is too large
Load Diff
48
src-sec/Cargo.toml
Normal file
48
src-sec/Cargo.toml
Normal file
@@ -0,0 +1,48 @@
|
||||
lints.workspace = true
|
||||
|
||||
[package]
|
||||
name = "src-sec"
|
||||
version = "0.13.1"
|
||||
repository = "https://github.com/GitoxideLabs/gitoxide"
|
||||
license = "MIT OR Apache-2.0"
|
||||
description = "A crate of the gitoxide project providing a shared trust model"
|
||||
authors = ["Sebastian Thiel <sebastian.thiel@icloud.com>"]
|
||||
edition = "2021"
|
||||
include = ["src/**/*", "LICENSE-*"]
|
||||
rust-version = "1.82"
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
## Data structures implement `serde::Serialize` and `serde::Deserialize`.
|
||||
serde = ["dep:serde", "bitflags/serde"]
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0.114", optional = true, default-features = false, features = [
|
||||
"std",
|
||||
"derive",
|
||||
] }
|
||||
bitflags = "2"
|
||||
|
||||
document-features = { version = "0.2.1", optional = true }
|
||||
|
||||
[target.'cfg(not(windows))'.dependencies]
|
||||
libc = "0.2.182"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
src-path = { version = "^0.11.1", path = "../src-path" }
|
||||
windows-sys = { version = "0.61.1", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_Security_Authorization",
|
||||
"Win32_Storage_FileSystem",
|
||||
"Win32_System_Memory",
|
||||
"Win32_System_Threading",
|
||||
] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.26.0"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
features = ["document-features"]
|
||||
1
src-sec/LICENSE-APACHE
Symbolic link
1
src-sec/LICENSE-APACHE
Symbolic link
@@ -0,0 +1 @@
|
||||
../LICENSE-APACHE
|
||||
1
src-sec/LICENSE-MIT
Symbolic link
1
src-sec/LICENSE-MIT
Symbolic link
@@ -0,0 +1 @@
|
||||
../LICENSE-MIT
|
||||
224
src-sec/src/identity.rs
Normal file
224
src-sec/src/identity.rs
Normal file
@@ -0,0 +1,224 @@
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
/// An account based identity
|
||||
pub struct Account {
|
||||
/// The user's name
|
||||
pub username: String,
|
||||
/// The user's password
|
||||
pub password: String,
|
||||
/// An OAuth refresh token that may accompany the password. It is to be treated confidentially, just like the password.
|
||||
pub oauth_refresh_token: Option<String>,
|
||||
}
|
||||
|
||||
/// Returns true if the given `path` is owned by the user who is executing the current process.
|
||||
///
|
||||
/// Note that this method is very specific to avoid having to deal with any operating system types.
|
||||
pub fn is_path_owned_by_current_user(path: &Path) -> std::io::Result<bool> {
|
||||
impl_::is_path_owned_by_current_user(path)
|
||||
}
|
||||
|
||||
// Wasi doesn't have a concept of a user, so this is implicitly true.
|
||||
#[cfg(target_os = "wasi")]
|
||||
mod impl_ {
|
||||
pub fn is_path_owned_by_current_user(_path: &std::path::Path) -> std::io::Result<bool> {
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(not(windows), not(target_os = "wasi")))]
|
||||
mod impl_ {
|
||||
use std::path::Path;
|
||||
|
||||
pub fn is_path_owned_by_current_user(path: &Path) -> std::io::Result<bool> {
|
||||
fn owner_from_path(path: &Path) -> std::io::Result<u32> {
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
let meta = std::fs::symlink_metadata(path)?;
|
||||
Ok(meta.uid())
|
||||
}
|
||||
|
||||
fn owner_of_current_process() -> std::io::Result<u32> {
|
||||
// SAFETY: there is no documented possibility for failure
|
||||
#[allow(unsafe_code)]
|
||||
let uid = unsafe { libc::geteuid() };
|
||||
Ok(uid)
|
||||
}
|
||||
use std::str::FromStr;
|
||||
|
||||
let owner_of_path = owner_from_path(path)?;
|
||||
let owner_of_process = owner_of_current_process()?;
|
||||
if owner_of_path == owner_of_process {
|
||||
Ok(true)
|
||||
} else if let Some(sudo_uid) =
|
||||
std::env::var_os("SUDO_UID").and_then(|val| val.to_str().and_then(|val_str| u32::from_str(val_str).ok()))
|
||||
{
|
||||
Ok(owner_of_path == sudo_uid)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
mod impl_ {
|
||||
use std::{
|
||||
io,
|
||||
mem::MaybeUninit,
|
||||
os::windows::io::{FromRawHandle as _, OwnedHandle},
|
||||
path::Path,
|
||||
ptr,
|
||||
};
|
||||
|
||||
macro_rules! error {
|
||||
($msg:expr) => {{
|
||||
let inner = io::Error::last_os_error();
|
||||
error!(inner, $msg);
|
||||
}};
|
||||
($inner:expr, $msg:expr) => {{
|
||||
return Err(io::Error::new($inner.kind(), $msg));
|
||||
}};
|
||||
}
|
||||
|
||||
pub fn is_path_owned_by_current_user(path: &Path) -> io::Result<bool> {
|
||||
use windows_sys::Win32::{
|
||||
Foundation::{GetLastError, LocalFree, ERROR_INSUFFICIENT_BUFFER, ERROR_INVALID_FUNCTION, ERROR_SUCCESS},
|
||||
Security::{
|
||||
Authorization::{GetNamedSecurityInfoW, SE_FILE_OBJECT},
|
||||
CheckTokenMembership, EqualSid, GetTokenInformation, IsWellKnownSid, TokenOwner,
|
||||
WinBuiltinAdministratorsSid, OWNER_SECURITY_INFORMATION, PSECURITY_DESCRIPTOR, TOKEN_OWNER,
|
||||
TOKEN_QUERY,
|
||||
},
|
||||
System::Threading::{GetCurrentProcess, GetCurrentThread, OpenProcessToken, OpenThreadToken},
|
||||
};
|
||||
|
||||
if !path.exists() {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
format!("{path:?} does not exist."),
|
||||
));
|
||||
}
|
||||
|
||||
// Home is not actually owned by the corresponding user
|
||||
// but it can be considered de-facto owned by the user
|
||||
// Ignore errors here and just do the regular checks below
|
||||
if gix_path::realpath(path).ok() == gix_path::env::home_dir() {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
#[allow(unsafe_code)]
|
||||
unsafe {
|
||||
let (folder_owner, descriptor) = {
|
||||
let mut folder_owner = MaybeUninit::uninit();
|
||||
let mut pdescriptor = MaybeUninit::uninit();
|
||||
let result = GetNamedSecurityInfoW(
|
||||
to_wide_path(path).as_ptr(),
|
||||
SE_FILE_OBJECT,
|
||||
OWNER_SECURITY_INFORMATION,
|
||||
folder_owner.as_mut_ptr(),
|
||||
ptr::null_mut(),
|
||||
ptr::null_mut(),
|
||||
ptr::null_mut(),
|
||||
pdescriptor.as_mut_ptr(),
|
||||
);
|
||||
|
||||
if result != ERROR_SUCCESS {
|
||||
if result == ERROR_INVALID_FUNCTION {
|
||||
// We cannot obtain security information, so we default to reduced trust
|
||||
// (false) rather than failing completely.
|
||||
return Ok(false);
|
||||
}
|
||||
let inner = io::Error::from_raw_os_error(result as _);
|
||||
error!(
|
||||
inner,
|
||||
format!(
|
||||
"Couldn't get security information for path '{}' with err {inner}",
|
||||
path.display()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
(folder_owner.assume_init(), pdescriptor.assume_init())
|
||||
};
|
||||
|
||||
struct Descriptor(PSECURITY_DESCRIPTOR);
|
||||
|
||||
impl Drop for Descriptor {
|
||||
fn drop(&mut self) {
|
||||
#[allow(unsafe_code)]
|
||||
// SAFETY: syscall only invoked if we have a valid descriptor
|
||||
unsafe {
|
||||
LocalFree(self.0 as _);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let _descriptor = Descriptor(descriptor);
|
||||
|
||||
let token = {
|
||||
let mut token = MaybeUninit::uninit();
|
||||
|
||||
// Use the current thread token if possible, otherwise open the process token
|
||||
if OpenThreadToken(GetCurrentThread(), TOKEN_QUERY, 1, token.as_mut_ptr()) == 0
|
||||
&& OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, token.as_mut_ptr()) == 0
|
||||
{
|
||||
error!("Couldn't acquire thread or process token");
|
||||
}
|
||||
token.assume_init()
|
||||
};
|
||||
|
||||
let _owned_token = OwnedHandle::from_raw_handle(token as _);
|
||||
|
||||
let buf = 'token_buf: {
|
||||
let mut buffer_size = 36;
|
||||
let mut heap_buf = vec![0; 36];
|
||||
|
||||
loop {
|
||||
if GetTokenInformation(
|
||||
token,
|
||||
TokenOwner,
|
||||
heap_buf.as_mut_ptr().cast(),
|
||||
heap_buf.len() as _,
|
||||
&mut buffer_size,
|
||||
) != 0
|
||||
{
|
||||
break 'token_buf heap_buf;
|
||||
}
|
||||
|
||||
if GetLastError() != ERROR_INSUFFICIENT_BUFFER {
|
||||
error!("Couldn't acquire token ownership");
|
||||
}
|
||||
|
||||
heap_buf.resize(buffer_size as _, 0);
|
||||
}
|
||||
};
|
||||
|
||||
let token_owner = (*buf.as_ptr().cast::<TOKEN_OWNER>()).Owner;
|
||||
|
||||
// If the current user is the owner of the parent folder then they also
|
||||
// own this file
|
||||
if EqualSid(folder_owner, token_owner) != 0 {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
// Admin-group owned folders are considered owned by the current user, if they are in the admin group
|
||||
if IsWellKnownSid(token_owner, WinBuiltinAdministratorsSid) == 0 {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let mut is_member = 0;
|
||||
if CheckTokenMembership(std::ptr::null_mut(), token_owner, &mut is_member) == 0 {
|
||||
error!("Couldn't check if user is an administrator");
|
||||
}
|
||||
|
||||
Ok(is_member != 0)
|
||||
}
|
||||
}
|
||||
|
||||
fn to_wide_path(path: impl AsRef<Path>) -> Vec<u16> {
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
let mut wide_path: Vec<_> = path.as_ref().as_os_str().encode_wide().collect();
|
||||
wide_path.push(0);
|
||||
wide_path
|
||||
}
|
||||
}
|
||||
61
src-sec/src/lib.rs
Normal file
61
src-sec/src/lib.rs
Normal file
@@ -0,0 +1,61 @@
|
||||
//! A shared trust model for `gitoxide` crates.
|
||||
//!
|
||||
//! ## Feature Flags
|
||||
#![cfg_attr(
|
||||
all(doc, feature = "document-features"),
|
||||
doc = ::document_features::document_features!()
|
||||
)]
|
||||
#![cfg_attr(all(doc, feature = "document-features"), feature(doc_cfg))]
|
||||
// `unsafe_code` not forbidden because we need to interact with the libc
|
||||
#![deny(missing_docs, rust_2018_idioms, unsafe_code)]
|
||||
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
/// A way to specify how 'safe' we feel about a resource, typically about a git repository.
|
||||
#[derive(Copy, Clone, Ord, PartialOrd, PartialEq, Eq, Debug, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum Trust {
|
||||
/// Caution is warranted when using the resource.
|
||||
Reduced,
|
||||
/// We have no doubts that this resource means no harm and it can be used at will.
|
||||
Full,
|
||||
}
|
||||
|
||||
///
|
||||
pub mod trust;
|
||||
|
||||
/// Allow, deny or forbid using a resource or performing an action.
|
||||
#[derive(Debug, Copy, Clone, PartialOrd, PartialEq, Ord, Eq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum Permission {
|
||||
/// Fail outright when trying to load a resource or performing an action.
|
||||
Forbid,
|
||||
/// Ignore resources or try to avoid performing an operation.
|
||||
Deny,
|
||||
/// Allow loading a resource or performing an action.
|
||||
Allow,
|
||||
}
|
||||
|
||||
///
|
||||
pub mod permission;
|
||||
|
||||
bitflags::bitflags! {
|
||||
/// Whether something can be read or written.
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Debug)]
|
||||
pub struct ReadWrite: u8 {
|
||||
/// The item can be read.
|
||||
const READ = 1 << 0;
|
||||
/// The item can be written
|
||||
const WRITE = 1 << 1;
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ReadWrite {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
std::fmt::Debug::fmt(self, f)
|
||||
}
|
||||
}
|
||||
|
||||
/// Various types to identify entities.
|
||||
pub mod identity;
|
||||
54
src-sec/src/permission.rs
Normal file
54
src-sec/src/permission.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
use std::fmt::{Debug, Display, Formatter};
|
||||
|
||||
use crate::Permission;
|
||||
|
||||
/// An error to use if an operation cannot proceed due to insufficient permissions.
|
||||
///
|
||||
/// It's up to the implementation to decide which permission is required for an operation, and which one
|
||||
/// causes errors.
|
||||
#[derive(Debug)]
|
||||
pub struct Error<R: std::fmt::Debug> {
|
||||
/// The resource which cannot be used.
|
||||
pub resource: R,
|
||||
}
|
||||
|
||||
impl<R> Display for Error<R>
|
||||
where
|
||||
R: std::fmt::Debug,
|
||||
{
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"Not allowed to handle resource {:?}: permission denied",
|
||||
self.resource
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> std::error::Error for Error<R> where R: std::fmt::Debug {}
|
||||
|
||||
impl Permission {
|
||||
/// Return true if this instance is `Permission::Allow`.
|
||||
pub fn is_allowed(&self) -> bool {
|
||||
matches!(self, Permission::Allow)
|
||||
}
|
||||
/// Check this permissions and produce a reply to indicate if the `resource` can be used and in which way.
|
||||
///
|
||||
/// Only if this permission is set to `Allow` will the resource be usable.
|
||||
pub fn check<R: std::fmt::Debug>(&self, resource: R) -> Result<Option<R>, Error<R>> {
|
||||
match self {
|
||||
Permission::Allow => Ok(Some(resource)),
|
||||
Permission::Deny => Ok(None),
|
||||
Permission::Forbid => Err(Error { resource }),
|
||||
}
|
||||
}
|
||||
|
||||
/// Like [`check()`][Self::check()], but degenerates the type to an option to make it more useful in cases where
|
||||
/// `Forbid` shouldn't abort the entire operation.
|
||||
pub fn check_opt<R: std::fmt::Debug>(&self, resource: R) -> Option<R> {
|
||||
match self {
|
||||
Permission::Allow => Some(resource),
|
||||
Permission::Deny | Permission::Forbid => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
56
src-sec/src/trust.rs
Normal file
56
src-sec/src/trust.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
use crate::Trust;
|
||||
|
||||
impl Trust {
|
||||
/// Derive `Full` trust if `path` is owned by the user executing the current process, or `Reduced` trust otherwise.
|
||||
pub fn from_path_ownership(path: &std::path::Path) -> std::io::Result<Self> {
|
||||
Ok(if crate::identity::is_path_owned_by_current_user(path)? {
|
||||
Trust::Full
|
||||
} else {
|
||||
Trust::Reduced
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait to help creating default values based on a trust level.
|
||||
pub trait DefaultForLevel {
|
||||
/// Produce a default value for the given trust `level`.
|
||||
fn default_for_level(level: Trust) -> Self;
|
||||
}
|
||||
|
||||
/// Associate instructions for how to deal with various `Trust` levels as they are encountered in the wild.
|
||||
pub struct Mapping<T> {
|
||||
/// The value for fully trusted resources.
|
||||
pub full: T,
|
||||
/// The value for resources with reduced trust.
|
||||
pub reduced: T,
|
||||
}
|
||||
|
||||
impl<T> Default for Mapping<T>
|
||||
where
|
||||
T: DefaultForLevel,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Mapping {
|
||||
full: T::default_for_level(Trust::Full),
|
||||
reduced: T::default_for_level(Trust::Reduced),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Mapping<T> {
|
||||
/// Obtain the value for the given trust `level`.
|
||||
pub fn by_level(&self, level: Trust) -> &T {
|
||||
match level {
|
||||
Trust::Full => &self.full,
|
||||
Trust::Reduced => &self.reduced,
|
||||
}
|
||||
}
|
||||
|
||||
/// Obtain the value for the given `level` once.
|
||||
pub fn into_value_by_level(self, level: Trust) -> T {
|
||||
match level {
|
||||
Trust::Full => self.full,
|
||||
Trust::Reduced => self.reduced,
|
||||
}
|
||||
}
|
||||
}
|
||||
17
src-sec/tests/sec/identity.rs
Normal file
17
src-sec/tests/sec/identity.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
#[test]
|
||||
fn is_path_owned_by_current_user() -> crate::Result {
|
||||
let dir = tempfile::tempdir()?;
|
||||
let file = dir.path().join("file");
|
||||
std::fs::write(&file, [])?;
|
||||
assert!(gix_sec::identity::is_path_owned_by_current_user(&file)?);
|
||||
assert!(gix_sec::identity::is_path_owned_by_current_user(dir.path())?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(windows)]
|
||||
fn windows_home() -> crate::Result {
|
||||
let home = gix_path::env::home_dir().expect("home dir is available");
|
||||
assert!(gix_sec::identity::is_path_owned_by_current_user(&home)?);
|
||||
Ok(())
|
||||
}
|
||||
37
src-sec/tests/sec/main.rs
Normal file
37
src-sec/tests/sec/main.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
pub type Result<T = ()> = std::result::Result<T, Box<dyn std::error::Error>>;
|
||||
|
||||
mod trust {
|
||||
use gix_sec::Trust;
|
||||
|
||||
#[test]
|
||||
fn ordering() {
|
||||
assert!(Trust::Reduced < Trust::Full);
|
||||
}
|
||||
}
|
||||
|
||||
mod permission {
|
||||
use gix_sec::Permission;
|
||||
|
||||
#[test]
|
||||
fn check() {
|
||||
assert_eq!(Permission::Allow.check("hi").unwrap(), Some("hi"));
|
||||
assert_eq!(Permission::Deny.check("hi").unwrap(), None);
|
||||
assert!(Permission::Forbid.check("hi").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_opt() {
|
||||
assert_eq!(Permission::Allow.check_opt("hi"), Some("hi"));
|
||||
assert_eq!(Permission::Deny.check_opt("hi"), None);
|
||||
assert_eq!(Permission::Forbid.check_opt("hi"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_allowed() {
|
||||
assert!(Permission::Allow.is_allowed());
|
||||
assert!(!Permission::Deny.is_allowed());
|
||||
assert!(!Permission::Forbid.is_allowed());
|
||||
}
|
||||
}
|
||||
|
||||
mod identity;
|
||||
Reference in New Issue
Block a user