create src

This commit is contained in:
awfixer
2026-03-11 02:04:19 -07:00
commit 52f7a22bf2
2595 changed files with 402870 additions and 0 deletions

1274
src-sec/CHANGELOG.md Normal file

File diff suppressed because it is too large Load Diff

48
src-sec/Cargo.toml Normal file
View 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
View File

@@ -0,0 +1 @@
../LICENSE-APACHE

1
src-sec/LICENSE-MIT Symbolic link
View File

@@ -0,0 +1 @@
../LICENSE-MIT

224
src-sec/src/identity.rs Normal file
View 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
View 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
View 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
View 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,
}
}
}

View 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
View 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;