From 64da82c661b2a28c7a258bd30c0084633d0e1896 Mon Sep 17 00:00:00 2001 From: Justin Wernick Date: Fri, 11 Aug 2023 22:04:19 +0200 Subject: Create group directories if they don't already exist - With the correct ownership - With the correct permissions Fix #5 --- .woodpecker.yml | 2 +- CHANGELOG.md | 9 +++++++ README.md | 3 ++- src/git.rs | 53 ++++++++++++++++++++++++++++++----------- src/lib.rs | 2 ++ src/user_info.rs | 9 ++++++- tests/cli.rs | 18 ++++++++++++++ tests/cli_test_utils/context.rs | 8 +++---- 8 files changed, 83 insertions(+), 21 deletions(-) diff --git a/.woodpecker.yml b/.woodpecker.yml index cee339a..bf90627 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -1,4 +1,4 @@ -pipeline: +steps: test: image: rust environment: [CARGO_TERM_COLOR=always] diff --git a/CHANGELOG.md b/CHANGELOG.md index c1035c4..334d087 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 license. This application is now dual licensed with the MIT license, as is common with Rust crates. +### Fix + +- When a repo is created in a directory which does not exist yet, the directory + is created with appropriate permissions. For example, when creating a personal + repo if `git/username` does not exist, it will be created with personal read, + write and execute access for the user only. If `git/group` does not exist, it + will be created owned by `group`, with group level read, write and execute + access, and the setgid flag set. + ## [0.2.0] - 2023-07-17 ### Added diff --git a/README.md b/README.md index adb7236..bdc02e9 100644 --- a/README.md +++ b/README.md @@ -134,7 +134,8 @@ cargo install shackle-shell Next, Shackle expects a specific directory structure. Specifically, personal git repos will live in `~/git/your-username` and shared repos will live in -`~/git/your-group`. +`~/git/your-group`. If you create a repo that requires one of these group +directories and it doesn't exist yet, Shackle will create it. If you have many users on your server, then consider making `~/git` a symlink to the actual shared location for your git repos. For example, on my repo, all git diff --git a/src/git.rs b/src/git.rs index 9fa3e78..ea55f2c 100644 --- a/src/git.rs +++ b/src/git.rs @@ -1,11 +1,12 @@ use crate::{ parser::{GitReceivePackArgs, GitUploadPackArgs}, - user_info::{get_user_groups, get_username}, + user_info::{get_gid, get_user_groups, get_username}, ShackleError, }; use git2::{ErrorCode, Repository, RepositoryInitMode, RepositoryInitOptions}; use std::{ fs, + os::unix::fs::PermissionsExt, path::{Path, PathBuf}, process::Command, }; @@ -75,24 +76,48 @@ pub fn init( description: &Option, branch: &str, ) -> Result { + if let Some(group) = &group { + if !verify_user_is_in_group(group) { + return Err(ShackleError::InvalidGroup); + } + } + + let git_prefix = git_dir_prefix(); + let collection_dir = match group { + Some(group) => group_git_dir(group), + None => personal_git_dir()?, + }; + let path = collection_dir.join(repo_name).with_extension("git"); + + if !git_prefix.is_dir() { + fs::create_dir(&git_prefix)?; + } + + if !collection_dir.is_dir() { + fs::create_dir(&collection_dir)?; + + if let Some(group) = group { + let gid = get_gid(&group).expect("User is in group but no group ID?"); + nix::unistd::chown(&collection_dir, None, Some(gid))?; + } + + let mut perms = collection_dir.metadata()?.permissions(); + perms.set_mode(match group { + Some(_) => 0o2770, + None => 0o700, + }); + fs::set_permissions(&collection_dir, perms)?; + } + let mut init_opts = RepositoryInitOptions::new(); init_opts .bare(true) - .mkdir(true) + .mkdir(false) .no_reinit(true) .initial_head(branch); - - let path = match group { - Some(group) => { - if !verify_user_is_in_group(group) { - return Err(ShackleError::InvalidGroup); - } - - init_opts.mode(RepositoryInitMode::SHARED_GROUP); - group_git_dir(group).join(repo_name).with_extension("git") - } - None => personal_git_dir()?.join(repo_name).with_extension("git"), - }; + if group.is_some() { + init_opts.mode(RepositoryInitMode::SHARED_GROUP); + } Repository::init_opts(&path, &init_opts)?; if let Some(description) = description { diff --git a/src/lib.rs b/src/lib.rs index 163e41b..62c050a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -97,6 +97,8 @@ pub enum ShackleError { #[error(transparent)] IoError(#[from] io::Error), #[error(transparent)] + NixError(#[from] nix::errno::Errno), + #[error(transparent)] GitError(#[from] git2::Error), #[error(transparent)] ReadlineError(#[from] ReadlineError), diff --git a/src/user_info.rs b/src/user_info.rs index 7a42db9..a352adb 100644 --- a/src/user_info.rs +++ b/src/user_info.rs @@ -1,4 +1,4 @@ -use nix::unistd::{getgroups, getuid, Group, User}; +use nix::unistd::{getgroups, getuid, Gid, Group, User}; pub fn get_username() -> Option { let uid = getuid(); @@ -13,6 +13,13 @@ pub fn get_user_groups() -> Vec { .collect() } +pub fn get_gid(group_name: &str) -> Option { + nix::unistd::Group::from_name(&group_name) + .ok() + .flatten() + .map(|group| group.gid) +} + #[cfg(test)] mod test { use super::*; diff --git a/tests/cli.rs b/tests/cli.rs index 01c3347..34249c2 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -2,6 +2,7 @@ mod cli_test_utils; use anyhow::Result; use cli_test_utils::*; +use std::os::unix::fs::MetadataExt; const REPO_NAME: &str = "my-repository"; const REPO_NAME_2: &str = "my-other-repository"; @@ -83,6 +84,23 @@ fn can_init_a_new_shared_git_repo() -> Result<()> { verify_repo_exists(&repo_dir); verify_repo_config_value(&repo_dir, "core.sharedrepository", Some("1")); + let expected_gid = nix::unistd::Group::from_name(&group) + .unwrap() + .unwrap() + .gid + .as_raw(); + let group_dir = repo_dir.parent().unwrap(); + let group_dir_metadata = group_dir.metadata().unwrap(); + assert_eq!(group_dir_metadata.gid(), expected_gid); + assert_eq!( + group_dir_metadata.mode(), + 0o42770, + "Mode is {:o}", + group_dir_metadata.mode() + ); + + assert_eq!(repo_dir.metadata().unwrap().gid(), expected_gid); + Ok(()) } diff --git a/tests/cli_test_utils/context.rs b/tests/cli_test_utils/context.rs index 2517040..2c61085 100644 --- a/tests/cli_test_utils/context.rs +++ b/tests/cli_test_utils/context.rs @@ -100,15 +100,15 @@ impl TestContext { } } +pub fn arbitrary_user_group() -> String { + get_user_groups().into_iter().next().unwrap() +} + pub fn personal_repo_path(repo_name: &str) -> String { let username = get_username().unwrap(); format!("git/{username}/{repo_name}.git") } -pub fn arbitrary_user_group() -> String { - get_user_groups().into_iter().next().unwrap() -} - pub fn group_repo_path(group: &str, repo_name: &str) -> String { format!("git/{group}/{repo_name}.git") } -- cgit v1.2.3