* [PATCH 1/3] credential: add new interactive config option
2024-09-20 0:00 [PATCH 0/3] maintenance: configure credentials to be silent Derrick Stolee via GitGitGadget
@ 2024-09-20 0:00 ` Derrick Stolee via GitGitGadget
2024-09-20 22:07 ` Junio C Hamano
2024-09-20 0:00 ` [PATCH 2/3] maintenance: add custom config to background jobs Derrick Stolee via GitGitGadget
` (2 subsequent siblings)
3 siblings, 1 reply; 8+ messages in thread
From: Derrick Stolee via GitGitGadget @ 2024-09-20 0:00 UTC (permalink / raw)
To: git
Cc: gitster, liuzhongbo.gg, Johannes.Schindelin, Derrick Stolee,
Derrick Stolee
From: Derrick Stolee <derrickstolee@github.com>
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 <stolee@gmail.com>
---
Documentation/config/credential.txt | 8 ++++++++
credential.c | 30 ++++++++++++++++++++++++++---
t/t5551-http-fetch-smart.sh | 22 +++++++++++++++++++++
3 files changed, 57 insertions(+), 3 deletions(-)
diff --git a/Documentation/config/credential.txt b/Documentation/config/credential.txt
index 0221c3e620d..470482ff4c2 100644
--- a/Documentation/config/credential.txt
+++ b/Documentation/config/credential.txt
@@ -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
diff --git a/credential.c b/credential.c
index ee46351ce01..6dea3859ece 100644
--- a/credential.c
+++ b/credential.c
@@ -13,6 +13,8 @@
#include "strbuf.h"
#include "urlmatch.h"
#include "git-compat-util.h"
+#include "trace2.h"
+#include "repository.h"
void credential_init(struct credential *c)
{
@@ -251,14 +253,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_has_capability(const struct credential_capability *capa,
@@ -501,8 +525,8 @@ void credential_fill(struct credential *c, int all_capabilities)
c->helpers.items[i].string);
}
- credential_getpass(c);
- if (!c->username && !c->password && !c->credential)
+ if (credential_getpass(c) ||
+ (!c->username && !c->password && !c->credential))
die("unable to get password from user");
}
diff --git a/t/t5551-http-fetch-smart.sh b/t/t5551-http-fetch-smart.sh
index 7b5ab0eae16..ceb3336a5c4 100755
--- a/t/t5551-http-fetch-smart.sh
+++ b/t/t5551-http-fetch-smart.sh
@@ -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 &&
--
gitgitgadget
^ permalink raw reply related [flat|nested] 8+ messages in thread
* [PATCH 2/3] maintenance: add custom config to background jobs
2024-09-20 0:00 [PATCH 0/3] maintenance: configure credentials to be silent Derrick Stolee via GitGitGadget
2024-09-20 0:00 ` [PATCH 1/3] credential: add new interactive config option Derrick Stolee via GitGitGadget
@ 2024-09-20 0:00 ` Derrick Stolee via GitGitGadget
2024-09-20 0:00 ` [PATCH 3/3] scalar: configure maintenance during 'reconfigure' Derrick Stolee via GitGitGadget
2024-09-20 21:56 ` [PATCH 0/3] maintenance: configure credentials to be silent Junio C Hamano
3 siblings, 0 replies; 8+ messages in thread
From: Derrick Stolee via GitGitGadget @ 2024-09-20 0:00 UTC (permalink / raw)
To: git
Cc: gitster, liuzhongbo.gg, Johannes.Schindelin, Derrick Stolee,
Derrick Stolee
From: Derrick Stolee <derrickstolee@github.com>
At the moment, some background jobs are getting blocked on credentials
during the 'prefetch' task. This leads to other tasks, such as
incremental repacks, getting blocked. Further, if a user manages to fix
their credentials, then they still need to cancel the background process
before their background maintenance can continue working.
Update the background schedules for our four scheduler integrations to
include these config options via '-c' options:
* 'credential.interactive=false' will stop Git and some credential
helpers from prompting in the UI (assuming the '-c' parameters are
carried through and respected by GCM).
* 'core.askPass=true' will replace the text fallback for a username
and password into the 'true' command, which will return a success in
its exit code, but Git will treat the empty string returned as an
invalid password and move on.
We can do some testing that the credentials are passed, at least in the
systemd case due to writing the service files.
Signed-off-by: Derrick Stolee <stolee@gmail.com>
---
builtin/gc.c | 53 ++++++++++++++++++++++++++++++++++++------
t/t7900-maintenance.sh | 3 +++
2 files changed, 49 insertions(+), 7 deletions(-)
diff --git a/builtin/gc.c b/builtin/gc.c
index 7dac9714054..fb1be542e06 100644
--- a/builtin/gc.c
+++ b/builtin/gc.c
@@ -1766,6 +1766,42 @@ static const char *get_frequency(enum schedule_priority schedule)
}
}
+static const char *extraconfig[] = {
+ "credential.interactive=false",
+ "core.askPass=true", /* 'true' returns success, but no output. */
+ NULL
+};
+
+static const char *get_extra_config_parameters(void) {
+ static const char *result = NULL;
+ struct strbuf builder = STRBUF_INIT;
+
+ if (result)
+ return result;
+
+ for (const char **s = extraconfig; s && *s; s++)
+ strbuf_addf(&builder, "-c %s ", *s);
+
+ result = strbuf_detach(&builder, NULL);
+ return result;
+}
+
+static const char *get_extra_launchctl_strings(void) {
+ static const char *result = NULL;
+ struct strbuf builder = STRBUF_INIT;
+
+ if (result)
+ return result;
+
+ for (const char **s = extraconfig; s && *s; s++) {
+ strbuf_addstr(&builder, "<string>-c</string>\n");
+ strbuf_addf(&builder, "<string>%s</string>\n", *s);
+ }
+
+ result = strbuf_detach(&builder, NULL);
+ return result;
+}
+
/*
* get_schedule_cmd` reads the GIT_TEST_MAINT_SCHEDULER environment variable
* to mock the schedulers that `git maintenance start` rely on.
@@ -1972,6 +2008,7 @@ static int launchctl_schedule_plist(const char *exec_path, enum schedule_priorit
"<array>\n"
"<string>%s/git</string>\n"
"<string>--exec-path=%s</string>\n"
+ "%s" /* For extra config parameters. */
"<string>for-each-repo</string>\n"
"<string>--keep-going</string>\n"
"<string>--config=maintenance.repo</string>\n"
@@ -1981,7 +2018,8 @@ static int launchctl_schedule_plist(const char *exec_path, enum schedule_priorit
"</array>\n"
"<key>StartCalendarInterval</key>\n"
"<array>\n";
- strbuf_addf(&plist, preamble, name, exec_path, exec_path, frequency);
+ strbuf_addf(&plist, preamble, name, exec_path, exec_path,
+ get_extra_launchctl_strings(), frequency);
switch (schedule) {
case SCHEDULE_HOURLY:
@@ -2216,11 +2254,12 @@ static int schtasks_schedule_task(const char *exec_path, enum schedule_priority
"<Actions Context=\"Author\">\n"
"<Exec>\n"
"<Command>\"%s\\headless-git.exe\"</Command>\n"
- "<Arguments>--exec-path=\"%s\" for-each-repo --keep-going --config=maintenance.repo maintenance run --schedule=%s</Arguments>\n"
+ "<Arguments>--exec-path=\"%s\" %s for-each-repo --keep-going --config=maintenance.repo maintenance run --schedule=%s</Arguments>\n"
"</Exec>\n"
"</Actions>\n"
"</Task>\n";
- fprintf(tfile->fp, xml, exec_path, exec_path, frequency);
+ fprintf(tfile->fp, xml, exec_path, exec_path,
+ get_extra_config_parameters(), frequency);
strvec_split(&child.args, cmd);
strvec_pushl(&child.args, "/create", "/tn", name, "/f", "/xml",
get_tempfile_path(tfile), NULL);
@@ -2361,8 +2400,8 @@ static int crontab_update_schedule(int run_maintenance, int fd)
"# replaced in the future by a Git command.\n\n");
strbuf_addf(&line_format,
- "%%d %%s * * %%s \"%s/git\" --exec-path=\"%s\" for-each-repo --keep-going --config=maintenance.repo maintenance run --schedule=%%s\n",
- exec_path, exec_path);
+ "%%d %%s * * %%s \"%s/git\" --exec-path=\"%s\" %s for-each-repo --keep-going --config=maintenance.repo maintenance run --schedule=%%s\n",
+ exec_path, exec_path, get_extra_config_parameters());
fprintf(cron_in, line_format.buf, minute, "1-23", "*", "hourly");
fprintf(cron_in, line_format.buf, minute, "0", "1-6", "daily");
fprintf(cron_in, line_format.buf, minute, "0", "0", "weekly");
@@ -2562,7 +2601,7 @@ static int systemd_timer_write_service_template(const char *exec_path)
"\n"
"[Service]\n"
"Type=oneshot\n"
- "ExecStart=\"%s/git\" --exec-path=\"%s\" for-each-repo --keep-going --config=maintenance.repo maintenance run --schedule=%%i\n"
+ "ExecStart=\"%s/git\" --exec-path=\"%s\" %s for-each-repo --keep-going --config=maintenance.repo maintenance run --schedule=%%i\n"
"LockPersonality=yes\n"
"MemoryDenyWriteExecute=yes\n"
"NoNewPrivileges=yes\n"
@@ -2572,7 +2611,7 @@ static int systemd_timer_write_service_template(const char *exec_path)
"RestrictSUIDSGID=yes\n"
"SystemCallArchitectures=native\n"
"SystemCallFilter=@system-service\n";
- if (fprintf(file, unit, exec_path, exec_path) < 0) {
+ if (fprintf(file, unit, exec_path, exec_path, get_extra_config_parameters()) < 0) {
error(_("failed to write to '%s'"), filename);
fclose(file);
goto error;
diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh
index abae7a97546..3cd7e1fcacb 100755
--- a/t/t7900-maintenance.sh
+++ b/t/t7900-maintenance.sh
@@ -825,6 +825,9 @@ test_expect_success 'start and stop Linux/systemd maintenance' '
test_systemd_analyze_verify "systemd/user/git-maintenance@daily.service" &&
test_systemd_analyze_verify "systemd/user/git-maintenance@weekly.service" &&
+ grep "core.askPass=true" "systemd/user/git-maintenance@.service" &&
+ grep "credential.interactive=false" "systemd/user/git-maintenance@.service" &&
+
printf -- "--user enable --now git-maintenance@%s.timer\n" hourly daily weekly >expect &&
test_cmp expect args &&
--
gitgitgadget
^ permalink raw reply related [flat|nested] 8+ messages in thread