-- SPDX-FileCopyrightText: © 2022—2026 Vladimir Zorin -- SPDX-License-Identifier: LicenseRef-OWL-1.0-or-later -- Licensed under OWL v1.0+. See LICENSE. -- Credential checking and config validation for private repositories; no -- git fixture and no server involved. local testimony = require("testimony") local crypto = require("crypto") local auth = require("lilgit.auth") local config = require("lilgit.config") local testify = testimony.new("== lilgit.auth ==") local auth_cfg = function(private) return { realm = "test realm", users = { alice = "hunter2", bob = "pass:with:colons" }, private = private, } end local basic = function(credentials) return { authorization = "Basic " .. crypto.b64_encode(credentials) } end testify:that("no private repos means no auth instance", function() testimony.assert_nil(auth.new(auth_cfg({}))) testimony.assert_nil(auth.new(nil)) end) testify:that("correct credentials authorize", function() local gate = auth.new(auth_cfg({ "secret" })) testimony.assert_true(gate:authorize(basic("alice:hunter2"))) -- A password may contain colons; only the first one separates. testimony.assert_true(gate:authorize(basic("bob:pass:with:colons"))) end) testify:that("wrong password, unknown user and missing header are refused", function() local gate = auth.new(auth_cfg({ "secret" })) testimony.assert_false(gate:authorize(basic("alice:hunter3"))) testimony.assert_false(gate:authorize(basic("alice:"))) testimony.assert_false(gate:authorize(basic("mallory:hunter2"))) testimony.assert_false(gate:authorize({})) testimony.assert_false(gate:authorize(nil)) end) testify:that("malformed authorization headers are refused", function() local gate = auth.new(auth_cfg({ "secret" })) for _, header in ipairs({ "Bearer " .. crypto.b64_encode("alice:hunter2"), "Basic", "Basic " .. crypto.b64_encode("alice"), -- no colon "Basic !!!not base64!!!", "Basic ", crypto.b64_encode("alice:hunter2"), -- no scheme }) do testimony.assert_false(gate:authorize({ authorization = header }), "accepted: " .. header) end -- The scheme token is case-insensitive. testimony.assert_true(gate:authorize({ authorization = "bAsIc " .. crypto.b64_encode("alice:hunter2") })) end) testify:that("challenge is a 401 naming the realm", function() local gate = auth.new(auth_cfg({ "secret" })) local content, status, headers = gate:challenge() testimony.assert_equal(401, status) testimony.assert_equal("Unauthorized", content) testimony.assert_equal('Basic realm="test realm", charset="UTF-8"', headers["www-authenticate"]) testimony.assert_equal("no-store", headers["cache-control"]) end) testify:that("mark flags registry entries and warns on unknown names", function() local warnings = {} local logger = { log = function(_, msg) warnings[#warnings + 1] = msg.repo end, } local registry = { by_name = { ["cat/secret"] = { name = "cat/secret" }, public = { name = "public" } } } auth.new(auth_cfg({ "cat/secret", "typo" })):mark(registry, logger) testimony.assert_true(registry.by_name["cat/secret"].private) testimony.assert_nil(registry.by_name.public.private) testimony.assert_equal(1, #warnings) testimony.assert_equal("typo", warnings[1]) end) testify:that("config.validate: auth section", function() local cfg_with = function(section) return { repos_dir = "/srv/git", repos = {}, markdown_cache = 0, auth = section } end testimony.assert_not_nil((config.validate(cfg_with(auth_cfg({ "secret" }))))) testimony.assert_not_nil((config.validate(cfg_with(auth_cfg({}))))) -- An absent section means auth is off. testimony.assert_not_nil((config.validate(cfg_with(nil)))) local bad = { { realm = "", users = {}, private = {} }, { realm = 'say "hi"', users = {}, private = {} }, { realm = "r", users = "alice", private = {} }, { realm = "r", users = { [""] = "p" }, private = {} }, { realm = "r", users = { ["a:b"] = "p" }, private = {} }, { realm = "r", users = { alice = "" }, private = {} }, { realm = "r", users = { alice = 42 }, private = {} }, { realm = "r", users = { alice = "p" }, private = { "a/b/c" } }, { realm = "r", users = { alice = "p" }, private = { "_" } }, { realm = "r", users = { alice = "p" }, private = { 42 } }, -- Private repos with nobody able to reach them. { realm = "r", users = {}, private = { "secret" } }, } for i, section in ipairs(bad) do testimony.assert_nil((config.validate(cfg_with(section))), "accepted invalid auth section " .. i) end end) testify:conclude()