Re: [PATCH] push: deny policy to prevent pushes to unwanted remotes.

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 




> On 30 May 2016, at 06:45, Antoine Queru <antoine.queru@xxxxxxxxxxxxxxxxxxxxxxx> wrote:
> 
> Currently, a user wanting to prevent accidental pushes to the wrong remote
> has to create a pre-push hook.
> The feature offers a configuration to allow users to prevent accidental pushes
> to the wrong remote. The user may define a list of whitelisted remotes, a list
> of blacklisted remotes and a default policy ("allow" or "deny"). A push
> is denied if the remote is explicitely blacklisted or if it isn't
> whitelisted and the default policy is "deny".
> 
> This feature is intended as a safety net more than a real security, the user
> will always be able to modify the config if he wants to. It is here for him to
> consciously restrict his push possibilities. For example, it may be useful
> for an unexperimented user fearing to push to the wrong remote, or for
> companies wanting to avoid unintentionnal leaking of private code on public
> repositories.

Thanks for working on this feature. Unfortunately I won't be able to test and review it before June 14. I am traveling without laptop and only very sporadic internet access :)

One thing that I noticed already: I think a custom warning/error message for rejected pushes would be important because, as you wrote above, this feature does not provide real security. That means if a push is rejected for someone in an organization then the user needs to understand what is going on. E.g. in my organization I would point the user to the open source contribution guidelines etc.

Thanks,
Lars


> 
> By default the whitelist/blacklist feature is disabled since the default policy
> is "allow".
> 
> Signed-off-by: Antoine Queru <antoine.queru@xxxxxxxxxxxxxxxxxxxxxxx>
> Signed-off-by: Francois Beutin <francois.beutin@xxxxxxxxxxxxxxxxxxxxxxx>
> Signed-off-by: Matthieu Moy <matthieu.moy@xxxxxxxxxxxxxxx>
> ---
> This is the first implementation of the feature proposed in SoCG 2016.
> The conversation about it can be found here:
> http://thread.gmane.org/gmane.comp.version-control.git/295166
> and http://thread.gmane.org/gmane.comp.version-control.git/289749.
> We have included in cc all the participants in the previous conversation.
> Documentation/config.txt            | 17 +++++++
> Documentation/git-push.txt          | 32 +++++++++++++
> builtin/push.c                      | 75 ++++++++++++++++++++++++++++---
> t/t5544-push-whitelist-blacklist.sh | 90 +++++++++++++++++++++++++++++++++++++
> 4 files changed, 209 insertions(+), 5 deletions(-)
> create mode 100755 t/t5544-push-whitelist-blacklist.sh
> 
> diff --git a/Documentation/config.txt b/Documentation/config.txt
> index 53f00db..1478ce3 100644
> --- a/Documentation/config.txt
> +++ b/Documentation/config.txt
> @@ -2517,6 +2517,23 @@ remote.pushDefault::
>    `branch.<name>.remote` for all branches, and is overridden by
>    `branch.<name>.pushRemote` for specific branches.
> 
> +remote.pushBlacklisted::
> +    The list of remotes the user is forbidden to push to.
> +    See linkgit:git-push[1]
> +
> +remote.pushWhitelisted::
> +    The list of remotes the user is allowed to push to.
> +    See linkgit:git-push[1]
> +
> +remote.pushDefaultPolicy::
> +    Can be set to either 'allow' or 'deny', defines the policy to adopt
> +    when the user tries to push to a remote not in the whitelist or 
> +    the blacklist. Defaults to 'allow'.
> +
> +remote.pushDenyMessage::
> +    The message printed when pushing to a forbidden remote.
> +    Defaults to "Pushing to this remote has been forbidden".
> +
> remote.<name>.url::
>    The URL of a remote repository.  See linkgit:git-fetch[1] or
>    linkgit:git-push[1].
> diff --git a/Documentation/git-push.txt b/Documentation/git-push.txt
> index cf6ee4a..644bfde 100644
> --- a/Documentation/git-push.txt
> +++ b/Documentation/git-push.txt
> @@ -368,6 +368,38 @@ reason::
>    refs, no explanation is needed. For a failed ref, the reason for
>    failure is described.
> 
> +
> +Note about denying pushes to the wrong remotes
> +----------------------------------------------
> +
> +If you fear accidental pushes to the wrong remotes, you can use the
> +blacklist/whitelist feature. The goal is to catch pushes to unwanted
> +remotes before they happen.
> +
> +The simplest way to forbid pushes to a remote is to add its url in the
> +config file under the 'remote.pushBlacklisted' variable.
> +A more restrictive way is to change 'remote.pushDefaultPolicy' to 'deny',
> +thus denying pushes to every remote not whitelisted. You can then add
> +the right remotes under 'remote.pushWhitelisted'.
> +
> +You can also configure more advanced acceptation/denial behavior following
> +this rule: the more the url in the config prefixes the asked url the more
> +priority it has.
> +
> +For example, if we set up the configuration variables like this:
> +    git config --add remote.pushBlacklisted repository.com
> +    git config --add remote.pushWhitelisted repository.com/Special_Folder
> +Any push of this form will be accepted:
> +    git push repository.com/Special_Folder/foo
> +While those ones for example will be denied:
> +    git push repository.com/Normal_Folder/bar
> +
> +Additionally, you can configure the message printed when a push is denied
> +with the 'remote.pushDenyMessage' configuration variable.
> +
> +An error will be raised if the url is blacklisted and whitelisted at the same.
> +
> +
> Note about fast-forwards
> ------------------------
> 
> diff --git a/builtin/push.c b/builtin/push.c
> index 4e9e4db..0aa1f7d 100644
> --- a/builtin/push.c
> +++ b/builtin/push.c
> @@ -11,6 +11,8 @@
> #include "submodule.h"
> #include "submodule-config.h"
> #include "send-pack.h"
> +#include "urlmatch.h"
> +#include "url.h"
> 
> static const char * const push_usage[] = {
>    N_("git push [<options>] [<repository> [<refspec>...]]"),
> @@ -353,6 +355,68 @@ static int push_with_options(struct transport *transport, int flags)
>    return 1;
> }
> 
> +/* 
> + * TODO: the shorter scp-like syntax for the SSH protocol isn't recognize
> + * with url_normalize
> + */
> +static const char *string_url_normalize(const char *repo_name)
> +{
> +    if (starts_with(repo_name, "file://"))
> +        return repo_name + 7;
> +    if (is_url(repo_name)) {
> +        struct url_info url_infos;
> +        url_normalize(repo_name, &url_infos);
> +        return url_infos.url + url_infos.host_off;
> +    }
> +    return repo_name;
> +}
> +
> +static int longest_prefix_size(const char *repo_to_push, const struct string_list *list)
> +{
> +    struct string_list_item *item_curr;
> +    int max_size = 0;
> +    if (!list)
> +        return 0;
> +    for_each_string_list_item(item_curr, list) {
> +        const char *repo_curr = string_url_normalize(item_curr->string);
> +        if (starts_with(repo_to_push, repo_curr) &&
> +            max_size < strlen(repo_curr)){
> +            max_size = strlen(repo_curr);
> +            if (strlen(repo_to_push) == max_size)
> +                break;
> +        }
> +    }
> +    return max_size;
> +}
> +
> +static void block_unauthorized_url(const char *repo)
> +{
> +    const char *default_policy, *deny_message;
> +    const struct string_list *whitelist, *blacklist;
> +    int whitelist_match_size, blacklist_match_size;
> +    const char* repo_normalize = string_url_normalize(repo);
> +    if (git_config_get_value("remote.pushDefaultPolicy", &default_policy))
> +        default_policy = "allow";
> +    if (git_config_get_value("remote.pushDenyMessage", &deny_message))
> +        deny_message = "Pushing to this remote is forbidden";
> +    whitelist = git_config_get_value_multi("remote.pushWhitelisted");
> +    blacklist = git_config_get_value_multi("remote.pushBlacklisted");
> +
> +    whitelist_match_size = longest_prefix_size(repo_normalize, whitelist);
> +    blacklist_match_size = longest_prefix_size(repo_normalize, blacklist);
> +
> +    if (whitelist_match_size < blacklist_match_size)
> +        die(_("%s"), deny_message);
> +    if (whitelist_match_size == blacklist_match_size) {
> +        if (whitelist_match_size)
> +            die(_("%s cannot be whitelisted and blacklisted at the same time"), repo);
> +        if (!strcmp(default_policy, "deny"))
> +            die(_("%s"), deny_message);
> +        if (strcmp(default_policy, "allow"))
> +            die(_("Unrecognized value for remote.pushDefaultPolicy, should be 'allow' or 'deny'"));
> +    }
> +}
> +
> static int do_push(const char *repo, int flags)
> {
>    int i, errs;
> @@ -404,15 +468,16 @@ static int do_push(const char *repo, int flags)
>    url_nr = push_url_of_remote(remote, &url);
>    if (url_nr) {
>        for (i = 0; i < url_nr; i++) {
> -            struct transport *transport =
> -                transport_get(remote, url[i]);
> +            struct transport *transport;
> +            block_unauthorized_url(url[i]);
> +            transport = transport_get(remote, url[i]);
>            if (push_with_options(transport, flags))
>                errs++;
>        }
>    } else {
> -        struct transport *transport =
> -            transport_get(remote, NULL);
> -
> +        struct transport *transport;
> +        block_unauthorized_url(remote->url[0]);
> +        transport = transport_get(remote, NULL);
>        if (push_with_options(transport, flags))
>            errs++;
>    }
> diff --git a/t/t5544-push-whitelist-blacklist.sh b/t/t5544-push-whitelist-blacklist.sh
> new file mode 100755
> index 0000000..e0683d6
> --- /dev/null
> +++ b/t/t5544-push-whitelist-blacklist.sh
> @@ -0,0 +1,90 @@
> +#!/bin/sh
> +
> +test_description='push allowed/denied depending on config options'
> +
> +. ./test-lib.sh
> +
> +test_expect_success 'setup' '
> +    test_commit commit &&
> +    git init --bare wlisted1 &&
> +    git init --bare wlisted1/folder &&
> +    git init --bare wlisted1/folder/blistedfolder &&
> +    git init --bare wlisted1/folder/blistedfolder/folder &&
> +    git init --bare wlisted2 &&
> +    git init --bare blisted1 &&
> +    git init --bare blisted1/folder &&
> +    git init --bare blisted2 &&
> +    git init --bare untracked &&
> +    git init --bare repo &&
> +    git remote add wlisted wlisted1 &&
> +    git remote add wlisted2 wlisted2 &&
> +    git remote add blisted blisted1 &&
> +    git remote add blisted2 blisted2 &&
> +    git remote add repo repo &&
> +    git config --add remote.pushWhitelisted wlisted1 &&
> +    git config --add remote.pushWhitelisted wlisted2 &&
> +    git config --add remote.pushWhitelisted repo &&
> +    git config --add remote.pushBlacklisted blisted1 &&
> +    git config --add remote.pushBlacklisted blisted2 &&
> +    git config --add remote.pushBlacklisted repo &&
> +    git config --add remote.pushBlacklisted wlisted1/folder/blistedfolder
> +'
> +
> +test_expect_success 'whitelist/blacklist with default pushDefaultPolicy' '
> +    git push wlisted1 HEAD &&
> +    git push wlisted2 HEAD &&
> +    test_must_fail git push blisted1 HEAD &&
> +    test_must_fail git push blisted2 HEAD &&
> +    git push untracked HEAD
> +'
> +
> +test_expect_success 'whitelist/blacklist with allow pushDefaultPolicy' '
> +    test_config remote.pushDefaultPolicy allow &&
> +    git push wlisted1 HEAD &&
> +    git push wlisted2 HEAD &&
> +    test_must_fail git push blisted1 HEAD &&
> +    test_must_fail git push blisted2 HEAD &&
> +    git push untracked HEAD
> +'
> +
> +test_expect_success 'whitelist/blacklist with deny pushDefaultPolicy' '
> +    test_config remote.pushDefaultPolicy deny &&
> +    git push wlisted1 HEAD &&
> +    git push wlisted2 HEAD &&
> +    test_must_fail git push blisted1 HEAD &&
> +    test_must_fail git push blisted2 HEAD &&
> +    test_must_fail git push untracked HEAD
> +'
> +
> +test_expect_success 'remote is whitelisted and blacklisted at the same time exception' '
> +    test_must_fail git push repo HEAD 2> result &&
> +    echo "fatal: repo cannot be whitelisted and blacklisted at the same time" > expected &&
> +    test_cmp result expected
> +'
> +
> +test_expect_success 'remote rejected default message' '
> +    test_must_fail git push blisted1 HEAD 2> result &&
> +    echo "fatal: Pushing to this remote is forbidden" > expected &&
> +    test_cmp result expected
> +'
> +
> +test_expect_success 'remote rejected custom message' '
> +    test_config remote.pushDenyMessage "denied" &&
> +    test_must_fail git push blisted1 HEAD 2> result &&
> +    echo "fatal: denied" > expected &&
> +    test_cmp result expected
> +'
> +
> +test_expect_success 'push accepted/rejected depending on config precision' '
> +    test_config remote.pushDefaultPolicy deny &&
> +    git push wlisted1/folder HEAD &&
> +    test_must_fail git push blisted1/folder HEAD &&
> +    test_must_fail git push wlisted1/folder/blistedfolder/folder HEAD
> +'
> +
> +test_expect_success 'unsetup' '
> +    test_unconfig remote.pushBlackListed &&
> +    test_unconfig remote.pushWhiteListed
> +'
> +
> +test_done
> -- 
> 2.8.2.627.gd450e06.dirty
> 
--
To unsubscribe from this list: send the line "unsubscribe git" in
the body of a message to majordomo@xxxxxxxxxxxxxxx
More majordomo info at  http://vger.kernel.org/majordomo-info.html



[Index of Archives]     [Linux Kernel Development]     [Gcc Help]     [IETF Annouce]     [DCCP]     [Netdev]     [Networking]     [Security]     [V4L]     [Bugtraq]     [Yosemite]     [MIPS Linux]     [ARM Linux]     [Linux Security]     [Linux RAID]     [Linux SCSI]     [Fedora Users]