CLI: positional rp-id/rp-name; init adds domains to an existing database
- 'paskia init [rp-id] [rp-name]' and 'paskia migrate [rp-id]' are now positional; comma separation and the --rp-id/--rp-name flags are gone. - With an existing paskia.kantadb, init adds the rp-id as a new domain (seeding its OIDC provider) or updates an existing domain's rp-name. - Origin allow-list semantics clarified: the bare '*' entry allows anything within the rp-id domain on any scheme and port (also the empty-list default and its display in the admin UI, replacing the synthetic '*.rp-id' row); '*.x' wildcards are https-only; exact entries match scheme, host and port. Legacy '*.rp-id' wildcards migrate to '*' to preserve their any-scheme meaning.
This commit is contained in:
+36
-19
@@ -20,7 +20,7 @@ from kanta import Kanta
|
||||
|
||||
from paskia.__main__ import _load_stored_config, main
|
||||
from paskia.db import legacy
|
||||
from paskia.db.structs import Config
|
||||
from paskia.db.structs import DB, Config
|
||||
from paskia.util.runtime import ServeConfig, clear_cache
|
||||
|
||||
|
||||
@@ -92,15 +92,7 @@ def test_init_defaults(run_cli, tmp_path):
|
||||
|
||||
|
||||
def test_init_full_options(run_cli, tmp_path):
|
||||
run_cli(
|
||||
"init",
|
||||
"--rp-id",
|
||||
"example.com",
|
||||
"--rp-name",
|
||||
"Example Corp",
|
||||
"--listen",
|
||||
"4402",
|
||||
)
|
||||
run_cli("init", "example.com", "Example Corp", "--listen", "4402")
|
||||
|
||||
config = stored_config(tmp_path)
|
||||
domain = config.domains["example.com"]
|
||||
@@ -109,16 +101,41 @@ def test_init_full_options(run_cli, tmp_path):
|
||||
assert config.listen == ["4402"]
|
||||
|
||||
|
||||
def test_init_multiple_rp_ids(run_cli, tmp_path):
|
||||
run_cli("init", "--rp-id", "company.com,app.com", "--rp-id", "pro.com")
|
||||
def test_init_adds_domains_to_existing_database(run_cli, tmp_path):
|
||||
"""Further rp-ids are added by repeating init; no comma separation."""
|
||||
run_cli("init", "company.com")
|
||||
run_cli("init", "app.com")
|
||||
run_cli("init", "pro.com", "Pro Corp")
|
||||
|
||||
config = stored_config(tmp_path)
|
||||
assert list(config.domains) == ["company.com", "app.com", "pro.com"]
|
||||
assert config.domains["pro.com"].rp_name == "Pro Corp"
|
||||
# OIDC providers seeded for the added domains
|
||||
assert set(converted_oidc(tmp_path)) == {"company.com", "app.com", "pro.com"}
|
||||
|
||||
|
||||
def test_init_refuses_existing_database(run_cli):
|
||||
def converted_oidc(tmp_path):
|
||||
async def _read():
|
||||
new_db = DB()
|
||||
kanta = Kanta(str(tmp_path / "paskia.kantadb"), new_db)
|
||||
await kanta.open(readonly=True)
|
||||
try:
|
||||
return set(kanta.data.oidc)
|
||||
finally:
|
||||
await kanta.close()
|
||||
|
||||
return asyncio.run(_read())
|
||||
|
||||
|
||||
def test_init_updates_rp_name_of_existing_domain(run_cli, tmp_path):
|
||||
run_cli("init", "example.com", "Old Name")
|
||||
run_cli("init", "example.com", "New Name")
|
||||
assert stored_config(tmp_path).domains["example.com"].rp_name == "New Name"
|
||||
|
||||
|
||||
def test_init_noop_on_existing_domain(run_cli):
|
||||
run_cli("init")
|
||||
with pytest.raises(SystemExit):
|
||||
with pytest.raises(SystemExit, match="already configured"):
|
||||
run_cli("init")
|
||||
|
||||
|
||||
@@ -131,7 +148,7 @@ def test_init_refuses_legacy_database(run_cli, tmp_path):
|
||||
def test_init_rejects_removed_options(run_cli):
|
||||
"""Origins and auth hosts are admin-interface configuration, not init's."""
|
||||
with pytest.raises(SystemExit):
|
||||
run_cli("init", "--rp-id", "example.com", "--auth-host", "auth.example.com")
|
||||
run_cli("init", "example.com", "--auth-host", "auth.example.com")
|
||||
with pytest.raises(SystemExit):
|
||||
run_cli("init", "--origin", "https://app.example.com")
|
||||
|
||||
@@ -142,7 +159,7 @@ def test_serve_requires_database(run_cli):
|
||||
|
||||
|
||||
def test_serve_uses_stored_config(run_cli, tmp_path):
|
||||
run_cli("init", "--rp-id", "example.com", "--rp-name", "Stored Name")
|
||||
run_cli("init", "example.com", "Stored Name")
|
||||
calls = run_cli()
|
||||
|
||||
assert calls["app"] == "paskia.fastapi.mainapp:app"
|
||||
@@ -197,7 +214,7 @@ def test_migrate_converts_legacy_database(run_cli, tmp_path):
|
||||
def test_migrate_multiple_legacy_databases_require_rp_id(run_cli, tmp_path):
|
||||
write_legacy_db(tmp_path, legacy.LegacyConfig(rp_id="one.com"))
|
||||
write_legacy_db(tmp_path, legacy.LegacyConfig(rp_id="two.com"))
|
||||
with pytest.raises(SystemExit, match="--rp-id"):
|
||||
with pytest.raises(SystemExit, match="paskia migrate"):
|
||||
run_cli("migrate")
|
||||
|
||||
|
||||
@@ -205,7 +222,7 @@ def test_migrate_explicit_rp_id_selects_candidate(run_cli, tmp_path):
|
||||
write_legacy_db(tmp_path, legacy.LegacyConfig(rp_id="one.com"))
|
||||
write_legacy_db(tmp_path, legacy.LegacyConfig(rp_id="two.com"))
|
||||
|
||||
run_cli("migrate", "--rp-id", "two.com")
|
||||
run_cli("migrate", "two.com")
|
||||
|
||||
config = stored_config(tmp_path)
|
||||
assert list(config.domains) == ["two.com"]
|
||||
@@ -217,7 +234,7 @@ def test_migrate_explicit_rp_id_selects_candidate(run_cli, tmp_path):
|
||||
def test_migrate_unknown_rp_id(run_cli, tmp_path):
|
||||
write_legacy_db(tmp_path, legacy.LegacyConfig(rp_id="one.com"))
|
||||
with pytest.raises(SystemExit, match="nope.com.paskiadb"):
|
||||
run_cli("migrate", "--rp-id", "nope.com")
|
||||
run_cli("migrate", "nope.com")
|
||||
|
||||
|
||||
def test_migrate_refuses_existing_database(run_cli):
|
||||
|
||||
+42
-2
@@ -227,6 +227,21 @@ class TestValidateConfig:
|
||||
)
|
||||
)
|
||||
|
||||
def test_star_origin_accepted_not_auth_host(self):
|
||||
domains.validate_config(
|
||||
Config(domains={"a.com": DomainConfig(origins={"*": True})})
|
||||
)
|
||||
with pytest.raises(ValueError, match="cannot be the auth host"):
|
||||
domains.validate_config(
|
||||
Config(
|
||||
domains={
|
||||
"a.com": DomainConfig(
|
||||
origins={"*": OriginEntry(auth_host=True)}
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
def test_auth_host_collision(self):
|
||||
with pytest.raises(ValueError, match="collides with a related origin"):
|
||||
domains.validate_config(
|
||||
@@ -417,6 +432,30 @@ class TestOriginValidation:
|
||||
with pytest.raises(ValueError, match="not allowed"):
|
||||
p.validate_origin("https://other.com")
|
||||
|
||||
def test_wildcard_is_https_only(self):
|
||||
"""A '*.example.com' entry does not fall back to other schemes."""
|
||||
p = Passkey(rp_id="example.com", origins=["*.example.com"])
|
||||
with pytest.raises(ValueError, match="not allowed"):
|
||||
p.validate_origin("http://example.com")
|
||||
with pytest.raises(ValueError, match="not allowed"):
|
||||
p.validate_origin("http://app.example.com:8080")
|
||||
|
||||
def test_star_entry_matches_any_scheme_and_port(self):
|
||||
"""The bare '*' entry allows anything within the rp-id domain."""
|
||||
p = Passkey(rp_id="example.com", origins=["*"])
|
||||
assert p.validate_origin("https://example.com")
|
||||
assert p.validate_origin("http://app.example.com:8080")
|
||||
with pytest.raises(ValueError, match="not allowed"):
|
||||
p.validate_origin("https://other.com")
|
||||
|
||||
def test_exact_entry_matches_scheme_and_port(self):
|
||||
p = Passkey(rp_id="localhost", origins=["http://localhost:4403"])
|
||||
assert p.validate_origin("http://localhost:4403")
|
||||
with pytest.raises(ValueError, match="not allowed"):
|
||||
p.validate_origin("https://localhost:4403")
|
||||
with pytest.raises(ValueError, match="not allowed"):
|
||||
p.validate_origin("http://localhost:4404")
|
||||
|
||||
def test_sub_wildcard_matches_only_its_subtree(self):
|
||||
p = Passkey(rp_id="example.com", origins=["*.app.example.com"])
|
||||
assert p.validate_origin("https://app.example.com")
|
||||
@@ -643,7 +682,7 @@ class TestLegacyConversion:
|
||||
kanta.data.config = LegacyConfig(
|
||||
rp_id="example.com",
|
||||
rp_name="Example",
|
||||
origins=["https://app.example.com"],
|
||||
origins=["https://app.example.com", "*.example.com"],
|
||||
)
|
||||
kanta.data.credentials[cred_uuid] = LegacyCredential(
|
||||
credential_id=b"credential-id",
|
||||
@@ -668,7 +707,8 @@ class TestLegacyConversion:
|
||||
config = convert_legacy_database(src_file, tmp_path / "paskia.kantadb")
|
||||
domain = config.domains["example.com"]
|
||||
assert domain.rp_name == "Example"
|
||||
assert domain.origins == {"app.example.com": True}
|
||||
# A legacy wildcard over the rp-id itself becomes the bare '*'
|
||||
assert domain.origins == {"app.example.com": True, "*": True}
|
||||
|
||||
converted = _read_db(tmp_path / "paskia.kantadb")
|
||||
assert converted.credentials[cred_uuid].rp_id == "example.com"
|
||||
|
||||
Reference in New Issue
Block a user