Skip to content

Commit

Permalink
credential: add new interactive config option
Browse files Browse the repository at this point in the history
When scripts or background maintenance wish to perform HTTP(S) requests,
there is a risk that our stored credentials might be invalid. At the
moment, this causes the credential helper to ping the user and block the
process. Even if the credential helper does not ping the user, Git falls
back to the 'askpass' method, which includes a direct ping to the user
via the terminal.

Even setting the 'core.askPass' config as something like 'echo' will
causes Git to fallback to a terminal prompt. It uses
git_terminal_prompt(), which finds the terminal from the environment and
ignores whether stdin has been redirected. This can also block the
process awaiting input.

Create a new config option to prevent user interaction, favoring a
failure to a blocked process.

The chosen name, 'credential.interactive', is taken from the config
option used by Git Credential Manager to already avoid user
interactivity, so there is already one credential helper that integrates
with this option. However, older versions of Git Credential Manager also
accepted other string values, including 'auto', 'never', and 'always'.
The modern use is to use a boolean value, but we should still be
careful that some users could have these non-booleans. Further, we
should respect 'never' the same as 'false'. This is respected by the
implementation and test, but not mentioned in the documentation.

The implementation for the Git interactions takes place within
credential_getpass(). The method prototype is modified to return an
'int' instead of 'void'. This allows us to detect that no attempt was
made to fill the given credential, changing the single caller slightly.

Also, a new trace2 region is added around the interactive portion of the
credential request. This provides a way to measure the amount of time
spent in that region for commands that _are_ interactive. It also makes
a conventient way to test that the config option works with
'test_region'.

Signed-off-by: Derrick Stolee <derrickstolee@github.com>
  • Loading branch information
derrickstolee authored and dscho committed Nov 14, 2023
1 parent 58922e4 commit e30a55e
Show file tree
Hide file tree
Showing 3 changed files with 57 additions and 3 deletions.
8 changes: 8 additions & 0 deletions Documentation/config/credential.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ credential.helper::
Note that multiple helpers may be defined. See linkgit:gitcredentials[7]
for details and examples.

credential.interactive::
By default, Git and any configured credential helpers will ask for
user input when new credentials are required. Many of these helpers
will succeed based on stored credentials if those credentials are
still valid. To avoid the possibility of user interactivity from
Git, set `credential.interactive=false`. Some credential helpers
respect this option as well.

credential.useHttpPath::
When acquiring credentials, consider the "path" component of an http
or https URL to be important. Defaults to false. See
Expand Down
30 changes: 27 additions & 3 deletions credential.c
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
#include "strbuf.h"
#include "urlmatch.h"
#include "git-compat-util.h"
#include "trace2.h"
#include "repository.h"

void credential_init(struct credential *c)
{
Expand Down Expand Up @@ -198,14 +200,36 @@ static char *credential_ask_one(const char *what, struct credential *c,
return xstrdup(r);
}

static void credential_getpass(struct credential *c)
static int credential_getpass(struct credential *c)
{
int interactive;
char *value;
if (!git_config_get_maybe_bool("credential.interactive", &interactive) &&
!interactive) {
trace2_data_intmax("credential", the_repository,
"interactive/skipped", 1);
return -1;
}
if (!git_config_get_string("credential.interactive", &value)) {
int same = !strcmp(value, "never");
free(value);
if (same) {
trace2_data_intmax("credential", the_repository,
"interactive/skipped", 1);
return -1;
}
}

trace2_region_enter("credential", "interactive", the_repository);
if (!c->username)
c->username = credential_ask_one("Username", c,
PROMPT_ASKPASS|PROMPT_ECHO);
if (!c->password)
c->password = credential_ask_one("Password", c,
PROMPT_ASKPASS);
trace2_region_leave("credential", "interactive", the_repository);

return 0;
}

int credential_read(struct credential *c, FILE *fp)
Expand Down Expand Up @@ -383,8 +407,8 @@ void credential_fill(struct credential *c)
c->helpers.items[i].string);
}

credential_getpass(c);
if (!c->username && !c->password)
if (credential_getpass(c) ||
(!c->username && !c->password))
die("unable to get password from user");
}

Expand Down
22 changes: 22 additions & 0 deletions t/t5551-http-fetch-smart.sh
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,28 @@ test_expect_success 'clone from password-protected repository' '
test_cmp expect actual
'

test_expect_success 'credential.interactive=false skips askpass' '
set_askpass bogus nonsense &&
(
GIT_TRACE2_EVENT="$(pwd)/interactive-true" &&
export GIT_TRACE2_EVENT &&
test_must_fail git clone --bare "$HTTPD_URL/auth/smart/repo.git" interactive-true-dir &&
test_region credential interactive interactive-true &&
GIT_TRACE2_EVENT="$(pwd)/interactive-false" &&
export GIT_TRACE2_EVENT &&
test_must_fail git -c credential.interactive=false \
clone --bare "$HTTPD_URL/auth/smart/repo.git" interactive-false-dir &&
test_region ! credential interactive interactive-false &&
GIT_TRACE2_EVENT="$(pwd)/interactive-never" &&
export GIT_TRACE2_EVENT &&
test_must_fail git -c credential.interactive=never \
clone --bare "$HTTPD_URL/auth/smart/repo.git" interactive-never-dir &&
test_region ! credential interactive interactive-never
)
'

test_expect_success 'clone from auth-only-for-push repository' '
echo two >expect &&
set_askpass wrong &&
Expand Down

0 comments on commit e30a55e

Please sign in to comment.