diff --git a/scripts/install-apt.sh b/scripts/install-apt.sh index a7ee06e..ab9038f 100755 --- a/scripts/install-apt.sh +++ b/scripts/install-apt.sh @@ -18,12 +18,21 @@ FORGE_URL="${FORGE_URL:-https://forgejo.heavyduty.builders}" OWNER="${OWNER:-heavy-duty}" DISTRIBUTION="${DISTRIBUTION:-stable}" COMPONENT="${COMPONENT:-main}" +FORGE_USER="${FORGE_USER:-}" +FORGE_TOKEN="${FORGE_TOKEN:-}" # Where apt configuration lives; overridable so tests can run against a # throwaway directory instead of the real /etc/apt. APT_ETC="${STOKE_APT_ETC:-/etc/apt}" KEYRING="$APT_ETC/keyrings/forgejo-$OWNER.asc" LIST="$APT_ETC/sources.list.d/forgejo-$OWNER.list" +AUTH="$APT_ETC/auth.conf.d/forgejo-$OWNER.conf" + +if { [ -n "$FORGE_USER" ] && [ -z "$FORGE_TOKEN" ]; } \ + || { [ -z "$FORGE_USER" ] && [ -n "$FORGE_TOKEN" ]; }; then + echo "error: FORGE_USER and FORGE_TOKEN must be set together" >&2 + exit 1 +fi SUDO="" if [ "$(id -u)" -ne 0 ]; then @@ -31,6 +40,18 @@ if [ "$(id -u)" -ne 0 ]; then SUDO="sudo" fi +CURL_AUTH=() +if [ -n "$FORGE_USER" ] && [ -n "$FORGE_TOKEN" ]; then + forge_host="${FORGE_URL#*://}" + forge_host="${forge_host%%/*}" + $SUDO install -d -m 0755 "$APT_ETC/auth.conf.d" + printf 'machine %s\nlogin %s\npassword %s\n' \ + "$forge_host" "$FORGE_USER" "$FORGE_TOKEN" \ + | $SUDO tee "$AUTH" >/dev/null + $SUDO chmod 0600 "$AUTH" + CURL_AUTH=(--netrc-file "$AUTH") +fi + update_only_source() { $SUDO apt-get update \ -o Dir::Etc::sourcelist="$1" \ @@ -81,7 +102,7 @@ ensure_nodejs_source() { echo "Adding APT source for $FORGE_URL/$OWNER ..." $SUDO install -d -m 0755 "$APT_ETC/keyrings" -curl -fsSL "$FORGE_URL/api/packages/$OWNER/debian/repository.key" | $SUDO tee "$KEYRING" >/dev/null +curl "${CURL_AUTH[@]}" -fsSL "$FORGE_URL/api/packages/$OWNER/debian/repository.key" | $SUDO tee "$KEYRING" >/dev/null echo "deb [signed-by=$KEYRING] $FORGE_URL/api/packages/$OWNER/debian $DISTRIBUTION $COMPONENT" \ | $SUDO tee "$LIST" >/dev/null # tee inherits our umask; apt's unprivileged _apt user must be able to @@ -94,7 +115,7 @@ $SUDO chmod 0644 "$KEYRING" "$LIST" # is fatal; any other curl outcome (e.g. a network hiccup) is left for # apt-get update to report. RELEASE_URL="$FORGE_URL/api/packages/$OWNER/debian/dists/$DISTRIBUTION/Release" -if [ "$(curl -sSL -o /dev/null -w '%{http_code}' "$RELEASE_URL" || true)" = "404" ]; then +if [ "$(curl "${CURL_AUTH[@]}" -sSL -o /dev/null -w '%{http_code}' "$RELEASE_URL" || true)" = "404" ]; then echo "error: no stoke package has been published to the $OWNER Debian registry yet" >&2 echo "($RELEASE_URL returned 404)." >&2 echo "Install stoke via npm or manually instead — see the README." >&2 diff --git a/test/install-apt.test.js b/test/install-apt.test.js index aea7648..3c6529e 100644 --- a/test/install-apt.test.js +++ b/test/install-apt.test.js @@ -14,12 +14,13 @@ const SCRIPT = path.join(__dirname, '..', 'scripts', 'install-apt.sh'); // candAfterNodesource Candidate after an update once nodesource.list exists // releaseStatus HTTP status curl reports for the registry Release file // sourceUpdateError stderr and exit 100 for the first signed stoke update +// forgeUser/token private-registry credentials // The apt-cache stub localizes the "Candidate:" label unless LC_ALL=C is set, // so every scenario doubles as a regression test for locale-safe parsing. const cleanups = []; process.on('exit', () => { for (const dir of cleanups) fs.rmSync(dir, { recursive: true, force: true }); }); -function runScenario({ candInitial, candAfterUpdate, candAfterNodesource, preexistingNodesourceList, releaseStatus, sourceUpdateError }) { +function runScenario({ candInitial, candAfterUpdate, candAfterNodesource, preexistingNodesourceList, releaseStatus, sourceUpdateError, forgeUser, forgeToken }) { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'stoke-apt-test-')); cleanups.push(root); const bin = path.join(root, 'bin'); @@ -92,6 +93,8 @@ function runScenario({ candInitial, candAfterUpdate, candAfterNodesource, preexi CAND_AFTER_NODESOURCE: candAfterNodesource || '', RELEASE_STATUS: releaseStatus || '', SOURCE_UPDATE_ERROR: sourceUpdateError || '', + FORGE_USER: forgeUser || '', + FORGE_TOKEN: forgeToken || '', LC_ALL: 'es_ES.UTF-8', // localized environment; the script must force C }, }); @@ -107,6 +110,8 @@ function runScenario({ candInitial, candAfterUpdate, candAfterNodesource, preexi nodesourceKeyMode: mode(path.join(aptEtc, 'keyrings', 'nodesource.asc')), forgeKeyMode: mode(path.join(aptEtc, 'keyrings', 'forgejo-heavy-duty.asc')), forgeList: read(path.join(aptEtc, 'sources.list.d', 'forgejo-heavy-duty.list')), + forgeAuth: read(path.join(aptEtc, 'auth.conf.d', 'forgejo-heavy-duty.conf')), + forgeAuthMode: mode(path.join(aptEtc, 'auth.conf.d', 'forgejo-heavy-duty.conf')), aptGetLog: read(path.join(state, 'apt-get.log')) || '', }; // Drop the throwaway tree after we have read everything we need. @@ -205,3 +210,31 @@ test('network update failure stays fatal and never disables signature verificati assert.doesNotMatch(s.forgeList, /trusted=yes/); assert.doesNotMatch(s.aptGetLog, /install -y stoke/); }); + +test('private-registry credentials stay in a root-readable auth file, not the source URL', () => { + const s = runScenario({ + candInitial: '22.23.1-1nodesource1', + forgeUser: 'apt-user', + forgeToken: 'secret-token', + }); + assert.equal(s.res.status, 0, s.res.stderr); + assert.equal(s.forgeAuthMode, 0o600); + assert.equal(s.forgeAuth, [ + 'machine forgejo.heavyduty.builders', + 'login apt-user', + 'password secret-token', + '', + ].join('\n')); + assert.doesNotMatch(s.forgeList, /apt-user|secret-token/); +}); + +test('incomplete private-registry credentials fail before configuring apt', () => { + const s = runScenario({ + candInitial: '22.23.1-1nodesource1', + forgeUser: 'apt-user', + }); + assert.notEqual(s.res.status, 0); + assert.match(s.res.stderr, /FORGE_USER and FORGE_TOKEN must be set together/); + assert.equal(s.forgeList, null); + assert.equal(s.aptGetLog, ''); +});