fix(clone): a CodeCommit region borrows saved key IDs; Clone runs in the background
An IAM SSH key ID logs in to every region of its AWS account, but a login was saved per host, so a repo in a region with no key ID of its own was refused. Clone now tries the key IDs saved for the other CodeCommit regions (each with its own key; the first ls-remote accepts, or the only one) and keeps that user in the clone's origin. Files → New → Clone repository steps aside for its progress chip as soon as the clone starts; a failure asking for a login reopens the form.
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
* ECREDENTIALS for a remote wanting a login), `/api/me/git` (key made once,
|
||||
* only the public half returned, 0600 on disk; named keys beside the default
|
||||
* ones, SSH logins per alias tied to one key, which a clone then offers
|
||||
* alone), and the member (S3) path of
|
||||
* alone; a CodeCommit region with no login borrowing another region's), and the member (S3) path of
|
||||
* `server/lib/git-identity.js` with run-as and git stubbed — what reaches
|
||||
* git's argv, and that a login typed without "remember" leaves the member's
|
||||
* store as it was.
|
||||
@@ -650,6 +650,44 @@ describe("several accounts: named keys and SSH logins per alias", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("CodeCommit: a region with no login borrows another region's", () => {
|
||||
it("clones as the first saved CodeCommit user the remote accepts, and keeps that user in origin", async () => {
|
||||
const bare = path.join(ROOT, "bare.git");
|
||||
const R = "git-codecommit.ap-southeast-2.amazonaws.com";
|
||||
const saved = [
|
||||
["git-codecommit.ap-southeast-1.amazonaws.com", "APKAONE"],
|
||||
["git-codecommit.us-east-1.amazonaws.com", "APKATWO"],
|
||||
];
|
||||
for (const [host, user] of saved) {
|
||||
assert.equal((await request("PUT", "/api/me/git/ssh-user", { host, user })).status, 200);
|
||||
}
|
||||
// The test forge stands in for the region: APKAONE is refused (no such
|
||||
// repo behind it), APKATWO is the account that owns the repo.
|
||||
const rules = [
|
||||
[`url.file://${ROOT}/nowhere.git.insteadOf`, `ssh://APKAONE@${R}/v1/repos/r`],
|
||||
[`url.file://${bare}.insteadOf`, `ssh://APKATWO@${R}/v1/repos/r`],
|
||||
];
|
||||
for (const [k, v] of rules) execFileSync("git", ["config", "--global", "--add", k, v]);
|
||||
try {
|
||||
const r = await request("POST", "/api/lanes/clone", {
|
||||
url: `ssh://${R}/v1/repos/r`,
|
||||
parent: WORK,
|
||||
name: "borrowed-login",
|
||||
});
|
||||
assert.equal(r.status, 201, JSON.stringify(r.body));
|
||||
const origin = execFileSync(
|
||||
"git",
|
||||
["-C", path.join(WORK, "borrowed-login"), "config", "remote.origin.url"],
|
||||
{ encoding: "utf8" }
|
||||
).trim();
|
||||
assert.equal(origin, `ssh://APKATWO@${R}/v1/repos/r`);
|
||||
} finally {
|
||||
for (const [k] of rules) execFileSync("git", ["config", "--global", "--unset-all", k]);
|
||||
for (const [host] of saved) await request("DELETE", `/api/me/git/ssh-user/${host}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("git-identity, member account (S3, stubbed)", () => {
|
||||
const runAs = require("../lib/run-as");
|
||||
const worktree = require("../lib/worktree");
|
||||
|
||||
@@ -715,6 +715,42 @@ async function linkedKeyFile(account, url) {
|
||||
return row ? (currentKeys(account).find((k) => k.id === row.key)?.file ?? null) : null;
|
||||
}
|
||||
|
||||
// AWS CodeCommit's SSH key ID belongs to an IAM user, so it logs in to EVERY
|
||||
// region of that AWS account — but our login rules are per host, and a repo
|
||||
// in a region the user never saved stayed refused. Such a clone borrows the
|
||||
// logins saved for the other CodeCommit regions (not aliases' hosts only: an
|
||||
// alias row's user is as valid), each with its own key when it names one.
|
||||
const CODECOMMIT_RE = /^git-codecommit\.[a-z0-9-]+\.amazonaws\.com$/;
|
||||
|
||||
/** `[{url, keyFile}]`: `url` with each borrowable CodeCommit user in it. */
|
||||
async function borrowedLogins(account, url) {
|
||||
if (!url.startsWith("ssh://")) return [];
|
||||
let u;
|
||||
try {
|
||||
u = new URL(url);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
const host = u.host.toLowerCase();
|
||||
if (u.username || !CODECOMMIT_RE.test(host)) return [];
|
||||
const rows = await listSshUsers(account);
|
||||
// The region has a login of its own: git's insteadOf already applies it.
|
||||
if (rows.some((r) => (r.alias ?? r.host) === host)) return [];
|
||||
const keys = currentKeys(account);
|
||||
const seen = new Set();
|
||||
const out = [];
|
||||
for (const r of rows) {
|
||||
if (!CODECOMMIT_RE.test(r.host)) continue;
|
||||
const keyFile = r.key ? (keys.find((k) => k.id === r.key)?.file ?? null) : null;
|
||||
if (r.key && !keyFile) continue;
|
||||
const id = `${r.user}\0${keyFile ?? ""}`;
|
||||
if (seen.has(id)) continue;
|
||||
seen.add(id);
|
||||
out.push({ url: `ssh://${r.user}@${host}${u.pathname}`, keyFile });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// ── Network commands in an existing repo (git bar fetch/pull/push) ─────────
|
||||
|
||||
/**
|
||||
@@ -878,12 +914,35 @@ async function cloneNow({ url, parent, name, once = null, partial = false }, thr
|
||||
if (once) throw err("EBADCRED", "a typed login is for https only — SSH uses your key");
|
||||
// A login naming its key (another AWS account, a second GitHub user)
|
||||
// offers that key ONLY — another key could log in as the wrong account.
|
||||
const linked = await linkedKeyFile(account, url);
|
||||
let linked = await linkedKeyFile(account, url);
|
||||
const sshFor = (file) => {
|
||||
const k = file ? [file] : keyFiles(account);
|
||||
const b = k.length ? `ssh${identityArgs(k)} -o IdentitiesOnly=yes` : "ssh";
|
||||
// BatchMode: no terminal to answer a prompt; accept-new: first contact
|
||||
// records the host key, a CHANGED one still fails.
|
||||
return `core.sshCommand=${b} -o BatchMode=yes -o StrictHostKeyChecking=accept-new`;
|
||||
};
|
||||
// A CodeCommit region with no login of its own: the first borrowed login
|
||||
// the remote accepts (one is taken as is). The clone's URL then names
|
||||
// that user, so the repo's `origin` keeps logging in the same way.
|
||||
const borrowed = await borrowedLogins(account, url);
|
||||
for (const b of borrowed) {
|
||||
const ok =
|
||||
borrowed.length === 1 ||
|
||||
(await git(parent, ["-c", sshFor(b.keyFile), "ls-remote", "--", b.url, "HEAD"], {
|
||||
timeout: 60_000,
|
||||
}).then(
|
||||
() => true,
|
||||
() => false
|
||||
));
|
||||
if (ok) {
|
||||
url = b.url;
|
||||
linked = b.keyFile;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const keys = linked ? [linked] : keyFiles(account);
|
||||
const base = keys.length ? `ssh${identityArgs(keys)} -o IdentitiesOnly=yes` : "ssh";
|
||||
// BatchMode: no terminal to answer a prompt; accept-new: first contact
|
||||
// records the host key, a CHANGED one still fails.
|
||||
pre.push("-c", `core.sshCommand=${base} -o BatchMode=yes -o StrictHostKeyChecking=accept-new`);
|
||||
pre.push("-c", sshFor(linked));
|
||||
if (keys.length) {
|
||||
// Persisted by FILE name — both default types plus every named key:
|
||||
// a default key made later is offered too (ssh skips a missing
|
||||
|
||||
Reference in New Issue
Block a user