mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-11-12 12:05:07 +00:00
feat(kv): accept an sqlite vault file path and read plain-tag tables on copy
A kv vault argument can be the path to an SQLite vault file. With -s, kv copy reads the VAULT_TAG table and the plain-tag table into the VAULT_TAG table.
This commit is contained in:
@@ -429,6 +429,8 @@ unshackle kv COMMAND [ARGS]...
|
|||||||
|
|
||||||
Copy content keys from one or more source vaults into a destination vault. unshackle skips rows whose KIDs match, unless the existing row has no content key. unshackle never alters or deletes existing data.
|
Copy content keys from one or more source vaults into a destination vault. unshackle skips rows whose KIDs match, unless the existing row has no content key. unshackle never alters or deletes existing data.
|
||||||
|
|
||||||
|
A vault argument is the name of a vault in the config, or the path to an SQLite vault file. Use a path to import a vault file that is not in the config.
|
||||||
|
|
||||||
| Option | Description |
|
| Option | Description |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `-s`, `--service` | Only copy data for a specific service. |
|
| `-s`, `--service` | Only copy data for a specific service. |
|
||||||
@@ -462,6 +464,7 @@ Make service tables on vaults that use tables, for every installed service, wher
|
|||||||
```shell
|
```shell
|
||||||
unshackle kv copy main backup1 backup2
|
unshackle kv copy main backup1 backup2
|
||||||
unshackle kv copy main backup -s EXAMPLE
|
unshackle kv copy main backup -s EXAMPLE
|
||||||
|
unshackle kv copy main ~/Downloads/key_store.db -s EXAMPLE
|
||||||
unshackle kv sync main mysql_vault
|
unshackle kv sync main mysql_vault
|
||||||
unshackle kv add keys.txt EXAMPLE main backup
|
unshackle kv add keys.txt EXAMPLE main backup
|
||||||
unshackle kv search 0123456789abcdef0123456789abcdef -s EXAMPLE -v main
|
unshackle kv search 0123456789abcdef0123456789abcdef -s EXAMPLE -v main
|
||||||
|
|||||||
@@ -317,6 +317,16 @@ $ unshackle kv copy local shared community
|
|||||||
|
|
||||||
`--service` and `--local-only` are mutually exclusive.
|
`--service` and `--local-only` are mutually exclusive.
|
||||||
|
|
||||||
|
To import an SQLite vault file that is not in the config, give its path as the source:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ unshackle kv copy local ~/Downloads/key_store.db -s EXAMPLE
|
||||||
|
```
|
||||||
|
|
||||||
|
`kv copy` only reads from a source, so the file stays unchanged. `kv sync` also accepts a path, but it writes content keys into that file.
|
||||||
|
|
||||||
|
With `--service`, `kv copy` reads the table named by the service's vault tag and the table named by its plain tag. It writes both into the vault-tag table, which is the table a download reads. Without `--service`, each table keeps its name.
|
||||||
|
|
||||||
A pair the destination vault has flagged in `bad_keys` is left behind, so a copy does not undo a flag. `bad_keys` is never treated as a service, so its rows are never copied into a service table.
|
A pair the destination vault has flagged in `bad_keys` is left behind, so a copy does not undo a flag. `bad_keys` is never treated as a service, so its rows are never copied into a service table.
|
||||||
|
|
||||||
### `kv sync`: mirror vaults both ways
|
### `kv sync`: mirror vaults both ways
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from click.testing import CliRunner
|
||||||
|
|
||||||
|
from unshackle.commands import kv as kv_module
|
||||||
|
from unshackle.vaults.SQLite import SQLite
|
||||||
|
|
||||||
|
KID = "0" * 31 + "1"
|
||||||
|
KEY = "a" * 32
|
||||||
|
|
||||||
|
|
||||||
|
def test_kv_copy_moves_a_plain_tag_table_from_a_file_into_the_vault_tag_table(
|
||||||
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||||
|
) -> None:
|
||||||
|
source = tmp_path / "key_store.db"
|
||||||
|
SQLite("seed", source).add_key("TESTSVC", KID, KEY)
|
||||||
|
dest = tmp_path / "local.db"
|
||||||
|
monkeypatch.setattr(kv_module.config, "key_vaults", [{"type": "SQLite", "name": "local", "path": str(dest)}])
|
||||||
|
monkeypatch.setattr(kv_module.Services, "get_vault_tag", lambda name: "Shared" if name == "TESTSVC" else name)
|
||||||
|
|
||||||
|
result = CliRunner().invoke(kv_module.kv, ["copy", "local", str(source), "-s", "TESTSVC"])
|
||||||
|
|
||||||
|
assert result.exit_code == 0, result.output
|
||||||
|
check = SQLite("check", dest)
|
||||||
|
assert check.get_key(KID, "Shared") == KEY
|
||||||
|
assert check.get_key(KID, "TESTSVC") is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_kv_copy_rejects_a_name_that_is_neither_configured_nor_a_file(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
monkeypatch.setattr(kv_module.config, "key_vaults", [])
|
||||||
|
|
||||||
|
result = CliRunner().invoke(kv_module.kv, ["copy", "local", "nowhere.db"])
|
||||||
|
|
||||||
|
assert result.exit_code != 0
|
||||||
|
assert "not defined in the config" in result.output
|
||||||
+19
-10
@@ -28,12 +28,15 @@ from unshackle.core.vaults import Vaults
|
|||||||
|
|
||||||
|
|
||||||
def load_vaults(vault_names: list[str]) -> Vaults:
|
def load_vaults(vault_names: list[str]) -> Vaults:
|
||||||
"""Load and validate vaults by name."""
|
"""Load and validate vaults by config name, or by the path of an SQLite vault file."""
|
||||||
vaults = Vaults()
|
vaults = Vaults()
|
||||||
for vault_name in vault_names:
|
for vault_name in vault_names:
|
||||||
vault_config = next((x for x in config.key_vaults if x["name"] == vault_name), None)
|
vault_config = next((x for x in config.key_vaults if x["name"] == vault_name), None)
|
||||||
if not vault_config:
|
if not vault_config:
|
||||||
raise click.ClickException(f"Vault ({vault_name}) is not defined in the config.")
|
path = Path(vault_name).expanduser()
|
||||||
|
if not path.is_file():
|
||||||
|
raise click.ClickException(f"Vault ({vault_name}) is not defined in the config or an existing file.")
|
||||||
|
vault_config = {"type": "SQLite", "name": path.stem, "path": path}
|
||||||
|
|
||||||
vault_type = vault_config["type"]
|
vault_type = vault_config["type"]
|
||||||
vault_args = vault_config.copy()
|
vault_args = vault_config.copy()
|
||||||
@@ -99,8 +102,10 @@ def add_keys_with_progress(vault: Vault, service: str, kid_keys: dict[str, str],
|
|||||||
return added
|
return added
|
||||||
|
|
||||||
|
|
||||||
def copy_service_data(to_vault: Vault, from_vault: Vault, service: str, log: logging.Logger) -> int:
|
def copy_service_data(
|
||||||
"""Copy data for a single service between vaults."""
|
to_vault: Vault, from_vault: Vault, service: str, log: logging.Logger, to_service: Optional[str] = None
|
||||||
|
) -> int:
|
||||||
|
"""Copy data for a single service between vaults, into the `to_service` table when given."""
|
||||||
if service.lower() == "bad_keys":
|
if service.lower() == "bad_keys":
|
||||||
return 0
|
return 0
|
||||||
try:
|
try:
|
||||||
@@ -117,7 +122,7 @@ def copy_service_data(to_vault: Vault, from_vault: Vault, service: str, log: log
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
try:
|
try:
|
||||||
added = add_keys_with_progress(to_vault, service, content_keys, log)
|
added = add_keys_with_progress(to_vault, to_service or service, content_keys, log)
|
||||||
except PermissionError:
|
except PermissionError:
|
||||||
log.warning(f"{service}: No permission to create table in {to_vault}, skipped")
|
log.warning(f"{service}: No permission to create table in {to_vault}, skipped")
|
||||||
return 0
|
return 0
|
||||||
@@ -165,7 +170,8 @@ def copy(
|
|||||||
It should be the name of a Key Vault defined in the config.
|
It should be the name of a Key Vault defined in the config.
|
||||||
|
|
||||||
The `from_vault_names` argument is the key vault(s) you wish to take
|
The `from_vault_names` argument is the key vault(s) you wish to take
|
||||||
data from. You may supply multiple key vaults.
|
data from. You may supply multiple key vaults. Each one is a vault name
|
||||||
|
from the config, or the path to an SQLite vault file.
|
||||||
"""
|
"""
|
||||||
if not from_vault_names:
|
if not from_vault_names:
|
||||||
raise click.ClickException("No Vaults were specified to copy data from.")
|
raise click.ClickException("No Vaults were specified to copy data from.")
|
||||||
@@ -184,8 +190,11 @@ def copy(
|
|||||||
if service and local_only:
|
if service and local_only:
|
||||||
raise click.UsageError("--service and --local-only are mutually exclusive.")
|
raise click.UsageError("--service and --local-only are mutually exclusive.")
|
||||||
|
|
||||||
|
source_tables: Optional[list[str]] = None
|
||||||
if service:
|
if service:
|
||||||
service = Services.get_vault_tag(service)
|
tag, service = Services.get_tag(service), Services.get_vault_tag(service)
|
||||||
|
# a vault filled outside unshackle can hold the keys under the plain tag, not the VAULT_TAG
|
||||||
|
source_tables = [service] if tag.lower() == service.lower() else [service, tag]
|
||||||
log.info(f"Filtering by service: {service}")
|
log.info(f"Filtering by service: {service}")
|
||||||
|
|
||||||
installed: Optional[set[str]] = None
|
installed: Optional[set[str]] = None
|
||||||
@@ -195,8 +204,8 @@ def copy(
|
|||||||
|
|
||||||
total_added = 0
|
total_added = 0
|
||||||
for from_vault in from_vaults:
|
for from_vault in from_vaults:
|
||||||
if service:
|
if source_tables:
|
||||||
services_to_copy = [service]
|
services_to_copy = source_tables
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
services_to_copy = list(from_vault.get_services())
|
services_to_copy = list(from_vault.get_services())
|
||||||
@@ -212,7 +221,7 @@ def copy(
|
|||||||
log.info(f"{from_vault.name}: skipping {skipped} service(s) not installed locally")
|
log.info(f"{from_vault.name}: skipping {skipped} service(s) not installed locally")
|
||||||
|
|
||||||
for service_tag in services_to_copy:
|
for service_tag in services_to_copy:
|
||||||
added = copy_service_data(to_vault, from_vault, service_tag, log)
|
added = copy_service_data(to_vault, from_vault, service_tag, log, to_service=service)
|
||||||
total_added += added
|
total_added += added
|
||||||
|
|
||||||
if total_added > 0:
|
if total_added > 0:
|
||||||
|
|||||||
Reference in New Issue
Block a user