diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bd53312ffa3..35935f00326 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,7 +61,7 @@ jobs: uses: actions-rs/cargo@v1 with: command: install - args: "gitoxide cargo-smart-release" + args: "--force gitoxide cargo-smart-release" lint: runs-on: ubuntu-latest diff --git a/Cargo.lock b/Cargo.lock index 40b0c62f58c..736915da01a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1121,6 +1121,7 @@ dependencies = [ "criterion", "dirs", "git-features", + "git-sec", "memchr", "nom", "pwd", @@ -1143,6 +1144,16 @@ dependencies = [ "unicase", ] +[[package]] +name = "git-credentials" +version = "0.1.0" +dependencies = [ + "bstr", + "git-sec", + "quick-error", + "serde", +] + [[package]] name = "git-date" version = "0.0.0" @@ -1351,6 +1362,7 @@ dependencies = [ "document-features", "futures-io", "futures-lite", + "git-credentials", "git-features", "git-hash", "git-packetline", @@ -1401,6 +1413,7 @@ dependencies = [ "document-features", "git-actor", "git-config", + "git-credentials", "git-diff", "git-features", "git-glob", @@ -1414,6 +1427,7 @@ dependencies = [ "git-protocol", "git-ref", "git-revision", + "git-sec", "git-tempfile", "git-testtools", "git-transport", @@ -1442,6 +1456,17 @@ dependencies = [ "thiserror", ] +[[package]] +name = "git-sec" +version = "0.1.0" +dependencies = [ + "bitflags", + "libc", + "serde", + "tempfile", + "windows", +] + [[package]] name = "git-submodule" version = "0.0.0" @@ -1498,6 +1523,7 @@ dependencies = [ "git-hash", "git-pack", "git-packetline", + "git-sec", "git-url", "maybe-async", "pin-project-lite", @@ -1829,9 +1855,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.122" +version = "0.2.123" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec647867e2bf0772e28c8bcde4f0d19a9216916e890543b5a03ed8ef27b8f259" +checksum = "cb691a747a7ab48abc15c5b42066eaafde10dc427e3b6ee2a1cf43db04c763bd" [[package]] name = "libgit2-sys" @@ -3123,17 +3149,30 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08746b4b7ac95f708b3cccceb97b7f9a21a8916dd47fc99b0e6aaf7208f26fd7" +dependencies = [ + "windows_aarch64_msvc 0.35.0", + "windows_i686_gnu 0.35.0", + "windows_i686_msvc 0.35.0", + "windows_x86_64_gnu 0.35.0", + "windows_x86_64_msvc 0.35.0", +] + [[package]] name = "windows-sys" version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5acdd78cb4ba54c0045ac14f62d8f94a03d10047904ae2a40afa1e99d8f70825" dependencies = [ - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_msvc", + "windows_aarch64_msvc 0.34.0", + "windows_i686_gnu 0.34.0", + "windows_i686_msvc 0.34.0", + "windows_x86_64_gnu 0.34.0", + "windows_x86_64_msvc 0.34.0", ] [[package]] @@ -3142,30 +3181,60 @@ version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17cffbe740121affb56fad0fc0e421804adf0ae00891205213b5cecd30db881d" +[[package]] +name = "windows_aarch64_msvc" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3bc5134e8ce0da5d64dcec3529793f1d33aee5a51fc2b4662e0f881dd463e6" + [[package]] name = "windows_i686_gnu" version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2564fde759adb79129d9b4f54be42b32c89970c18ebf93124ca8870a498688ed" +[[package]] +name = "windows_i686_gnu" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0343a6f35bf43a07b009b8591b78b10ea03de86b06f48e28c96206cd0f453b50" + [[package]] name = "windows_i686_msvc" version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cd9d32ba70453522332c14d38814bceeb747d80b3958676007acadd7e166956" +[[package]] +name = "windows_i686_msvc" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1acdcbf4ca63d8e7a501be86fee744347186275ec2754d129ddeab7a1e3a02e4" + [[package]] name = "windows_x86_64_gnu" version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfce6deae227ee8d356d19effc141a509cc503dfd1f850622ec4b0f84428e1f4" +[[package]] +name = "windows_x86_64_gnu" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "893c0924c5a990ec73cd2264d1c0cba1773a929e1a3f5dbccffd769f8c4edebb" + [[package]] name = "windows_x86_64_msvc" version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d19538ccc21819d01deaf88d6a17eae6596a12e9aafdbb97916fb49896d89de9" +[[package]] +name = "windows_x86_64_msvc" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a29bd61f32889c822c99a8fdf2e93378bd2fae4d7efd2693fab09fcaaf7eff4b" + [[package]] name = "xz2" version = "0.1.6" diff --git a/Cargo.toml b/Cargo.toml index 2e6b663e8fe..3a1285979bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -146,8 +146,10 @@ members = [ "git-packetline", "git-mailmap", "git-note", + "git-sec", "git-submodule", "git-transport", + "git-credentials", "git-protocol", "git-pack", "git-odb", diff --git a/Makefile b/Makefile index 11a4416f7ca..fa713ab6adc 100644 --- a/Makefile +++ b/Makefile @@ -89,6 +89,8 @@ check: ## Build all code in suitable configurations cd git-object && cargo check --all-features \ && cargo check --features verbose-object-parsing-errors cd git-index && cargo check --features serde1 + cd git-credentials && cargo check --features serde1 + cd git-sec && cargo check --features serde1 cd git-revision && cargo check --features serde1 cd git-attributes && cargo check --features serde1 cd git-glob && cargo check --features serde1 diff --git a/README.md b/README.md index a8bb132e47c..23842349b53 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,8 @@ Crates that seem feature complete and need to see some more use before they can * [git-attributes](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-attributes) * [git-quote](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-quote) * **idea** + * [git-credentials](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-credentials) + * [git-sec](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-sec) * [git-note](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-note) * [git-date](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-date) * [git-pathspec](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-pathspec) diff --git a/crate-status.md b/crate-status.md index ae3bf0330dc..c996cc28d84 100644 --- a/crate-status.md +++ b/crate-status.md @@ -233,6 +233,13 @@ A mechanism to associate metadata with any object, and keep revisions of it usin ### git-date * [ ] parse git dates + +### git-credentials +* [x] launch git credentials helpers with a given action + +### git-sec + +Provides a trust model to share across gitoxide crates. It helps configuring how to interact with external processes, among other things. ### git-glob * [x] parse pattern diff --git a/etc/check-package-size.sh b/etc/check-package-size.sh index 605ed86da73..9fd9bf6d429 100755 --- a/etc/check-package-size.sh +++ b/etc/check-package-size.sh @@ -4,7 +4,7 @@ set -eu -o pipefail function enter () { local dir="${1:?need directory to enter}" - echo $' in' $dir + echo -n $' in' $dir $'\t→\t' cd $dir } @@ -36,6 +36,11 @@ echo "in root: gitoxide CLI" (enter git-traverse && indent cargo diet -n --package-size-limit 10KB) (enter git-url && indent cargo diet -n --package-size-limit 15KB) (enter git-validate && indent cargo diet -n --package-size-limit 5KB) +(enter git-date && indent cargo diet -n --package-size-limit 5KB) +(enter git-note && indent cargo diet -n --package-size-limit 5KB) +(enter git-sec && indent cargo diet -n --package-size-limit 5KB) +(enter git-tix && indent cargo diet -n --package-size-limit 5KB) +(enter git-credentials && indent cargo diet -n --package-size-limit 5KB) (enter git-object && indent cargo diet -n --package-size-limit 25KB) (enter git-commitgraph && indent cargo diet -n --package-size-limit 25KB) (enter git-pack && indent cargo diet -n --package-size-limit 115KB) diff --git a/git-config/Cargo.toml b/git-config/Cargo.toml index f250dba80c2..18a5d9dd077 100644 --- a/git-config/Cargo.toml +++ b/git-config/Cargo.toml @@ -15,6 +15,8 @@ include = ["src/**/*", "LICENSE-*", "README.md", "CHANGELOG.md"] [dependencies] git-features = { version = "^0.20.0", path = "../git-features"} +git-sec = { version = "^0.1.0", path = "../git-sec" } + dirs = "4" nom = { version = "7", default_features = false, features = [ "std" ] } memchr = "2" diff --git a/git-config/src/lib.rs b/git-config/src/lib.rs index cbb883e3162..238b250344f 100644 --- a/git-config/src/lib.rs +++ b/git-config/src/lib.rs @@ -59,12 +59,65 @@ pub mod fs; pub mod parser; pub mod values; -// mod de; -// mod ser; -// mod error; -// pub use de::{from_str, Deserializer}; -// pub use error::{Error, Result}; -// pub use ser::{to_string, Serializer}; +mod permissions { + use crate::Permissions; + + impl Permissions { + /// Allow everything which usually relates to a fully trusted environment + pub fn all() -> Self { + use git_sec::Permission::*; + Permissions { + system: Allow, + global: Allow, + user: Allow, + repository: Allow, + worktree: Allow, + env: Allow, + includes: Allow, + } + } + + /// If in doubt, this configuration can be used to safely load configuration from sources which is usually trusted, + /// that is system and user configuration. Do load any configuration that isn't trusted as it's now owned by the current user. + pub fn secure() -> Self { + use git_sec::Permission::*; + Permissions { + system: Allow, + global: Allow, + user: Allow, + repository: Deny, + worktree: Deny, + env: Allow, + includes: Deny, + } + } + } +} + +/// Configure security relevant options when loading a git configuration. +#[derive(Copy, Clone, Ord, PartialOrd, PartialEq, Eq, Debug, Hash)] +#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] +pub struct Permissions { + /// How to use the system configuration. + /// This is defined as `$(prefix)/etc/gitconfig` on unix. + pub system: git_sec::Permission, + /// How to use the global configuration. + /// This is usually `~/.gitconfig`. + pub global: git_sec::Permission, + /// How to use the user configuration. + /// Second user-specific configuration path; if `$XDG_CONFIG_HOME` is not + /// set or empty, `$HOME/.config/git/config` will be used. + pub user: git_sec::Permission, + /// How to use the repository configuration. + pub repository: git_sec::Permission, + /// How to use worktree configuration from `config.worktree`. + // TODO: figure out how this really applies and provide more information here. + pub worktree: git_sec::Permission, + /// How to use the configuration from environment variables. + pub env: git_sec::Permission, + /// What to do when include files are encountered in loaded configuration. + pub includes: git_sec::Permission, +} #[cfg(test)] pub mod test_util; diff --git a/git-credentials/CHANGELOG.md b/git-credentials/CHANGELOG.md new file mode 100644 index 00000000000..2e51d6646ec --- /dev/null +++ b/git-credentials/CHANGELOG.md @@ -0,0 +1,29 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## 0.0.0 (2022-04-15) + +An empty crate without any content to reserve the name for the gitoxide project. + +### Commit Statistics + + + + - 1 commit contributed to the release. + - 0 commits where understood as [conventional](https://www.conventionalcommits.org). + - 1 unique issue was worked on: [#386](https://github.com/Byron/gitoxide/issues/386) + +### Commit Details + + + +
view details + + * **[#386](https://github.com/Byron/gitoxide/issues/386)** + - add frame for git-credentials crate ([`be7a9cf`](https://github.com/Byron/gitoxide/commit/be7a9cf776f958ac7228457bb4e1415f86f8e575)) +
+ diff --git a/git-credentials/Cargo.toml b/git-credentials/Cargo.toml new file mode 100644 index 00000000000..a1dc3c665c1 --- /dev/null +++ b/git-credentials/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "git-credentials" +version = "0.1.0" +repository = "https://github.com/Byron/gitoxide" +license = "MIT/Apache-2.0" +description = "A WIP crate of the gitoxide project to interact with git credentials helpers" +authors = ["Sebastian Thiel "] +edition = "2018" + +[lib] +doctest = false + +[features] +## Data structures implement `serde::Serialize` and `serde::Deserialize`. +serde1 = ["serde", "bstr/serde1", "git-sec/serde1"] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +git-sec = { version = "^0.1.0", path = "../git-sec" } +quick-error = "2.0.0" +serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"] } +bstr = { version = "0.2.13", default-features = false, features = ["std"]} + +[package.metadata.docs.rs] +all-features = true diff --git a/git-protocol/src/credentials.rs b/git-credentials/src/helper.rs similarity index 67% rename from git-protocol/src/credentials.rs rename to git-credentials/src/helper.rs index d2db3f75250..32a10c90960 100644 --- a/git-protocol/src/credentials.rs +++ b/git-credentials/src/helper.rs @@ -3,14 +3,13 @@ use std::{ process::{Command, Stdio}, }; -use git_transport::client; use quick_error::quick_error; -/// The result used in [`helper()`]. +/// The result used in [`action()`]. pub type Result = std::result::Result, Error>; quick_error! { - /// The error used in the [credentials helper][helper()]. + /// The error used in the [credentials helper][action()]. #[derive(Debug)] #[allow(missing_docs)] pub enum Error { @@ -28,7 +27,7 @@ quick_error! { } } -/// The action to perform by the credentials [`helper()`]. +/// The action to perform by the credentials [`action()`]. #[derive(Clone, Debug)] pub enum Action<'a> { /// Provide credentials using the given repository URL (as &str) as context. @@ -69,11 +68,11 @@ impl NextAction { } } -/// The outcome of [`helper()`]. +/// The outcome of [`action()`]. pub struct Outcome { /// The obtained identity. - pub identity: client::Identity, - /// A handle to the action to perform next using another call to [`helper()`]. + pub identity: git_sec::identity::Account, + /// A handle to the action to perform next using another call to [`action()`]. pub next: NextAction, } @@ -87,11 +86,13 @@ fn git_program() -> &'static str { "git" } +// TODO(sec): reimplement helper execution so it won't use the `git credential` anymore to allow enforcing our own security model. +// Currently we support more flexible configuration than downright not working at all. /// Call the `git` credentials helper program performing the given `action`. /// /// Usually the first call is performed with [`Action::Fill`] to obtain an identity, which subsequently can be used. /// On successful usage, use [`NextAction::approve()`], otherwise [`NextAction::reject()`]. -pub fn helper(action: Action<'_>) -> Result { +pub fn action(action: Action<'_>) -> Result { let mut cmd = Command::new(git_program()); cmd.arg("credential") .arg(action.as_str()) @@ -128,7 +129,7 @@ pub fn helper(action: Action<'_>) -> Result { .map(|(_, n)| n.to_owned()) }; Ok(Some(Outcome { - identity: client::Identity::Account { + identity: git_sec::identity::Account { username: find("username")?, password: find("password")?, }, @@ -173,3 +174,70 @@ pub fn decode_message(mut input: impl io::Read) -> io::Result>>() } + +#[cfg(test)] +mod tests { + use super::*; + type Result = std::result::Result<(), Box>; + + mod encode_message { + use super::*; + use bstr::ByteSlice; + + #[test] + fn from_url() -> super::Result { + let mut out = Vec::new(); + encode_message("https://github.com/byron/gitoxide", &mut out)?; + assert_eq!(out.as_bstr(), b"url=https://github.com/byron/gitoxide\n\n".as_bstr()); + Ok(()) + } + + mod invalid { + use super::*; + use std::io; + + #[test] + fn contains_null() { + assert_eq!( + encode_message("https://foo\u{0}", Vec::new()).err().map(|e| e.kind()), + Some(io::ErrorKind::Other) + ); + } + #[test] + fn contains_newline() { + assert_eq!( + encode_message("https://foo\n", Vec::new()).err().map(|e| e.kind()), + Some(io::ErrorKind::Other) + ); + } + } + } + + mod decode_message { + use super::*; + + #[test] + fn typical_response() -> super::Result { + assert_eq!( + decode_message( + "protocol=https +host=example.com +username=bob +password=secr3t\n\n +this=is-skipped-past-empty-line" + .as_bytes() + )?, + vec![ + ("protocol", "https"), + ("host", "example.com"), + ("username", "bob"), + ("password", "secr3t") + ] + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect::>() + ); + Ok(()) + } + } +} diff --git a/git-credentials/src/lib.rs b/git-credentials/src/lib.rs new file mode 100644 index 00000000000..b6ff3d34fb1 --- /dev/null +++ b/git-credentials/src/lib.rs @@ -0,0 +1,7 @@ +#![forbid(unsafe_code)] +#![deny(missing_docs, rust_2018_idioms)] +//! Interact with git credentials in various ways and launch helper programs. + +/// +pub mod helper; +pub use helper::action as helper; diff --git a/git-protocol/Cargo.toml b/git-protocol/Cargo.toml index def322526c9..9bef72410f5 100644 --- a/git-protocol/Cargo.toml +++ b/git-protocol/Cargo.toml @@ -42,6 +42,7 @@ required-features = ["async-client"] git-features = { version = "^0.20.0", path = "../git-features", features = ["progress"] } git-transport = { version = "^0.16.0", path = "../git-transport" } git-hash = { version = "^0.9.3", path = "../git-hash" } +git-credentials = { version = "^0.1.0", path = "../git-credentials" } quick-error = "2.0.0" serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"]} diff --git a/git-protocol/src/fetch/error.rs b/git-protocol/src/fetch/error.rs index 6c401e7eb1b..5459ffc9f35 100644 --- a/git-protocol/src/fetch/error.rs +++ b/git-protocol/src/fetch/error.rs @@ -18,7 +18,7 @@ quick_error! { from() source(err) } - Credentials(err: credentials::Error) { + Credentials(err: credentials::helper::Error) { display("Failed to obtain, approve or reject credentials") from() source(err) diff --git a/git-protocol/src/fetch/tests/arguments.rs b/git-protocol/src/fetch/tests/arguments.rs index 874f2469677..c2c102246b7 100644 --- a/git-protocol/src/fetch/tests/arguments.rs +++ b/git-protocol/src/fetch/tests/arguments.rs @@ -20,14 +20,14 @@ struct Transport { mod impls { use git_transport::{ client, - client::{Error, Identity, MessageKind, RequestWriter, SetServiceResponse, WriteMode}, + client::{Error, MessageKind, RequestWriter, SetServiceResponse, WriteMode}, Protocol, Service, }; use crate::fetch::tests::arguments::Transport; impl client::TransportWithoutIO for Transport { - fn set_identity(&mut self, identity: Identity) -> Result<(), Error> { + fn set_identity(&mut self, identity: client::Account) -> Result<(), Error> { self.inner.set_identity(identity) } @@ -64,13 +64,13 @@ mod impls { use async_trait::async_trait; use git_transport::{ client, - client::{Error, Identity, MessageKind, RequestWriter, SetServiceResponse, WriteMode}, + client::{Error, MessageKind, RequestWriter, SetServiceResponse, WriteMode}, Protocol, Service, }; use crate::fetch::tests::arguments::Transport; impl client::TransportWithoutIO for Transport { - fn set_identity(&mut self, identity: Identity) -> Result<(), Error> { + fn set_identity(&mut self, identity: client::Account) -> Result<(), Error> { self.inner.set_identity(identity) } diff --git a/git-protocol/src/fetch_fn.rs b/git-protocol/src/fetch_fn.rs index 9b6bdd4141e..a0597341257 100644 --- a/git-protocol/src/fetch_fn.rs +++ b/git-protocol/src/fetch_fn.rs @@ -58,7 +58,7 @@ pub async fn fetch( fetch_mode: FetchConnection, ) -> Result<(), Error> where - F: FnMut(credentials::Action<'_>) -> credentials::Result, + F: FnMut(credentials::helper::Action<'_>) -> credentials::helper::Result, D: Delegate, T: client::Transport, { @@ -85,8 +85,8 @@ where drop(result); // needed to workaround this: https://github.com/rust-lang/rust/issues/76149 let url = transport.to_url(); progress.set_name("authentication"); - let credentials::Outcome { identity, next } = - authenticate(credentials::Action::Fill(&url))?.expect("FILL provides an identity"); + let credentials::helper::Outcome { identity, next } = + authenticate(credentials::helper::Action::Fill(&url))?.expect("FILL provides an identity"); transport.set_identity(identity)?; progress.step(); progress.set_name("handshake (authenticated)"); diff --git a/git-protocol/src/lib.rs b/git-protocol/src/lib.rs index f62cb5c09a0..f346013ea92 100644 --- a/git-protocol/src/lib.rs +++ b/git-protocol/src/lib.rs @@ -10,11 +10,10 @@ #![deny(unsafe_code)] #![deny(rust_2018_idioms, missing_docs)] +pub use git_credentials as credentials; /// A convenience export allowing users of git-protocol to use the transport layer without their own cargo dependency. pub use git_transport as transport; -/// -pub mod credentials; /// #[cfg(any(feature = "blocking-client", feature = "async-client"))] pub mod fetch; diff --git a/git-protocol/tests/async-protocol.rs b/git-protocol/tests/async-protocol.rs index d6af99e9760..772e2bd386d 100644 --- a/git-protocol/tests/async-protocol.rs +++ b/git-protocol/tests/async-protocol.rs @@ -5,6 +5,5 @@ pub fn fixture_bytes(path: &str) -> Vec { .expect("fixture to be present and readable") } -mod credentials; mod fetch; mod remote_progress; diff --git a/git-protocol/tests/blocking-protocol.rs b/git-protocol/tests/blocking-protocol.rs index d6af99e9760..772e2bd386d 100644 --- a/git-protocol/tests/blocking-protocol.rs +++ b/git-protocol/tests/blocking-protocol.rs @@ -5,6 +5,5 @@ pub fn fixture_bytes(path: &str) -> Vec { .expect("fixture to be present and readable") } -mod credentials; mod fetch; mod remote_progress; diff --git a/git-protocol/tests/credentials/mod.rs b/git-protocol/tests/credentials/mod.rs deleted file mode 100644 index 1d7e6ec344b..00000000000 --- a/git-protocol/tests/credentials/mod.rs +++ /dev/null @@ -1,86 +0,0 @@ -mod encode_message { - use bstr::ByteSlice; - use git_protocol::credentials; - - #[test] - fn from_url() -> crate::Result { - let mut out = Vec::new(); - credentials::encode_message("https://github.com/byron/gitoxide", &mut out)?; - assert_eq!(out.as_bstr(), b"url=https://github.com/byron/gitoxide\n\n".as_bstr()); - Ok(()) - } - - mod invalid { - use std::io; - - use git_protocol::credentials; - - #[test] - fn contains_null() { - assert_eq!( - credentials::encode_message("https://foo\u{0}", Vec::new()) - .err() - .map(|e| e.kind()), - Some(io::ErrorKind::Other) - ); - } - #[test] - fn contains_newline() { - assert_eq!( - credentials::encode_message("https://foo\n", Vec::new()) - .err() - .map(|e| e.kind()), - Some(io::ErrorKind::Other) - ); - } - } -} - -mod decode_message { - use git_protocol::credentials; - - #[test] - fn typical_response() -> crate::Result { - assert_eq!( - credentials::decode_message( - "protocol=https -host=example.com -username=bob -password=secr3t\n\n -this=is-skipped-past-empty-line" - .as_bytes() - )?, - vec![ - ("protocol", "https"), - ("host", "example.com"), - ("username", "bob"), - ("password", "secr3t") - ] - .iter() - .map(|(k, v)| (k.to_string(), v.to_string())) - .collect::>() - ); - Ok(()) - } - - mod invalid { - use std::io; - - use git_protocol::credentials; - - #[test] - fn null_in_key() -> crate::Result { - assert_eq!( - credentials::decode_message( - "protocol=https -host=examp\0le.com" - .as_bytes() - ) - .err() - .map(|e| e.kind()), - Some(io::ErrorKind::Other), - ); - Ok(()) - } - } -} diff --git a/git-repository/Cargo.toml b/git-repository/Cargo.toml index f6bf517ed6f..c3865265e90 100644 --- a/git-repository/Cargo.toml +++ b/git-repository/Cargo.toml @@ -49,7 +49,7 @@ max-performance = ["git-features/parallel", "git-features/zlib-ng-compat", "git- local-time-support = ["git-actor/local-time-support"] ## Re-export stability tier 2 crates for convenience and make `Repository` struct fields with types from these crates publicly accessible. ## Doing so is less stable than the stability tier 1 that `git-repository` is a member of. -unstable = ["git-index", "git-worktree", "git-mailmap", "git-glob"] +unstable = ["git-index", "git-worktree", "git-mailmap", "git-glob", "git-credentials"] ## Print debugging information about usage of object database caches, useful for tuning cache sizes. cache-efficiency-debug = ["git-features/cache-efficiency-debug"] @@ -60,6 +60,7 @@ git-ref = { version = "^0.12.1", path = "../git-ref" } git-tempfile = { version = "^2.0.0", path = "../git-tempfile" } git-lock = { version = "^2.0.0", path = "../git-lock" } git-validate = { version ="^0.5.3", path = "../git-validate" } +git-sec = { version = "^0.1.0", path = "../git-sec" } git-config = { version = "^0.2.1", path = "../git-config" } git-odb = { version = "^0.28.0", path = "../git-odb" } @@ -79,6 +80,7 @@ git-features = { version = "^0.20.0", path = "../git-features", features = ["pro # unstable only git-glob = { version = "^0.2.0", path = "../git-glob", optional = true } +git-credentials = { version = "^0.1.0", path = "../git-credentials", optional = true } git-index = { version = "^0.2.0", path = "../git-index", optional = true } git-worktree = { version = "^0.1.0", path = "../git-worktree", optional = true } diff --git a/git-repository/src/lib.rs b/git-repository/src/lib.rs index 54c29e8260e..7303cd73e19 100644 --- a/git-repository/src/lib.rs +++ b/git-repository/src/lib.rs @@ -89,6 +89,8 @@ //! * [`bstr`][bstr] //! * [`index`] //! * [`glob`] +//! * [`credentials`] +//! * [`sec`] //! * [`worktree`] //! * [`mailmap`] //! * [`objs`] @@ -122,6 +124,8 @@ use std::path::PathBuf; // This also means that their major version changes affect our major version, but that's alright as we directly expose their // APIs/instances anyway. pub use git_actor as actor; +#[cfg(all(feature = "unstable", feature = "git-credentials"))] +pub use git_credentials as credentials; #[cfg(all(feature = "unstable", feature = "git-diff"))] pub use git_diff as diff; use git_features::threading::OwnShared; @@ -142,6 +146,7 @@ pub use git_odb as odb; pub use git_protocol as protocol; pub use git_ref as refs; pub use git_revision as revision; +pub use git_sec as sec; #[cfg(feature = "unstable")] pub use git_tempfile as tempfile; #[cfg(feature = "unstable")] @@ -179,7 +184,7 @@ pub(crate) type Config = OwnShared>; /// A repository path which either points to a work tree or the `.git` repository itself. #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub enum Path { - /// The currently checked out or nascent work tree of a git repositore + /// The currently checked out or nascent work tree of a git repository WorkTree(PathBuf), /// The git repository itself Repository(PathBuf), @@ -195,6 +200,7 @@ pub mod id; pub mod object; pub mod reference; mod repository; +pub use repository::{permissions, permissions::Permissions}; pub mod tag; /// The kind of `Repository` @@ -277,7 +283,8 @@ pub mod rev_parse { /// pub mod init { - use std::{convert::TryInto, path::Path}; + use crate::ThreadSafeRepository; + use std::path::Path; /// The error returned by [`crate::init()`]. #[derive(Debug, thiserror::Error)] @@ -295,33 +302,51 @@ pub mod init { /// Fails without action if there is already a `.git` repository inside of `directory`, but /// won't mind if the `directory` otherwise is non-empty. pub fn init(directory: impl AsRef, kind: crate::Kind) -> Result { + use git_sec::trust::DefaultForLevel; let path = crate::path::create::into(directory.as_ref(), kind)?; - Ok(path.try_into()?) + let (git_dir, worktree_dir) = path.into_repository_and_work_tree_directories(); + ThreadSafeRepository::open_from_paths( + git_dir, + worktree_dir, + crate::open::Options::default_for_level(git_sec::Trust::Full), + ) + .map_err(Into::into) } } } /// pub mod discover { - use std::{convert::TryInto, path::Path}; - - use crate::path::discover; + use crate::{path, ThreadSafeRepository}; + use std::path::Path; /// The error returned by [`crate::discover()`]. #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] pub enum Error { #[error(transparent)] - Discover(#[from] discover::existing::Error), + Discover(#[from] path::discover::Error), #[error(transparent)] Open(#[from] crate::open::Error), } - impl crate::ThreadSafeRepository { + impl ThreadSafeRepository { /// Try to open a git repository in `directory` and search upwards through its parents until one is found. pub fn discover(directory: impl AsRef) -> Result { - let path = discover::existing(directory)?; - Ok(path.try_into()?) + Self::discover_opts(directory, Default::default(), Default::default()) + } + + /// Try to open a git repository in `directory` and search upwards through its parents until one is found, + /// while applying `options`. Then use the `trust_map` to determine which of our own repository options to use + /// for instantiations. + pub fn discover_opts( + directory: impl AsRef, + options: crate::path::discover::Options, + trust_map: git_sec::trust::Mapping, + ) -> Result { + let (path, trust) = path::discover_opts(directory, options)?; + let (git_dir, worktree_dir) = path.into_repository_and_work_tree_directories(); + Self::open_from_paths(git_dir, worktree_dir, trust_map.into_value_by_level(trust)).map_err(Into::into) } } } diff --git a/git-repository/src/open.rs b/git-repository/src/open.rs index f52b0aeafed..0312fe30814 100644 --- a/git-repository/src/open.rs +++ b/git-repository/src/open.rs @@ -1,6 +1,8 @@ use std::path::PathBuf; +use crate::Permissions; use git_features::threading::OwnShared; +use git_sec::Trust; /// A way to configure the usage of replacement objects, see `git replace`. pub enum ReplacementObjects { @@ -61,6 +63,7 @@ impl ReplacementObjects { pub struct Options { object_store_slots: git_odb::store::init::Slots, replacement_objects: ReplacementObjects, + permissions: crate::Permissions, } impl Options { @@ -78,12 +81,36 @@ impl Options { self } + // TODO: tests + /// Set the given permissions, which are typically derived by a `Trust` level. + pub fn permissions(mut self, permissions: crate::Permissions) -> Self { + self.permissions = permissions; + self + } + /// Open a repository at `path` with the options set so far. pub fn open(self, path: impl Into) -> Result { crate::ThreadSafeRepository::open_opts(path, self) } } +impl git_sec::trust::DefaultForLevel for Options { + fn default_for_level(level: Trust) -> Self { + match level { + git_sec::Trust::Full => Options { + object_store_slots: Default::default(), + replacement_objects: Default::default(), + permissions: Permissions::all(), + }, + git_sec::Trust::Reduced => Options { + object_store_slots: git_odb::store::init::Slots::Given(32), // limit resource usage + replacement_objects: ReplacementObjects::Disable, // don't be tricked into seeing manufactured objects + permissions: Default::default(), + }, + } + } +} + /// The error returned by [`crate::open()`]. #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] @@ -94,6 +121,8 @@ pub enum Error { NotARepository(#[from] crate::path::is::Error), #[error(transparent)] ObjectStoreInitialization(#[from] std::io::Error), + #[error("The git directory at '{}' is considered unsafe as it's not owned by the current user.", .path.display())] + UnsafeGitDir { path: std::path::PathBuf }, } impl crate::ThreadSafeRepository { @@ -102,8 +131,9 @@ impl crate::ThreadSafeRepository { Self::open_opts(path, Options::default()) } - /// Open a git repository at the given `path`, possibly expanding it to `path/.git` if `path` is a work tree dir. - fn open_opts(path: impl Into, options: Options) -> Result { + /// Open a git repository at the given `path`, possibly expanding it to `path/.git` if `path` is a work tree dir, and use + /// `options` for fine-grained control. + pub fn open_opts(path: impl Into, options: Options) -> Result { let path = path.into(); let (path, kind) = match crate::path::is::git(&path) { Ok(kind) => (path, kind), @@ -123,15 +153,25 @@ impl crate::ThreadSafeRepository { Options { object_store_slots, replacement_objects, + permissions, }: Options, ) -> Result { - let mut config = crate::config::Cache::new(&git_dir)?; + if *permissions.git_dir != git_sec::ReadWrite::all() { + // TODO: respect `save.directory`, which needs more support from git-config to do properly. + return Err(Error::UnsafeGitDir { path: git_dir }); + } + // TODO: assure we handle the worktree-dir properly as we can have config per worktree with an extension. + // This would be something read in later as have to first check for extensions. Also this means + // that each worktree, even if accessible through this instance, has to come in its own Repository instance + // as it may have its own configuration. That's fine actually. + let config = crate::config::Cache::new(&git_dir)?; match worktree_dir { None if !config.is_bare => { worktree_dir = Some(git_dir.parent().expect("parent is always available").to_owned()); } Some(_) => { - config.is_bare = false; + // note that we might be bare even with a worktree directory - work trees don't have to be + // the parent of a non-bare repository. } None => {} } diff --git a/git-repository/src/path/discover.rs b/git-repository/src/path/discover.rs index 0b4e1cff310..1de4b2da8ed 100644 --- a/git-repository/src/path/discover.rs +++ b/git-repository/src/path/discover.rs @@ -1,107 +1,167 @@ -//! -use std::{ - borrow::Cow, - path::{Component, Path}, -}; +use std::path::PathBuf; -use crate::path; +/// The error returned by [path::discover::existing()][super::existing()]. +#[derive(Debug, thiserror::Error)] +#[allow(missing_docs)] +pub enum Error { + #[error("Failed to access a directory, or path is not a directory: '{}'", .path.display())] + InaccessibleDirectory { path: PathBuf }, + #[error("Could find a git repository in '{}' or in any of its parents", .path.display())] + NoGitRepository { path: PathBuf }, + #[error("Could not determine trust level for path '{}'.", .path.display())] + CheckTrust { + path: PathBuf, + #[source] + err: std::io::Error, + }, +} -/// -pub mod existing { - use std::path::PathBuf; +/// Options to help guide the [discovery][function::discover()] of repositories, along with their options +/// when instantiated. +pub struct Options { + /// When discovering a repository, assure it has at least this trust level or ignore it otherwise. + /// + /// This defaults to [`Reduced`][git_sec::Trust::Reduced] as our default settings are geared towards avoiding abuse. + /// Set it to `Full` to only see repositories that [are owned by the current user][git_sec::Trust::from_path_ownership()]. + pub required_trust: git_sec::Trust, +} - /// The error returned by [path::discover::existing()][super::existing()]. - #[derive(Debug, thiserror::Error)] - #[allow(missing_docs)] - pub enum Error { - #[error("Failed to access a directory, or path is not a directory: '{}'", .path.display())] - InaccessibleDirectory { path: PathBuf }, - #[error("Could find a git repository in '{}' or in any of its parents", .path.display())] - NoGitRepository { path: PathBuf }, +impl Default for Options { + fn default() -> Self { + Options { + required_trust: git_sec::Trust::Reduced, + } } } -/// Find the location of the git repository directly in `directory` or in any of its parent directories. -/// -/// Fail if no valid-looking git repository could be found. -pub fn existing(directory: impl AsRef) -> Result { - // Canonicalize the path so that `Path::parent` _actually_ gives - // us the parent directory. (`Path::parent` just strips off the last - // path component, which means it will not do what you expect when - // working with paths paths that contain '..'.) - let directory = maybe_canonicalize(directory.as_ref()).map_err(|_| existing::Error::InaccessibleDirectory { - path: directory.as_ref().into(), - })?; - if !directory.is_dir() { - return Err(existing::Error::InaccessibleDirectory { - path: directory.into_owned(), - }); - } +pub(crate) mod function { + use super::{Error, Options}; + use git_sec::Trust; + use std::{ + borrow::Cow, + path::{Component, Path}, + }; - let mut cursor: &Path = &directory; - loop { - if let Ok(kind) = path::is::git(cursor) { - break Ok(crate::Path::from_dot_git_dir(cursor, kind)); - } - let git_dir = cursor.join(".git"); - if let Ok(kind) = path::is::git(&git_dir) { - break Ok(crate::Path::from_dot_git_dir(git_dir, kind)); + use crate::path; + + /// Find the location of the git repository directly in `directory` or in any of its parent directories and provide + /// an associated Trust level by looking at the git directory's ownership, and control discovery using `options`. + /// + /// Fail if no valid-looking git repository could be found. + // TODO: tests for trust-based discovery + pub fn discover_opts( + directory: impl AsRef, + Options { required_trust }: Options, + ) -> Result<(crate::Path, git_sec::Trust), Error> { + // Canonicalize the path so that `Path::parent` _actually_ gives + // us the parent directory. (`Path::parent` just strips off the last + // path component, which means it will not do what you expect when + // working with paths paths that contain '..'.) + let directory = maybe_canonicalize(directory.as_ref()).map_err(|_| Error::InaccessibleDirectory { + path: directory.as_ref().into(), + })?; + if !directory.is_dir() { + return Err(Error::InaccessibleDirectory { + path: directory.into_owned(), + }); } - match cursor.parent() { - Some(parent) => cursor = parent, - None => { - break Err(existing::Error::NoGitRepository { - path: directory.into_owned(), - }) + + let filter_by_trust = + |x: &std::path::Path, kind: crate::path::Kind| -> Result, Error> { + let trust = + git_sec::Trust::from_path_ownership(x).map_err(|err| Error::CheckTrust { path: x.into(), err })?; + Ok((trust >= required_trust).then(|| (crate::Path::from_dot_git_dir(x, kind), trust))) + }; + + let mut cursor = directory.clone(); + 'outer: loop { + for append_dot_git in &[false, true] { + if *append_dot_git { + cursor = cursor.into_owned().into(); + if let Cow::Owned(p) = &mut cursor { + p.push(".git"); + } + } + if let Ok(kind) = path::is::git(&cursor) { + match filter_by_trust(&cursor, kind)? { + Some(res) => break 'outer Ok(res), + None => { + break 'outer Err(Error::NoGitRepository { + path: directory.into_owned(), + }) + } + } + } + if *append_dot_git { + if let Cow::Owned(p) = &mut cursor { + p.pop(); + } + } + } + match cursor.parent() { + Some(parent) => cursor = parent.to_owned().into(), + None => { + break Err(Error::NoGitRepository { + path: directory.into_owned(), + }) + } } } } -} -fn maybe_canonicalize(path: &Path) -> std::io::Result> { - let ends_with_relative_component = path - .components() - .last() - .map_or(true, |c| matches!(c, Component::CurDir | Component::ParentDir)); - if ends_with_relative_component { - path.canonicalize().map(Into::into) - } else { - Ok(path.into()) + /// Find the location of the git repository directly in `directory` or in any of its parent directories, and provide + /// the trust level derived from Path ownership. + /// + /// Fail if no valid-looking git repository could be found. + pub fn discover(directory: impl AsRef) -> Result<(crate::Path, Trust), Error> { + discover_opts(directory, Default::default()) } -} -#[cfg(test)] -mod maybe_canonicalize { - use super::*; - - fn relative_component_count(path: impl AsRef) -> usize { - path.as_ref() + fn maybe_canonicalize(path: &Path) -> std::io::Result> { + let ends_with_relative_component = path .components() - .filter(|c| matches!(c, Component::CurDir | Component::ParentDir)) - .count() + .last() + .map_or(true, |c| matches!(c, Component::CurDir | Component::ParentDir)); + if ends_with_relative_component { + path.canonicalize().map(Into::into) + } else { + Ok(path.into()) + } } - #[test] - fn empty_paths_are_invalid() { - assert!( - maybe_canonicalize(Path::new("")).is_err(), - "empty paths are not equivalent to '.' but are non-existing" - ); - } + #[cfg(test)] + mod maybe_canonicalize { + use super::*; - #[test] - fn paths_starting_with_dot_but_end_with_normal_path_are_not_canonicalized() { - assert_eq!( - relative_component_count(maybe_canonicalize(&Path::new(".").join("hello")).unwrap()), - 1, - ); - } + fn relative_component_count(path: impl AsRef) -> usize { + path.as_ref() + .components() + .filter(|c| matches!(c, Component::CurDir | Component::ParentDir)) + .count() + } - #[test] - fn paths_ending_with_non_normal_component_are_canonicalized() { - assert_eq!( - relative_component_count(maybe_canonicalize(&Path::new(".").join(".")).unwrap()), - 0, - ); + #[test] + fn empty_paths_are_invalid() { + assert!( + maybe_canonicalize(Path::new("")).is_err(), + "empty paths are not equivalent to '.' but are non-existing" + ); + } + + #[test] + fn paths_starting_with_dot_but_end_with_normal_path_are_not_canonicalized() { + assert_eq!( + relative_component_count(maybe_canonicalize(&Path::new(".").join("hello")).unwrap()), + 1, + ); + } + + #[test] + fn paths_ending_with_non_normal_component_are_canonicalized() { + assert_eq!( + relative_component_count(maybe_canonicalize(&Path::new(".").join(".")).unwrap()), + 0, + ); + } } } diff --git a/git-repository/src/path/is.rs b/git-repository/src/path/is.rs index 312352e37b7..e6570f4d6d3 100644 --- a/git-repository/src/path/is.rs +++ b/git-repository/src/path/is.rs @@ -1,4 +1,3 @@ -//! use std::{ ffi::OsStr, path::{Path, PathBuf}, diff --git a/git-repository/src/path/mod.rs b/git-repository/src/path/mod.rs index e618c68c457..bc0ab7ce30a 100644 --- a/git-repository/src/path/mod.rs +++ b/git-repository/src/path/mod.rs @@ -4,7 +4,10 @@ use crate::{Kind, Path}; /// pub mod create; +/// pub mod discover; +pub use discover::function::{discover, discover_opts}; +/// pub mod is; impl AsRef for Path { diff --git a/git-repository/src/repository/mod.rs b/git-repository/src/repository/mod.rs index 532b008fa93..15dca027d23 100644 --- a/git-repository/src/repository/mod.rs +++ b/git-repository/src/repository/mod.rs @@ -39,6 +39,64 @@ impl crate::Repository { } } +/// Various permissions for parts of git repositories. +pub mod permissions { + use git_sec::permission::Resource; + use git_sec::{Access, Trust}; + + /// Permissions associated with various resources of a git repository + pub struct Permissions { + /// Control how a git-dir can be used. + /// + /// Note that a repository won't be usable at all unless read and write permissions are given. + pub git_dir: Access, + } + + impl Permissions { + /// Return permissions similar to what git does when the repository isn't owned by the current user, + /// thus refusing all operations in it. + pub fn strict() -> Self { + Permissions { + git_dir: Access::resource(git_sec::ReadWrite::empty()), + } + } + + /// Return permissions that will not include configuration files not owned by the current user, + /// but trust system and global configuration files along with those which are owned by the current user. + /// + /// This allows to read and write repositories even if they aren't owned by the current user, but avoid using + /// anything else that could cause us to write into unknown locations or use programs beyond our `PATH`. + pub fn secure() -> Self { + Permissions { + git_dir: Access::resource(git_sec::ReadWrite::all()), + } + } + + /// Everything is allowed with this set of permissions, thus we read all configuration and do what git typically + /// does with owned repositories. + pub fn all() -> Self { + Permissions { + git_dir: Access::resource(git_sec::ReadWrite::all()), + } + } + } + + impl git_sec::trust::DefaultForLevel for Permissions { + fn default_for_level(level: Trust) -> Self { + match level { + Trust::Full => Permissions::all(), + Trust::Reduced => Permissions::secure(), + } + } + } + + impl Default for Permissions { + fn default() -> Self { + Permissions::secure() + } + } +} + mod init { use std::cell::RefCell; diff --git a/git-repository/src/repository/thread_safe.rs b/git-repository/src/repository/thread_safe.rs index 10bdaa7f47e..d206d54e87a 100644 --- a/git-repository/src/repository/thread_safe.rs +++ b/git-repository/src/repository/thread_safe.rs @@ -17,21 +17,6 @@ mod access { } } -mod from_path { - use std::convert::TryFrom; - - use crate::Path; - - impl TryFrom for crate::ThreadSafeRepository { - type Error = crate::open::Error; - - fn try_from(value: Path) -> Result { - let (git_dir, worktree_dir) = value.into_repository_and_work_tree_directories(); - crate::ThreadSafeRepository::open_from_paths(git_dir, worktree_dir, Default::default()) - } - } -} - mod location { impl crate::ThreadSafeRepository { diff --git a/git-repository/tests/discover/mod.rs b/git-repository/tests/discover/mod.rs index 8f434f8f6fd..69735a7d222 100644 --- a/git-repository/tests/discover/mod.rs +++ b/git-repository/tests/discover/mod.rs @@ -6,9 +6,10 @@ mod existing { #[test] fn from_bare_git_dir() -> crate::Result { let dir = repo_path()?.join("bare.git"); - let path = git_repository::path::discover::existing(&dir)?; + let (path, trust) = git_repository::path::discover(&dir)?; assert_eq!(path.as_ref(), dir, "the bare .git dir is directly returned"); assert_eq!(path.kind(), Kind::Bare); + assert_eq!(trust, git_sec::Trust::Full); Ok(()) } @@ -16,35 +17,38 @@ mod existing { fn from_inside_bare_git_dir() -> crate::Result { let git_dir = repo_path()?.join("bare.git"); let dir = git_dir.join("objects"); - let path = git_repository::path::discover::existing(&dir)?; + let (path, trust) = git_repository::path::discover(&dir)?; assert_eq!( path.as_ref(), git_dir, "the bare .git dir is found while traversing upwards" ); assert_eq!(path.kind(), Kind::Bare); + assert_eq!(trust, git_sec::Trust::Full); Ok(()) } #[test] fn from_git_dir() -> crate::Result { let dir = repo_path()?.join(".git"); - let path = git_repository::path::discover::existing(&dir)?; + let (path, trust) = git_repository::path::discover(&dir)?; assert_eq!(path.kind(), Kind::WorkTree); assert_eq!( path.into_repository_and_work_tree_directories().0, dir, "the .git dir is directly returned if valid" ); + assert_eq!(trust, git_sec::Trust::Full); Ok(()) } #[test] fn from_working_dir() -> crate::Result { let dir = repo_path()?; - let path = git_repository::path::discover::existing(&dir)?; + let (path, trust) = git_repository::path::discover(&dir)?; assert_eq!(path.as_ref(), dir, "a working tree dir yields the git dir"); assert_eq!(path.kind(), Kind::WorkTree); + assert_eq!(trust, git_sec::Trust::Full); Ok(()) } @@ -52,9 +56,10 @@ mod existing { fn from_nested_dir() -> crate::Result { let working_dir = repo_path()?; let dir = working_dir.join("some/very/deeply/nested/subdir"); - let path = git_repository::path::discover::existing(&dir)?; + let (path, trust) = git_repository::path::discover(&dir)?; assert_eq!(path.kind(), Kind::WorkTree); assert_eq!(path.as_ref(), working_dir, "a working tree dir yields the git dir"); + assert_eq!(trust, git_sec::Trust::Full); Ok(()) } @@ -67,7 +72,7 @@ mod existing { // exploring ancestors.) let working_dir = repo_path()?; let dir = working_dir.join("some/very/deeply/nested/subdir/../../../../../../.."); - let path = git_repository::path::discover::existing(&dir)?; + let (path, trust) = git_repository::path::discover(&dir)?; assert_eq!(path.kind(), Kind::WorkTree); assert_eq!( path.as_ref() @@ -82,6 +87,7 @@ mod existing { working_dir.canonicalize()?, "a relative path that climbs above the test repo should yield the gitoxide repo" ); + assert_eq!(trust, git_sec::Trust::Full); Ok(()) } @@ -89,9 +95,10 @@ mod existing { fn from_nested_dir_inside_a_git_dir() -> crate::Result { let working_dir = repo_path()?; let dir = working_dir.join(".git").join("objects"); - let path = git_repository::path::discover::existing(&dir)?; + let (path, trust) = git_repository::path::discover(&dir)?; assert_eq!(path.kind(), Kind::WorkTree); assert_eq!(path.as_ref(), working_dir, "we find .git directories on the way"); + assert_eq!(trust, git_sec::Trust::Full); Ok(()) } diff --git a/git-sec/CHANGELOG.md b/git-sec/CHANGELOG.md new file mode 100644 index 00000000000..ac24617d490 --- /dev/null +++ b/git-sec/CHANGELOG.md @@ -0,0 +1,29 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## 0.0.0 (2022-04-15) + +An empty crate without any content to reserve the name for the gitoxide project. + +### Commit Statistics + + + + - 1 commit contributed to the release. + - 0 commits where understood as [conventional](https://www.conventionalcommits.org). + - 1 unique issue was worked on: [#386](https://github.com/Byron/gitoxide/issues/386) + +### Commit Details + + + +
view details + + * **[#386](https://github.com/Byron/gitoxide/issues/386)** + - An empty crate for git-sec ([`96a922c`](https://github.com/Byron/gitoxide/commit/96a922c4c9be194aaa4928fb21c9690a5c6e4445)) +
+ diff --git a/git-sec/Cargo.toml b/git-sec/Cargo.toml new file mode 100644 index 00000000000..7a40bcb7b82 --- /dev/null +++ b/git-sec/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "git-sec" +version = "0.1.0" +repository = "https://github.com/Byron/gitoxide" +license = "MIT/Apache-2.0" +description = "A WIP crate of the gitoxide project providing a shared trust model" +authors = ["Sebastian Thiel "] +edition = "2018" + +[lib] +doctest = false + +[features] +## Data structures implement `serde::Serialize` and `serde::Deserialize`. +serde1 = [ "serde" ] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +serde = { version = "1.0.114", optional = true, default-features = false, features = ["std", "derive"] } +bitflags = "1.3.2" + +[target.'cfg(not(windows))'.dependencies] +libc = "0.2.123" + +[target.'cfg(windows)'.dependencies] +windows = { version = "0.35.0", features = [ "alloc", + "Win32_Foundation", + "Win32_Security_Authorization", + "Win32_Storage_FileSystem", + "Win32_System_Memory", + "Win32_System_Threading" +] } + +[dev-dependencies] +tempfile = "3.3.0" diff --git a/git-sec/src/identity.rs b/git-sec/src/identity.rs new file mode 100644 index 00000000000..2483c0fc356 --- /dev/null +++ b/git-sec/src/identity.rs @@ -0,0 +1,106 @@ +use std::path::Path; + +#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] +#[cfg_attr(feature = "serde1", 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, +} + +/// 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: impl AsRef) -> std::io::Result { + impl_::is_path_owned_by_current_user(path) +} + +#[cfg(not(windows))] +mod impl_ { + use std::path::Path; + + pub fn is_path_owned_by_current_user(path: impl AsRef) -> std::io::Result { + fn owner_from_path(path: impl AsRef) -> std::io::Result { + use std::os::unix::fs::MetadataExt; + let meta = std::fs::symlink_metadata(path)?; + Ok(meta.uid()) + } + + fn owner_of_current_process() -> std::io::Result { + // SAFETY: there is no documented possibility for failure + #[allow(unsafe_code)] + let uid = unsafe { libc::geteuid() }; + Ok(uid) + } + + Ok(owner_from_path(path)? == owner_of_current_process()?) + } +} + +#[cfg(windows)] +mod impl_ { + use std::path::Path; + + fn err(msg: impl Into) -> std::io::Error { + std::io::Error::new(std::io::ErrorKind::Other, msg.into()) + } + + pub fn is_path_owned_by_current_user(path: impl AsRef) -> std::io::Result { + use windows::{ + core::PCWSTR, + Win32::{ + Foundation::{BOOL, ERROR_SUCCESS, HANDLE, PSID}, + Security::{ + Authorization::{GetNamedSecurityInfoW, SE_FILE_OBJECT}, + CheckTokenMembershipEx, OWNER_SECURITY_INFORMATION, PSECURITY_DESCRIPTOR, + }, + System::Memory::LocalFree, + }, + }; + + let mut err_msg = None; + let mut is_owned = false; + + #[allow(unsafe_code)] + unsafe { + let mut psid = PSID::default(); + let mut pdescriptor = PSECURITY_DESCRIPTOR::default(); + let wpath = to_wide_path(&path); + + let result = GetNamedSecurityInfoW( + PCWSTR(wpath.as_ptr()), + SE_FILE_OBJECT, + OWNER_SECURITY_INFORMATION, + &mut psid, + std::ptr::null_mut(), + std::ptr::null_mut(), + std::ptr::null_mut(), + &mut pdescriptor, + ); + + if result == ERROR_SUCCESS.0 { + let mut is_member = BOOL(0); + if CheckTokenMembershipEx(HANDLE::default(), psid, 0, &mut is_member).as_bool() { + is_owned = is_member.as_bool(); + } else { + err_msg = String::from("Could not check token membership").into(); + } + } else { + err_msg = format!("Could not get security information for path with err: {}", result).into(); + } + + LocalFree(pdescriptor.0 as isize); + } + + err_msg.map(|msg| Err(err(msg))).unwrap_or(Ok(is_owned)) + } + + fn to_wide_path(path: impl AsRef) -> Vec { + 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 + } +} diff --git a/git-sec/src/lib.rs b/git-sec/src/lib.rs new file mode 100644 index 00000000000..47d074bfd3f --- /dev/null +++ b/git-sec/src/lib.rs @@ -0,0 +1,155 @@ +#![deny(unsafe_code, rust_2018_idioms, missing_docs)] +//! A shared trust model for `gitoxide` crates. + +use std::marker::PhantomData; +use std::ops::Deref; + +/// 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 = "serde1", 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 { + 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: impl AsRef) -> std::io::Result { + Ok(crate::identity::is_path_owned_by_current_user(path.as_ref())? + .then(|| Trust::Full) + .unwrap_or(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 { + /// The value for fully trusted resources. + pub full: T, + /// The value for resources with reduced trust. + pub reduced: T, + } + + impl Default for Mapping + where + T: DefaultForLevel, + { + fn default() -> Self { + Mapping { + full: T::default_for_level(Trust::Full), + reduced: T::default_for_level(Trust::Reduced), + } + } + } + + impl Mapping { + /// 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, + } + } + } +} + +/// +pub mod permission { + use crate::Access; + + /// A marker trait to signal tags for permissions. + pub trait Tag {} + + /// A tag indicating that a permission is applying to the contents of a configuration file. + pub struct Config; + impl Tag for Config {} + + /// A tag indicating that a permission is applying to the resource itself. + pub struct Resource; + impl Tag for Resource {} + + impl

Access { + /// Create a permission for values contained in git configuration files. + /// + /// This applies permissions to values contained inside of these files. + pub fn config(permission: P) -> Self { + Access { + permission, + _data: Default::default(), + } + } + } + + impl

Access { + /// Create a permission a file or directory itself. + /// + /// This applies permissions to a configuration file itself and whether it can be used at all, or to a directory + /// to read from or write to. + pub fn resource(permission: P) -> Self { + Access { + permission, + _data: Default::default(), + } + } + } +} + +/// Allow, deny or forbid using a resource or performing an action. +#[derive(Debug, Copy, Clone, PartialOrd, PartialEq, Ord, Eq, Hash)] +#[cfg_attr(feature = "serde1", 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 reasource or performing an action. + Allow, +} + +bitflags::bitflags! { + /// Whether something can be read or written. + #[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] + pub struct ReadWrite: u8 { + /// The item can be read. + const READ = 1 << 0; + /// The item can be written + const WRITE = 1 << 1; + } +} + +/// A container to define tagged access permissions, rendering the permission read-only. +pub struct Access { + /// The access permission itself. + permission: P, + _data: PhantomData, +} + +impl Deref for Access { + type Target = P; + + fn deref(&self) -> &Self::Target { + &self.permission + } +} + +/// Various types to identify entities. +pub mod identity; diff --git a/git-sec/tests/identity/mod.rs b/git-sec/tests/identity/mod.rs new file mode 100644 index 00000000000..99fc3f71427 --- /dev/null +++ b/git-sec/tests/identity/mod.rs @@ -0,0 +1,9 @@ +#[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!(git_sec::identity::is_path_owned_by_current_user(file)?); + assert!(git_sec::identity::is_path_owned_by_current_user(dir.path())?); + Ok(()) +} diff --git a/git-sec/tests/sec.rs b/git-sec/tests/sec.rs new file mode 100644 index 00000000000..245e02b5e4a --- /dev/null +++ b/git-sec/tests/sec.rs @@ -0,0 +1,12 @@ +pub type Result = std::result::Result>; + +mod trust { + use git_sec::Trust; + + #[test] + fn ordering() { + assert!(Trust::Reduced < Trust::Full); + } +} + +mod identity; diff --git a/git-transport/Cargo.toml b/git-transport/Cargo.toml index 401f92e06b5..be6ba876802 100644 --- a/git-transport/Cargo.toml +++ b/git-transport/Cargo.toml @@ -52,6 +52,7 @@ required-features = ["async-client"] [dependencies] git-features = { version = "^0.20.0", path = "../git-features" } git-url = { version = "^0.4.0", path = "../git-url" } +git-sec = { version = "^0.1.0", path = "../git-sec" } git-packetline = { version = "^0.12.4", path = "../git-packetline" } serde = { version = "1.0.114", optional = true, default-features = false, features = ["std", "derive"]} diff --git a/git-transport/src/client/blocking_io/http/mod.rs b/git-transport/src/client/blocking_io/http/mod.rs index 7f30032258a..1c8d7053bc9 100644 --- a/git-transport/src/client/blocking_io/http/mod.rs +++ b/git-transport/src/client/blocking_io/http/mod.rs @@ -32,7 +32,7 @@ pub struct Transport { http: H, service: Option, line_provider: Option>, - identity: Option, + identity: Option, } impl Transport { @@ -71,21 +71,17 @@ impl Transport { #[allow(clippy::unnecessary_wraps, unknown_lints)] fn add_basic_auth_if_present(&self, headers: &mut Vec>) -> Result<(), client::Error> { - if let Some(identity) = &self.identity { - match identity { - client::Identity::Account { username, password } => { - #[cfg(not(debug_assertions))] - if self.url.starts_with("http://") { - return Err(client::Error::AuthenticationRefused( - "Will not send credentials in clear text over http", - )); - } - headers.push(Cow::Owned(format!( - "Authorization: Basic {}", - base64::encode(format!("{}:{}", username, password)) - ))) - } + if let Some(git_sec::identity::Account { username, password }) = &self.identity { + #[cfg(not(debug_assertions))] + if self.url.starts_with("http://") { + return Err(client::Error::AuthenticationRefused( + "Will not send credentials in clear text over http", + )); } + headers.push(Cow::Owned(format!( + "Authorization: Basic {}", + base64::encode(format!("{}:{}", username, password)) + ))) } Ok(()) } @@ -100,7 +96,7 @@ fn append_url(base: &str, suffix: &str) -> String { } impl client::TransportWithoutIO for Transport { - fn set_identity(&mut self, identity: client::Identity) -> Result<(), client::Error> { + fn set_identity(&mut self, identity: git_sec::identity::Account) -> Result<(), client::Error> { self.identity = Some(identity); Ok(()) } diff --git a/git-transport/src/client/mod.rs b/git-transport/src/client/mod.rs index 7cf1ec4576a..1df469d8656 100644 --- a/git-transport/src/client/mod.rs +++ b/git-transport/src/client/mod.rs @@ -26,7 +26,9 @@ pub mod capabilities; pub use capabilities::Capabilities; mod non_io_types; -pub use non_io_types::{Error, Identity, MessageKind, WriteMode}; +pub use non_io_types::{Error, MessageKind, WriteMode}; + +pub use git_sec::identity::Account; /// #[cfg(any(feature = "blocking-client", feature = "async-client"))] diff --git a/git-transport/src/client/non_io_types.rs b/git-transport/src/client/non_io_types.rs index 7fcbf4078de..6ffabe43500 100644 --- a/git-transport/src/client/non_io_types.rs +++ b/git-transport/src/client/non_io_types.rs @@ -29,19 +29,6 @@ pub enum MessageKind { Text(&'static [u8]), } -#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] -#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] -/// An identity for use when authenticating the transport layer. -pub enum Identity { - /// An account based identity - Account { - /// The user's name - username: String, - /// The user's password - password: String, - }, -} - pub(crate) mod connect { use quick_error::quick_error; quick_error! { diff --git a/git-transport/src/client/traits.rs b/git-transport/src/client/traits.rs index e7a5ab40cc0..f57b2fe2d09 100644 --- a/git-transport/src/client/traits.rs +++ b/git-transport/src/client/traits.rs @@ -2,10 +2,7 @@ use std::ops::{Deref, DerefMut}; #[cfg(any(feature = "blocking-client", feature = "async-client"))] use crate::client::{MessageKind, RequestWriter, WriteMode}; -use crate::{ - client::{Error, Identity}, - Protocol, -}; +use crate::{client::Error, Protocol}; /// This trait represents all transport related functions that don't require any input/output to be done which helps /// implementation to share more code across blocking and async programs. @@ -16,7 +13,7 @@ pub trait TransportWithoutIO { /// of the identity in order to mark it as invalid. Otherwise the user might have difficulty updating obsolete /// credentials. /// Please note that most transport layers are unauthenticated and thus return [an error][Error::AuthenticationUnsupported] here. - fn set_identity(&mut self, _identity: Identity) -> Result<(), Error> { + fn set_identity(&mut self, _identity: git_sec::identity::Account) -> Result<(), Error> { Err(Error::AuthenticationUnsupported) } /// Get a writer for sending data and obtaining the response. It can be configured in various ways @@ -53,7 +50,7 @@ pub trait TransportWithoutIO { // Would be nice if the box implementation could auto-forward to all implemented traits. impl TransportWithoutIO for Box { - fn set_identity(&mut self, identity: Identity) -> Result<(), Error> { + fn set_identity(&mut self, identity: git_sec::identity::Account) -> Result<(), Error> { self.deref_mut().set_identity(identity) } @@ -76,7 +73,7 @@ impl TransportWithoutIO for Box { } impl TransportWithoutIO for &mut T { - fn set_identity(&mut self, identity: Identity) -> Result<(), Error> { + fn set_identity(&mut self, identity: git_sec::identity::Account) -> Result<(), Error> { self.deref_mut().set_identity(identity) } diff --git a/git-transport/tests/client/blocking_io/http/mod.rs b/git-transport/tests/client/blocking_io/http/mod.rs index b6ad121cd01..1659ad51fa8 100644 --- a/git-transport/tests/client/blocking_io/http/mod.rs +++ b/git-transport/tests/client/blocking_io/http/mod.rs @@ -9,7 +9,7 @@ use std::{ use bstr::ByteSlice; use git_transport::{ - client::{self, http, Identity, SetServiceResponse, Transport, TransportV2Ext, TransportWithoutIO}, + client::{self, http, SetServiceResponse, Transport, TransportV2Ext, TransportWithoutIO}, Protocol, Service, }; @@ -42,7 +42,7 @@ fn assert_error_status( fn http_authentication_error_can_be_differentiated_and_identity_is_transmitted() -> crate::Result { let (server, mut client) = assert_error_status(401, std::io::ErrorKind::PermissionDenied)?; server.next_read_and_respond_with(fixture_bytes("v1/http-handshake.response")); - client.set_identity(Identity::Account { + client.set_identity(git_sec::identity::Account { username: "user".into(), password: "password".into(), })?;