-
Notifications
You must be signed in to change notification settings - Fork 285
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
New hook to prevent bookmark creation by prefix
Summary: Git has this limitation that since refs are stored in the file system, a user cannot create two refs like `refs/heads/some_branch` and `refs/heads/some_branch/another` because storing the latter ref would require creating folders `refs`, `heads` and `some_branch` while creating the former would require creating `some_branch` as a file. We cannot have a file and a directory with the same name at the same level, hence this behavior is disallowed. Mononoke does not prevent this by default since refs (bookmarks) in Mononoke are stored as entries in a DB table so there is no such restriction. However, to maintain parity with vanilla Git, we have to put this check in place as a hook. Reviewed By: andreacampi Differential Revision: D66768700 fbshipit-source-id: 8d80ff324248f8bc06201a3e57e3194aa46afdbf
- Loading branch information
1 parent
5c1ef0b
commit 94f4603
Showing
4 changed files
with
252 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
120 changes: 120 additions & 0 deletions
120
eden/mononoke/hooks/src/implementations/block_new_bookmark_creations_by_prefix.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
/* | ||
* Copyright (c) Meta Platforms, Inc. and affiliates. | ||
* | ||
* This software may be used and distributed according to the terms of the | ||
* GNU General Public License version 2. | ||
*/ | ||
|
||
use std::str::FromStr; | ||
|
||
use anyhow::Error; | ||
use anyhow::Result; | ||
use async_trait::async_trait; | ||
use bookmarks::BookmarkKey; | ||
use bookmarks::BookmarkPrefix; | ||
use context::CoreContext; | ||
use mononoke_types::BonsaiChangeset; | ||
use mononoke_types::MPath; | ||
use serde::Deserialize; | ||
|
||
use crate::BookmarkHook; | ||
use crate::CrossRepoPushSource; | ||
use crate::HookConfig; | ||
use crate::HookExecution; | ||
use crate::HookRejectionInfo; | ||
use crate::HookStateProvider; | ||
use crate::PushAuthoredBy; | ||
|
||
#[derive(Clone, Debug, Deserialize)] | ||
pub struct BlockNewBookmarkCreationsByPrefixConfig { | ||
message: Option<String>, | ||
} | ||
|
||
#[derive(Clone, Debug)] | ||
pub struct BlockNewBookmarkCreationsByPrefixHook { | ||
config: BlockNewBookmarkCreationsByPrefixConfig, | ||
} | ||
|
||
impl BlockNewBookmarkCreationsByPrefixHook { | ||
pub fn new(config: &HookConfig) -> Result<Self> { | ||
Self::with_config(config.parse_options()?) | ||
} | ||
|
||
pub fn with_config(config: BlockNewBookmarkCreationsByPrefixConfig) -> Result<Self> { | ||
Ok(Self { config }) | ||
} | ||
} | ||
|
||
#[async_trait] | ||
impl BookmarkHook for BlockNewBookmarkCreationsByPrefixHook { | ||
async fn run<'this: 'cs, 'ctx: 'this, 'cs, 'fetcher: 'cs>( | ||
&'this self, | ||
ctx: &'ctx CoreContext, | ||
bookmark: &BookmarkKey, | ||
_from: &'cs BonsaiChangeset, | ||
content_manager: &'fetcher dyn HookStateProvider, | ||
_cross_repo_push_source: CrossRepoPushSource, | ||
_push_authored_by: PushAuthoredBy, | ||
) -> Result<HookExecution, Error> { | ||
let bookmark_state = content_manager.get_bookmark_state(ctx, bookmark).await?; | ||
if !bookmark_state.is_new() { | ||
return Ok(HookExecution::Accepted); | ||
} | ||
// Ensure we append a trailing slash if the bookmark doesn't have one. This is because | ||
// we are trying to check if the bookmark matches any existing bookmarks as a path component | ||
// e.g. some/bookmark/ matching some/bookmark/path. | ||
let bookmark_prefix_str = if !bookmark.as_str().ends_with("/") { | ||
format!("{bookmark}/") | ||
} else { | ||
bookmark.to_string() | ||
}; | ||
// Check if this bookmark itself is a path prefix of any existing bookmark | ||
let bookmark_prefix = BookmarkPrefix::from_str(bookmark_prefix_str.as_str())?; | ||
if content_manager | ||
.bookmark_exists_with_prefix(ctx.clone(), &bookmark_prefix) | ||
.await? | ||
{ | ||
if let Some(message) = &self.config.message { | ||
return Ok(HookExecution::Rejected(HookRejectionInfo::new_long( | ||
"Invalid bookmark creation is restricted in this repository.", | ||
message.clone(), | ||
))); | ||
} else { | ||
return Ok(HookExecution::Rejected(HookRejectionInfo::new_long( | ||
"Invalid bookmark creation is restricted in this repository.", | ||
format!( | ||
"Creation of bookmark \"{bookmark}\" was blocked because it exists as a path prefix of an existing bookmark", | ||
), | ||
))); | ||
} | ||
} | ||
// The current bookmark is not a path prefix of any existing bookmark, so check if any of its path | ||
// prefixes exist as a bookmark for this repo. | ||
for bookmark_prefix_path in MPath::new(bookmark_prefix_str.as_str())?.into_ancestors() { | ||
let bookmark_prefix_path = | ||
BookmarkKey::from_str(std::str::from_utf8(&bookmark_prefix_path.to_vec())?)?; | ||
// Check if the path ancestors of this bookmark already exist as bookmark in the repo | ||
if content_manager | ||
.get_bookmark_state(ctx, &bookmark_prefix_path) | ||
.await? | ||
.is_existing() | ||
{ | ||
if let Some(message) = &self.config.message { | ||
return Ok(HookExecution::Rejected(HookRejectionInfo::new_long( | ||
"Invalid bookmark creation is restricted in this repository.", | ||
message.clone(), | ||
))); | ||
} else { | ||
return Ok(HookExecution::Rejected(HookRejectionInfo::new_long( | ||
"Invalid bookmark creation is restricted in this repository.", | ||
format!( | ||
"Creation of bookmark \"{bookmark}\" was blocked because its path prefix \"{bookmark_prefix_path}\" already exists as a bookmark", | ||
), | ||
))); | ||
} | ||
} | ||
} | ||
|
||
Ok(HookExecution::Accepted) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
122 changes: 122 additions & 0 deletions
122
...ion/mononoke_git_server/test-mononoke-git-server-block-new-bookmark-creations-by-prefix.t
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
# Copyright (c) Meta Platforms, Inc. and affiliates. | ||
# | ||
# This software may be used and distributed according to the terms of the | ||
# GNU General Public License found in the LICENSE file in the root | ||
# directory of this source tree. | ||
|
||
$ . "${TEST_FIXTURES}/library.sh" | ||
|
||
$ REPOTYPE="blob_files" | ||
$ setup_common_config $REPOTYPE | ||
$ GIT_REPO_ORIGIN="${TESTTMP}/origin/repo-git" | ||
$ GIT_REPO_SUBMODULE="${TESTTMP}/origin/repo-submodule-git" | ||
$ GIT_REPO="${TESTTMP}/repo-git" | ||
|
||
# Setup git repository | ||
$ mkdir -p "$GIT_REPO_ORIGIN" | ||
$ cd "$GIT_REPO_ORIGIN" | ||
$ git init -q | ||
$ echo "this is file1" > file1 | ||
$ git add file1 | ||
$ git commit -qam "Add file1" | ||
$ old_head=$(git rev-parse HEAD) | ||
$ git tag -a -m"new tag" first_tag | ||
$ echo "this is file2" > file2 | ||
$ git add file2 | ||
$ git commit -qam "Add file2" | ||
$ git tag -a empty_tag -m "" | ||
$ cd "$TESTTMP" | ||
$ git clone "$GIT_REPO_ORIGIN" | ||
Cloning into 'repo-git'... | ||
done. | ||
|
||
# Import it into Mononoke | ||
$ cd "$TESTTMP" | ||
$ quiet gitimport "$GIT_REPO" --derive-hg --generate-bookmarks full-repo | ||
|
||
# Set Mononoke as the Source of Truth | ||
$ set_mononoke_as_source_of_truth_for_git | ||
|
||
$ cd "$TESTTMP"/mononoke-config | ||
$ cat >> repos/repo/server.toml <<EOF | ||
> [[bookmarks]] | ||
> regex=".*" | ||
> [[bookmarks.hooks]] | ||
> hook_name="block_new_bookmark_creations_by_prefix" | ||
> [[hooks]] | ||
> name="block_new_bookmark_creations_by_prefix" | ||
> config_json='''{ | ||
> }''' | ||
> bypass_pushvar="x-git-allow-invalid-bookmarks=1" | ||
> EOF | ||
$ cd "${TESTTMP}" | ||
|
||
# Start up the Mononoke Git Service | ||
$ mononoke_git_service | ||
# Clone the Git repo from Mononoke | ||
$ git_client clone $MONONOKE_GIT_SERVICE_BASE_URL/$REPONAME.git | ||
Cloning into 'repo'... | ||
|
||
# Add some new commits to the cloned repo and push it to remote | ||
$ cd repo | ||
$ echo new_file > new_file | ||
$ git add . | ||
$ git commit -qam "Commit" | ||
|
||
# This push works | ||
$ git_client push origin --all | ||
To https://localhost:$LOCAL_PORT/repos/git/ro/repo.git | ||
e8615d6..8ff9b0a master_bookmark -> master_bookmark | ||
|
||
$ echo brand_new_file > brand_new_file | ||
$ git add . | ||
$ git commit -qam "Commit" | ||
|
||
# This push is blocked | ||
$ git_client push origin HEAD:master_bookmark/another_master | ||
To https://localhost:$LOCAL_PORT/repos/git/ro/repo.git | ||
! [remote rejected] HEAD -> master_bookmark/another_master (hooks failed: | ||
block_new_bookmark_creations_by_prefix for f53155321de7df9aa68c3b4b418019e612f0fa4b: Creation of bookmark "heads/master_bookmark/another_master" was blocked because its path prefix "heads/master_bookmark" already exists as a bookmark | ||
|
||
For more information about hooks and bypassing, refer https://fburl.com/wiki/mb4wtk1j) | ||
error: failed to push some refs to 'https://localhost:$LOCAL_PORT/repos/git/ro/repo.git' | ||
[1] | ||
|
||
# Create a new branch and push to it | ||
$ git checkout -b just/some/created/branch | ||
Switched to a new branch 'just/some/created/branch' | ||
$ echo new_content > new_content | ||
$ git add . | ||
$ git commit -qam "New content commit" | ||
$ git_client push origin HEAD:just/some/created/branch | ||
To https://localhost:$LOCAL_PORT/repos/git/ro/repo.git | ||
* [new branch] HEAD -> just/some/created/branch | ||
|
||
# Try pushing a path prefix of the branch. This will fail | ||
$ echo more_content > more_content | ||
$ git add . | ||
$ git commit -qam "More new content" | ||
$ git_client push origin HEAD:just/some/created | ||
To https://localhost:$LOCAL_PORT/repos/git/ro/repo.git | ||
! [remote rejected] HEAD -> just/some/created (hooks failed: | ||
block_new_bookmark_creations_by_prefix for 134d5c589615ac5e391391b82f46f3722f89c924: Creation of bookmark "heads/just/some/created" was blocked because it exists as a path prefix of an existing bookmark | ||
|
||
For more information about hooks and bypassing, refer https://fburl.com/wiki/mb4wtk1j) | ||
error: failed to push some refs to 'https://localhost:$LOCAL_PORT/repos/git/ro/repo.git' | ||
[1] | ||
$ git_client push origin HEAD:just | ||
To https://localhost:$LOCAL_PORT/repos/git/ro/repo.git | ||
! [remote rejected] HEAD -> just (hooks failed: | ||
block_new_bookmark_creations_by_prefix for 134d5c589615ac5e391391b82f46f3722f89c924: Creation of bookmark "heads/just" was blocked because it exists as a path prefix of an existing bookmark | ||
|
||
For more information about hooks and bypassing, refer https://fburl.com/wiki/mb4wtk1j) | ||
error: failed to push some refs to 'https://localhost:$LOCAL_PORT/repos/git/ro/repo.git' | ||
[1] | ||
|
||
# Try pushing a prefix of the branch that is not path-prefix. This should work | ||
$ echo yet_more_content > yet_more_content | ||
$ git add . | ||
$ git commit -qam "More new content" | ||
$ git_client push origin HEAD:just/some/cr | ||
To https://localhost:$LOCAL_PORT/repos/git/ro/repo.git | ||
* [new branch] HEAD -> just/some/cr |