promisor-remote: check advertised name or URL

A previous commit introduced a "promisor.acceptFromServer" configuration variable with only "None" or "All" as valid values. Let's introduce "KnownName" and "KnownUrl" as valid values for this configuration option to give more choice to a client about which promisor remotes it might accept among those that the server advertised. In case of "KnownName", the client will accept promisor remotes which are already configured on the client and have the same name as those advertised by the client. This could be useful in a corporate setup where servers and clients are trusted to not switch names and URLs, but where some kind of control is still useful. In case of "KnownUrl", the client will accept promisor remotes which have both the same name and the same URL configured on the client as the name and URL advertised by the server. This is the most secure option, so it should be used if possible. Signed-off-by: Christian Couder <chriscool@tuxfamily.org> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Christian Couder committed Feb 18, 2025 at 12:32 UTC 36463e32dfef16dc3a903763f14f5bbf6e0248e7
3 files changed +138 -12
Documentation/config/promisor.adoc
+16 -6
@@ -12,9 +12,19 @@ promisor.advertise::
12 promisor.acceptFromServer::
13 If set to "all", a client will accept all the promisor remotes
14 a server might advertise using the "promisor-remote"
15 - capability. Default is "none", which means no promisor remote
16 - advertised by a server will be accepted. By accepting a
17 - promisor remote, the client agrees that the server might omit
18 - objects that are lazily fetchable from this promisor remote
19 - from its responses to "fetch" and "clone" requests from the
20 - client. See linkgit:gitprotocol-v2[5].
15 + capability. If set to "knownName" the client will accept
16 + promisor remotes which are already configured on the client
17 + and have the same name as those advertised by the client. This
18 + is not very secure, but could be used in a corporate setup
19 + where servers and clients are trusted to not switch name and
20 + URLs. If set to "knownUrl", the client will accept promisor
21 + remotes which have both the same name and the same URL
22 + configured on the client as the name and URL advertised by the
23 + server. This is more secure than "all" or "knownName", so it
24 + should be used if possible instead of those options. Default
25 + is "none", which means no promisor remote advertised by a
26 + server will be accepted. By accepting a promisor remote, the
27 + client agrees that the server might omit objects that are
28 + lazily fetchable from this promisor remote from its responses
29 + to "fetch" and "clone" requests from the client. See
30 + linkgit:gitprotocol-v2[5].
promisor-remote.c
+54 -6
@@ -368,30 +368,73 @@ char *promisor_remote_info(struct repository *repo)
368 return strbuf_detach(&sb, NULL);
369 }
370
371 +/*
372 + * Find first index of 'nicks' where there is 'nick'. 'nick' is
373 + * compared case insensitively to the strings in 'nicks'. If not found
374 + * 'nicks->nr' is returned.
375 + */
376 +static size_t remote_nick_find(struct strvec *nicks, const char *nick)
377 +{
378 + for (size_t i = 0; i < nicks->nr; i++)
379 + if (!strcasecmp(nicks->v[i], nick))
380 + return i;
381 + return nicks->nr;
382 +}
383 +
384 enum accept_promisor {
385 ACCEPT_NONE = 0,
386 + ACCEPT_KNOWN_URL,
387 + ACCEPT_KNOWN_NAME,
388 ACCEPT_ALL
389 };
390
391 static int should_accept_remote(enum accept_promisor accept,
377 - const char *remote_name UNUSED,
378 - const char *remote_url UNUSED)
392 + const char *remote_name, const char *remote_url,
393 + struct strvec *names, struct strvec *urls)
394 {
395 + size_t i;
396 +
397 if (accept == ACCEPT_ALL)
398 return 1;
399
383 - BUG("Unhandled 'enum accept_promisor' value '%d'", accept);
400 + i = remote_nick_find(names, remote_name);
401 +
402 + if (i >= names->nr)
403 + /* We don't know about that remote */
404 + return 0;
405 +
406 + if (accept == ACCEPT_KNOWN_NAME)
407 + return 1;
408 +
409 + if (accept != ACCEPT_KNOWN_URL)
410 + BUG("Unhandled 'enum accept_promisor' value '%d'", accept);
411 +
412 + if (!strcmp(urls->v[i], remote_url))
413 + return 1;
414 +
415 + warning(_("known remote named '%s' but with url '%s' instead of '%s'"),
416 + remote_name, urls->v[i], remote_url);
417 +
418 + return 0;
419 }
420
386 -static void filter_promisor_remote(struct strvec *accepted, const char *info)
421 +static void filter_promisor_remote(struct repository *repo,
422 + struct strvec *accepted,
423 + const char *info)
424 {
425 struct strbuf **remotes;
426 const char *accept_str;
427 enum accept_promisor accept = ACCEPT_NONE;
428 + struct strvec names = STRVEC_INIT;
429 + struct strvec urls = STRVEC_INIT;
430
431 if (!git_config_get_string_tmp("promisor.acceptfromserver", &accept_str)) {
432 if (!*accept_str || !strcasecmp("None", accept_str))
433 accept = ACCEPT_NONE;
434 + else if (!strcasecmp("KnownUrl", accept_str))
435 + accept = ACCEPT_KNOWN_URL;
436 + else if (!strcasecmp("KnownName", accept_str))
437 + accept = ACCEPT_KNOWN_NAME;
438 else if (!strcasecmp("All", accept_str))
439 accept = ACCEPT_ALL;
440 else
@@ -402,6 +445,9 @@ static void filter_promisor_remote(struct strvec *accepted, const char *info)
445 if (accept == ACCEPT_NONE)
446 return;
447
448 + if (accept != ACCEPT_ALL)
449 + promisor_info_vecs(repo, &names, &urls);
450 +
451 /* Parse remote info received */
452
453 remotes = strbuf_split_str(info, ';', 0);
@@ -431,7 +477,7 @@ static void filter_promisor_remote(struct strvec *accepted, const char *info)
477 if (remote_url)
478 decoded_url = url_percent_decode(remote_url);
479
434 - if (decoded_name && should_accept_remote(accept, decoded_name, decoded_url))
480 + if (decoded_name && should_accept_remote(accept, decoded_name, decoded_url, &names, &urls))
481 strvec_push(accepted, decoded_name);
482
483 strbuf_list_free(elems);
@@ -439,6 +485,8 @@ static void filter_promisor_remote(struct strvec *accepted, const char *info)
485 free(decoded_url);
486 }
487
488 + strvec_clear(&names);
489 + strvec_clear(&urls);
490 strbuf_list_free(remotes);
491 }
492
@@ -447,7 +495,7 @@ char *promisor_remote_reply(const char *info)
495 struct strvec accepted = STRVEC_INIT;
496 struct strbuf reply = STRBUF_INIT;
497
450 - filter_promisor_remote(&accepted, info);
498 + filter_promisor_remote(the_repository, &accepted, info);
499
500 if (!accepted.nr)
501 return NULL;
t/t5710-promisor-remote-capability.sh
+68
@@ -160,6 +160,74 @@ test_expect_success "init + fetch with promisor.advertise set to 'true'" '
160 check_missing_objects server 1 "$oid"
161 '
162
163 +test_expect_success "clone with promisor.acceptfromserver set to 'KnownName'" '
164 + git -C server config promisor.advertise true &&
165 +
166 + # Clone from server to create a client
167 + GIT_NO_LAZY_FETCH=0 git clone -c remote.lop.promisor=true \
168 + -c remote.lop.fetch="+refs/heads/*:refs/remotes/lop/*" \
169 + -c remote.lop.url="file://$(pwd)/lop" \
170 + -c promisor.acceptfromserver=KnownName \
171 + --no-local --filter="blob:limit=5k" server client &&
172 + test_when_finished "rm -rf client" &&
173 +
174 + # Check that the largest object is still missing on the server
175 + check_missing_objects server 1 "$oid"
176 +'
177 +
178 +test_expect_success "clone with 'KnownName' and different remote names" '
179 + git -C server config promisor.advertise true &&
180 +
181 + # Clone from server to create a client
182 + GIT_NO_LAZY_FETCH=0 git clone -c remote.serverTwo.promisor=true \
183 + -c remote.serverTwo.fetch="+refs/heads/*:refs/remotes/lop/*" \
184 + -c remote.serverTwo.url="file://$(pwd)/lop" \
185 + -c promisor.acceptfromserver=KnownName \
186 + --no-local --filter="blob:limit=5k" server client &&
187 + test_when_finished "rm -rf client" &&
188 +
189 + # Check that the largest object is not missing on the server
190 + check_missing_objects server 0 "" &&
191 +
192 + # Reinitialize server so that the largest object is missing again
193 + initialize_server 1 "$oid"
194 +'
195 +
196 +test_expect_success "clone with promisor.acceptfromserver set to 'KnownUrl'" '
197 + git -C server config promisor.advertise true &&
198 +
199 + # Clone from server to create a client
200 + GIT_NO_LAZY_FETCH=0 git clone -c remote.lop.promisor=true \
201 + -c remote.lop.fetch="+refs/heads/*:refs/remotes/lop/*" \
202 + -c remote.lop.url="file://$(pwd)/lop" \
203 + -c promisor.acceptfromserver=KnownUrl \
204 + --no-local --filter="blob:limit=5k" server client &&
205 + test_when_finished "rm -rf client" &&
206 +
207 + # Check that the largest object is still missing on the server
208 + check_missing_objects server 1 "$oid"
209 +'
210 +
211 +test_expect_success "clone with 'KnownUrl' and different remote urls" '
212 + ln -s lop serverTwo &&
213 +
214 + git -C server config promisor.advertise true &&
215 +
216 + # Clone from server to create a client
217 + GIT_NO_LAZY_FETCH=0 git clone -c remote.lop.promisor=true \
218 + -c remote.lop.fetch="+refs/heads/*:refs/remotes/lop/*" \
219 + -c remote.lop.url="file://$(pwd)/serverTwo" \
220 + -c promisor.acceptfromserver=KnownUrl \
221 + --no-local --filter="blob:limit=5k" server client &&
222 + test_when_finished "rm -rf client" &&
223 +
224 + # Check that the largest object is not missing on the server
225 + check_missing_objects server 0 "" &&
226 +
227 + # Reinitialize server so that the largest object is missing again
228 + initialize_server 1 "$oid"
229 +'
230 +
231 test_expect_success "clone with promisor.advertise set to 'true' but don't delete the client" '
232 git -C server config promisor.advertise true &&
233