grz-cli 2.0.0__tar.gz → 3.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {grz_cli-2.0.0 → grz_cli-3.0.0}/CHANGELOG.md +29 -0
- {grz_cli-2.0.0 → grz_cli-3.0.0}/PKG-INFO +2 -2
- {grz_cli-2.0.0 → grz_cli-3.0.0}/pyproject.toml +2 -1
- grz_cli-3.0.0/src/grz_cli/__init__.py +1 -0
- {grz_cli-2.0.0 → grz_cli-3.0.0}/src/grz_cli/cli.py +7 -1
- {grz_cli-2.0.0 → grz_cli-3.0.0}/src/grz_cli/commands/encrypt.py +28 -24
- grz_cli-3.0.0/src/grz_cli/models/config.py +74 -0
- {grz_cli-2.0.0 → grz_cli-3.0.0}/src/grz_cli/utils/version_check.py +2 -14
- grz_cli-3.0.0/tests/test_config.py +109 -0
- grz_cli-3.0.0/tests/test_encrypt.py +54 -0
- grz_cli-2.0.0/src/grz_cli/__init__.py +0 -1
- grz_cli-2.0.0/src/grz_cli/models/config.py +0 -19
- {grz_cli-2.0.0 → grz_cli-3.0.0}/.gitignore +0 -0
- {grz_cli-2.0.0 → grz_cli-3.0.0}/LICENSE +0 -0
- {grz_cli-2.0.0 → grz_cli-3.0.0}/MANIFEST.in +0 -0
- {grz_cli-2.0.0 → grz_cli-3.0.0}/README.md +0 -0
- {grz_cli-2.0.0 → grz_cli-3.0.0}/docs/version-compatibility-check.md +0 -0
- {grz_cli-2.0.0 → grz_cli-3.0.0}/src/__init__.py +0 -0
- {grz_cli-2.0.0 → grz_cli-3.0.0}/src/grz_cli/commands/__init__.py +0 -0
- {grz_cli-2.0.0 → grz_cli-3.0.0}/src/grz_cli/commands/get_id.py +0 -0
- {grz_cli-2.0.0 → grz_cli-3.0.0}/src/grz_cli/commands/submit.py +0 -0
- {grz_cli-2.0.0 → grz_cli-3.0.0}/src/grz_cli/commands/upload.py +0 -0
- {grz_cli-2.0.0 → grz_cli-3.0.0}/src/grz_cli/commands/validate.py +0 -0
- {grz_cli-2.0.0 → grz_cli-3.0.0}/src/grz_cli/models/__init__.py +0 -0
- {grz_cli-2.0.0 → grz_cli-3.0.0}/src/grz_cli/py.typed +0 -0
- {grz_cli-2.0.0 → grz_cli-3.0.0}/tests/conftest.py +0 -0
- {grz_cli-2.0.0 → grz_cli-3.0.0}/tests/test_smoke.py +0 -0
- {grz_cli-2.0.0 → grz_cli-3.0.0}/tests/test_submit.py +0 -0
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [3.0.0](https://github.com/BfArM-MVH/grz-tools/compare/grz-cli-v2.0.0...grz-cli-v3.0.0) (2026-09-26)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### ⚠ BREAKING CHANGES
|
|
7
|
+
|
|
8
|
+
* **grzctl:** the grzctl config has no `keys` section. Each inbox names its private key. An inbox's `private_key_passphrase` comes before `C4GH_PASSPHRASE`.
|
|
9
|
+
* **grz-cli:** `Crypt4GH.prepare_c4gh_keys`, `Crypt4GH.decrypt_file`, `Submission.encrypt`, `EncryptedSubmission.decrypt`, `Worker.encrypt` and `Worker.decrypt` take `X25519PrivateKey` and `X25519PublicKey` objects instead of paths. The `Crypt4GH` key loaders return these objects.
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* **grz-cli,grz-common,grz-db,grzctl:** record why a submission failed and classify S3 errors ([#690](https://github.com/BfArM-MVH/grz-tools/issues/690)) ([f1d3fc8](https://github.com/BfArM-MVH/grz-tools/commit/f1d3fc804387c4fb6de861cccb6a99b521ac0056))
|
|
14
|
+
* **grz-cli:** accept the submitter private key inline ([06f12e6](https://github.com/BfArM-MVH/grz-tools/commit/06f12e625878c87413fbc1afa77ce8708c03a3df))
|
|
15
|
+
* **grz-cli:** add keys.submitter_private_key_passphrase ([06f12e6](https://github.com/BfArM-MVH/grz-tools/commit/06f12e625878c87413fbc1afa77ce8708c03a3df))
|
|
16
|
+
* **grz-common:** load crypt4gh keys in memory ([06f12e6](https://github.com/BfArM-MVH/grz-tools/commit/06f12e625878c87413fbc1afa77ce8708c03a3df))
|
|
17
|
+
* **grz-common:** recommend grz-cli 3.0.0 in the bundled version policy ([bf3e6bf](https://github.com/BfArM-MVH/grz-tools/commit/bf3e6bf4f2ed21cd1c7bdf4d6aa8710ece4f60d3))
|
|
18
|
+
* **grzctl:** accept every crypt4gh key in the config inline ([06f12e6](https://github.com/BfArM-MVH/grz-tools/commit/06f12e625878c87413fbc1afa77ce8708c03a3df))
|
|
19
|
+
* **grzctl:** Centralized version.json and its distribution using grzctl ([#667](https://github.com/BfArM-MVH/grz-tools/issues/667)) ([6ed9cb0](https://github.com/BfArM-MVH/grz-tools/commit/6ed9cb04bede9fc9b34b7ce6d3aa9a0df9d42ae4))
|
|
20
|
+
* **grzctl:** configure each crypt4gh key where grzctl uses it ([#694](https://github.com/BfArM-MVH/grz-tools/issues/694)) ([06f12e6](https://github.com/BfArM-MVH/grz-tools/commit/06f12e625878c87413fbc1afa77ce8708c03a3df))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Bug Fixes
|
|
24
|
+
|
|
25
|
+
* **grz-cli,grz-common,grz-db,grzctl:** require grz-common 4, grz-db 4 and grz-pydantic-models 4 ([#688](https://github.com/BfArM-MVH/grz-tools/issues/688)) ([bf3e6bf](https://github.com/BfArM-MVH/grz-tools/commit/bf3e6bf4f2ed21cd1c7bdf4d6aa8710ece4f60d3))
|
|
26
|
+
* **grz-common:** keep crypt4gh private keys out of DEBUG logs ([06f12e6](https://github.com/BfArM-MVH/grz-tools/commit/06f12e625878c87413fbc1afa77ce8708c03a3df))
|
|
27
|
+
* **grz-common:** leave the raw input out of config validation errors ([06f12e6](https://github.com/BfArM-MVH/grz-tools/commit/06f12e625878c87413fbc1afa77ce8708c03a3df))
|
|
28
|
+
* **grzctl,grz-common,grz-cli:** drop grzctl submit and grzctl's grz-cli dependency ([#675](https://github.com/BfArM-MVH/grz-tools/issues/675)) ([e6f27c1](https://github.com/BfArM-MVH/grz-tools/commit/e6f27c1714fc73d5159b29356914bc345a797c0a))
|
|
29
|
+
* **grzctl:** keep the stored upload date in db submission populate ([f1d3fc8](https://github.com/BfArM-MVH/grz-tools/commit/f1d3fc804387c4fb6de861cccb6a99b521ac0056))
|
|
30
|
+
* **grzctl:** mask db.author.private_key in dump-config ([06f12e6](https://github.com/BfArM-MVH/grz-tools/commit/06f12e625878c87413fbc1afa77ce8708c03a3df))
|
|
31
|
+
|
|
3
32
|
## [2.0.0](https://github.com/BfArM-MVH/grz-tools/compare/grz-cli-v1.8.0...grz-cli-v2.0.0) (2026-08-13)
|
|
4
33
|
|
|
5
34
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: grz-cli
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.0
|
|
4
4
|
Summary: Tool for validation, encryption and upload of MV submissions to GDCs.
|
|
5
5
|
Project-URL: Homepage, https://github.com/BfArM-MVH/grz-tools
|
|
6
6
|
Project-URL: Repository, https://github.com/BfArM-MVH/grz-tools
|
|
@@ -13,7 +13,7 @@ Keywords: GDC,GRZ,S3
|
|
|
13
13
|
Classifier: Operating System :: OS Independent
|
|
14
14
|
Classifier: Programming Language :: Python :: 3
|
|
15
15
|
Requires-Python: >=3.12
|
|
16
|
-
Requires-Dist: grz-common<4
|
|
16
|
+
Requires-Dist: grz-common<5,>=4
|
|
17
17
|
Requires-Dist: packaging<27,>=23
|
|
18
18
|
Description-Content-Type: text/markdown
|
|
19
19
|
|
|
@@ -14,7 +14,7 @@ authors = [
|
|
|
14
14
|
dynamic = ["version"]
|
|
15
15
|
dependencies = [
|
|
16
16
|
"packaging >=23,<27",
|
|
17
|
-
"grz-common >=
|
|
17
|
+
"grz-common >=4,<5",
|
|
18
18
|
]
|
|
19
19
|
classifiers = [
|
|
20
20
|
"Programming Language :: Python :: 3",
|
|
@@ -30,6 +30,7 @@ test = [
|
|
|
30
30
|
"pytest-cov",
|
|
31
31
|
"pytest-mock",
|
|
32
32
|
"pytest-sugar",
|
|
33
|
+
"pytest-xdist",
|
|
33
34
|
]
|
|
34
35
|
|
|
35
36
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "3.0.0"
|
|
@@ -11,6 +11,7 @@ from pathlib import Path
|
|
|
11
11
|
from textwrap import dedent
|
|
12
12
|
|
|
13
13
|
import click
|
|
14
|
+
import grz_common.exceptions as grzexc
|
|
14
15
|
import grz_pydantic_models.submission.metadata
|
|
15
16
|
from grz_common.cli import config_file
|
|
16
17
|
from grz_common.cli.dump_config import dump_config
|
|
@@ -95,7 +96,12 @@ def main():
|
|
|
95
96
|
Main entry point for the CLI application.
|
|
96
97
|
"""
|
|
97
98
|
cli = build_cli()
|
|
98
|
-
|
|
99
|
+
try:
|
|
100
|
+
cli()
|
|
101
|
+
except grzexc.GrzError as e:
|
|
102
|
+
# an expected failure, such as an invalid metadata.json: log its message, not a traceback
|
|
103
|
+
log.error(e)
|
|
104
|
+
sys.exit(1)
|
|
99
105
|
|
|
100
106
|
|
|
101
107
|
if __name__ == "__main__":
|
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
import logging
|
|
4
4
|
import sys
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from tempfile import NamedTemporaryFile
|
|
7
6
|
from typing import Any
|
|
8
7
|
|
|
9
8
|
import click
|
|
10
9
|
import grz_common.cli as grzcli
|
|
10
|
+
from grz_common.models.base import get_secret_value
|
|
11
|
+
from grz_common.utils.crypt import Crypt4GH
|
|
11
12
|
from grz_common.workers.worker import Worker
|
|
12
13
|
|
|
13
14
|
from ..models.config import EncryptConfig
|
|
@@ -34,9 +35,26 @@ def encrypt(configuration: dict[str, Any], submission_dir, force, check_validati
|
|
|
34
35
|
"""
|
|
35
36
|
config = EncryptConfig.model_validate(configuration)
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
if config.keys.grz_public_key is not None:
|
|
39
|
+
grz_public_key = Crypt4GH.load_public_key(config.keys.grz_public_key, key_name="keys.grz_public_key")
|
|
40
|
+
elif config.keys.grz_public_key_path is not None:
|
|
41
|
+
grz_public_key = Crypt4GH.retrieve_public_key(config.keys.grz_public_key_path)
|
|
42
|
+
else:
|
|
43
|
+
# This case cannot occur here, but an explicit check is needed for type-checking.
|
|
44
|
+
sys.exit("Either keys.grz_public_key or keys.grz_public_key_path must be set for encryption.")
|
|
45
|
+
|
|
46
|
+
submitter_private_key = None
|
|
47
|
+
passphrase = get_secret_value(config.keys.submitter_private_key_passphrase)
|
|
48
|
+
if config.keys.submitter_private_key is not None:
|
|
49
|
+
submitter_private_key = Crypt4GH.load_private_key(
|
|
50
|
+
config.keys.submitter_private_key.get_secret_value(),
|
|
51
|
+
passphrase=passphrase,
|
|
52
|
+
key_name="keys.submitter_private_key",
|
|
53
|
+
)
|
|
54
|
+
elif config.keys.submitter_private_key_path is not None:
|
|
55
|
+
submitter_private_key = Crypt4GH.retrieve_private_key(
|
|
56
|
+
config.keys.submitter_private_key_path, passphrase=passphrase
|
|
57
|
+
)
|
|
40
58
|
|
|
41
59
|
log.info("Starting encryption...")
|
|
42
60
|
|
|
@@ -48,25 +66,11 @@ def encrypt(configuration: dict[str, Any], submission_dir, force, check_validati
|
|
|
48
66
|
log_dir=submission_dir / "logs",
|
|
49
67
|
encrypted_files_dir=submission_dir / "encrypted_files",
|
|
50
68
|
)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
submitter_private_key_path=submitter_privkey_path,
|
|
58
|
-
force=force,
|
|
59
|
-
check_validation_logs=check_validation_logs,
|
|
60
|
-
)
|
|
61
|
-
else:
|
|
62
|
-
# This case cannot occur here, but an explicit check is needed for type-checking.
|
|
63
|
-
if config.keys.grz_public_key_path is None:
|
|
64
|
-
sys.exit("GRZ public key path is required for encryption.")
|
|
65
|
-
worker_inst.encrypt(
|
|
66
|
-
config.keys.grz_public_key_path,
|
|
67
|
-
submitter_private_key_path=submitter_privkey_path,
|
|
68
|
-
force=force,
|
|
69
|
-
check_validation_logs=check_validation_logs,
|
|
70
|
-
)
|
|
69
|
+
worker_inst.encrypt(
|
|
70
|
+
grz_public_key,
|
|
71
|
+
submitter_private_key=submitter_private_key,
|
|
72
|
+
force=force,
|
|
73
|
+
check_validation_logs=check_validation_logs,
|
|
74
|
+
)
|
|
71
75
|
|
|
72
76
|
log.info("Encryption successful!")
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Self
|
|
3
|
+
|
|
4
|
+
from grz_common.models.base import Crypt4GHPublicKey, FilePath, IgnoringBaseModel
|
|
5
|
+
from grz_common.models.identifiers import IdentifiersConfigModel
|
|
6
|
+
from grz_common.models.s3 import S3ConfigModel
|
|
7
|
+
from pydantic import ConfigDict, SecretStr, model_validator
|
|
8
|
+
|
|
9
|
+
log = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class KeyModel(IgnoringBaseModel):
|
|
13
|
+
grz_public_key: Crypt4GHPublicKey | None = None
|
|
14
|
+
"""
|
|
15
|
+
The public key of the recipient (the associated GRZ).
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
grz_public_key_path: FilePath | None = None
|
|
19
|
+
"""
|
|
20
|
+
Path to the crypt4gh public key of the recipient (the associated GRZ).
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
submitter_private_key: SecretStr | None = None
|
|
24
|
+
"""
|
|
25
|
+
The submitter's crypt4gh private key (optional).
|
|
26
|
+
grz-cli signs the encrypted files with this key if it is set.
|
|
27
|
+
If neither this field nor ``submitter_private_key_path`` is set, grz-cli signs them with a random key.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
submitter_private_key_path: FilePath | None = None
|
|
31
|
+
"""
|
|
32
|
+
Path to the submitter's crypt4gh private key (optional).
|
|
33
|
+
grz-cli signs the encrypted files with this key if it is set.
|
|
34
|
+
If neither this field nor ``submitter_private_key`` is set, grz-cli signs them with a random key.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
submitter_private_key_passphrase: SecretStr | None = None
|
|
38
|
+
"""
|
|
39
|
+
Passphrase to the submitter's crypt4gh private key (optional).
|
|
40
|
+
grz-cli takes the passphrase from this field, else from ``C4GH_PASSPHRASE``, else from a prompt.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
@model_validator(mode="after")
|
|
44
|
+
def validate_grz_public_key(self) -> Self:
|
|
45
|
+
if self.grz_public_key is None and self.grz_public_key_path is None:
|
|
46
|
+
raise ValueError("Either grz_public_key or grz_public_key_path must be set.")
|
|
47
|
+
if self.grz_public_key is not None and self.grz_public_key_path is not None:
|
|
48
|
+
raise ValueError("Only one of grz_public_key or grz_public_key_path must be set.")
|
|
49
|
+
return self
|
|
50
|
+
|
|
51
|
+
@model_validator(mode="after")
|
|
52
|
+
def validate_submitter_private_key(self) -> Self:
|
|
53
|
+
if self.submitter_private_key is not None and self.submitter_private_key_path is not None:
|
|
54
|
+
raise ValueError("Only one of submitter_private_key or submitter_private_key_path must be set.")
|
|
55
|
+
return self
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class KeyConfigModel(IgnoringBaseModel):
|
|
59
|
+
# errors would show the raw input, and SecretStr does not mask the submitter private key and its passphrase there
|
|
60
|
+
model_config = ConfigDict(hide_input_in_errors=True)
|
|
61
|
+
|
|
62
|
+
keys: KeyModel
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class UploadConfig(S3ConfigModel):
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class EncryptConfig(KeyConfigModel):
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class ValidateConfig(IdentifiersConfigModel):
|
|
74
|
+
pass
|
|
@@ -4,7 +4,7 @@ from datetime import UTC, datetime
|
|
|
4
4
|
from importlib.metadata import version
|
|
5
5
|
|
|
6
6
|
from grz_common.models.s3 import S3Options
|
|
7
|
-
from grz_common.models.version import VersionFile, VersionInfo
|
|
7
|
+
from grz_common.models.version import VERSION_FILE_KEY, VersionFile, VersionInfo
|
|
8
8
|
from packaging import version as pkg_version
|
|
9
9
|
|
|
10
10
|
logger = logging.getLogger(__name__)
|
|
@@ -37,7 +37,7 @@ def _select_active_policy(
|
|
|
37
37
|
|
|
38
38
|
def check_version_and_exit_if_needed(
|
|
39
39
|
s3_options: S3Options,
|
|
40
|
-
version_file_key: str =
|
|
40
|
+
version_file_key: str = VERSION_FILE_KEY,
|
|
41
41
|
) -> None:
|
|
42
42
|
"""Validate the installed grz-cli version against the policy defined in version.json."""
|
|
43
43
|
version_file = VersionFile.from_s3(s3_options, version_file_key)
|
|
@@ -46,18 +46,6 @@ def check_version_and_exit_if_needed(
|
|
|
46
46
|
_check_policy_and_exit_if_needed("grz-cli", current_version, version_file.grzcli_version)
|
|
47
47
|
|
|
48
48
|
|
|
49
|
-
def check_metadata_version_and_exit_if_needed(
|
|
50
|
-
s3_options: S3Options,
|
|
51
|
-
metadata_schema_version: str,
|
|
52
|
-
version_file_key: str = "version.json",
|
|
53
|
-
) -> None:
|
|
54
|
-
"""Validate the metadata schema version against the policy defined in version.json."""
|
|
55
|
-
version_file = VersionFile.from_s3(s3_options, version_file_key)
|
|
56
|
-
|
|
57
|
-
current_version = pkg_version.Version(metadata_schema_version)
|
|
58
|
-
_check_policy_and_exit_if_needed("metadata", current_version, version_file.metadata_version)
|
|
59
|
-
|
|
60
|
-
|
|
61
49
|
def _check_policy_and_exit_if_needed(
|
|
62
50
|
subject: str,
|
|
63
51
|
current_version: pkg_version.Version,
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Tests for ``KeyModel``, the keys section of the grz-cli config, which moved here from grz-common."""
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
from grz_cli.models.config import EncryptConfig, KeyModel
|
|
8
|
+
from pydantic import ValidationError
|
|
9
|
+
|
|
10
|
+
PUBLIC_KEY = "-----BEGIN CRYPT4GH PUBLIC KEY-----\n7JZ9eRjhOo1zB8HfoQK1ULCR3Wpnl91hF2K8FtpmeQ8=\n-----END CRYPT4GH PUBLIC KEY-----\n"
|
|
11
|
+
"""A crypt4gh public key as text. ``Crypt4GHPublicKey`` only checks for the markers, so it need not decode."""
|
|
12
|
+
|
|
13
|
+
SUBMITTER_PRIVATE_KEY = "submitter-private-key"
|
|
14
|
+
"""Stands for a private key given inline. The model does not parse it, so it need not be a key."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_grz_common_keeps_no_copy_of_the_key_models():
|
|
18
|
+
assert importlib.util.find_spec("grz_common.models.keys") is None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_encrypt_config_reads_the_keys_section(tmp_path: Path):
|
|
22
|
+
public_key_path = tmp_path / "grz.pub"
|
|
23
|
+
public_key_path.write_text(PUBLIC_KEY)
|
|
24
|
+
|
|
25
|
+
config = EncryptConfig.model_validate({"keys": {"grz_public_key_path": str(public_key_path)}})
|
|
26
|
+
|
|
27
|
+
assert config.keys.grz_public_key_path == public_key_path
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_a_config_with_the_removed_grz_private_key_path_still_loads():
|
|
31
|
+
"""grz-cli never read ``keys.grz_private_key_path``. Old configs still set it, so the model ignores it."""
|
|
32
|
+
keys = KeyModel.model_validate({"grz_public_key": PUBLIC_KEY, "grz_private_key_path": "/no/such/key.sec"})
|
|
33
|
+
|
|
34
|
+
assert "grz_private_key_path" not in keys.model_dump()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_neither_grz_public_key_nor_grz_public_key_path_fails():
|
|
38
|
+
with pytest.raises(ValidationError, match="Either grz_public_key or grz_public_key_path must be set"):
|
|
39
|
+
KeyModel()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_both_grz_public_key_and_grz_public_key_path_fails(tmp_path: Path):
|
|
43
|
+
public_key_path = tmp_path / "grz.pub"
|
|
44
|
+
public_key_path.write_text(PUBLIC_KEY)
|
|
45
|
+
|
|
46
|
+
with pytest.raises(ValidationError, match="Only one of grz_public_key or grz_public_key_path must be set"):
|
|
47
|
+
KeyModel(grz_public_key=PUBLIC_KEY, grz_public_key_path=public_key_path)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@pytest.mark.parametrize(
|
|
51
|
+
"public_key",
|
|
52
|
+
[
|
|
53
|
+
PUBLIC_KEY,
|
|
54
|
+
"-----BEGIN CRYPT4GH PUBLIC KEY-----\n7JZ9eRjhOo1zB8HfoQK1ULCR3Wpnl91hF2K8FtpmeQ8=\n",
|
|
55
|
+
"7JZ9eRjhOo1zB8HfoQK1ULCR3Wpnl91hF2K8FtpmeQ8=\n-----END CRYPT4GH PUBLIC KEY-----\n",
|
|
56
|
+
],
|
|
57
|
+
ids=["both markers", "BEGIN marker only", "END marker only"],
|
|
58
|
+
)
|
|
59
|
+
def test_grz_public_key_with_a_marker_passes(public_key: str):
|
|
60
|
+
assert KeyModel(grz_public_key=public_key).grz_public_key == public_key
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_grz_public_key_without_markers_fails():
|
|
64
|
+
with pytest.raises(ValidationError, match="Invalid public key format"):
|
|
65
|
+
KeyModel(grz_public_key="7JZ9eRjhOo1zB8HfoQK1ULCR3Wpnl91hF2K8FtpmeQ8=")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_both_submitter_private_key_and_submitter_private_key_path_fails(tmp_path: Path):
|
|
69
|
+
private_key_path = tmp_path / "submitter.sec"
|
|
70
|
+
private_key_path.write_text(SUBMITTER_PRIVATE_KEY)
|
|
71
|
+
|
|
72
|
+
with pytest.raises(
|
|
73
|
+
ValidationError, match="Only one of submitter_private_key or submitter_private_key_path must be set"
|
|
74
|
+
):
|
|
75
|
+
KeyModel(
|
|
76
|
+
grz_public_key=PUBLIC_KEY,
|
|
77
|
+
submitter_private_key=SUBMITTER_PRIVATE_KEY,
|
|
78
|
+
submitter_private_key_path=private_key_path,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_the_submitter_private_key_is_masked_in_dumps():
|
|
83
|
+
keys = KeyModel(grz_public_key=PUBLIC_KEY, submitter_private_key=SUBMITTER_PRIVATE_KEY)
|
|
84
|
+
|
|
85
|
+
assert SUBMITTER_PRIVATE_KEY not in repr(keys)
|
|
86
|
+
assert SUBMITTER_PRIVATE_KEY not in keys.model_dump_json()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_the_submitter_private_key_passphrase_is_masked_in_dumps():
|
|
90
|
+
passphrase = "submitter-passphrase"
|
|
91
|
+
keys = KeyModel(grz_public_key=PUBLIC_KEY, submitter_private_key_passphrase=passphrase)
|
|
92
|
+
|
|
93
|
+
assert passphrase not in repr(keys)
|
|
94
|
+
assert passphrase not in keys.model_dump_json()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_a_config_error_does_not_show_the_submitter_private_key(tmp_path: Path):
|
|
98
|
+
private_key_path = tmp_path / "submitter.sec"
|
|
99
|
+
private_key_path.write_text(SUBMITTER_PRIVATE_KEY)
|
|
100
|
+
keys = {
|
|
101
|
+
"grz_public_key": PUBLIC_KEY,
|
|
102
|
+
"submitter_private_key": SUBMITTER_PRIVATE_KEY,
|
|
103
|
+
"submitter_private_key_path": str(private_key_path),
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
with pytest.raises(ValidationError) as exc_info:
|
|
107
|
+
EncryptConfig.model_validate({"keys": keys})
|
|
108
|
+
|
|
109
|
+
assert SUBMITTER_PRIVATE_KEY not in str(exc_info.value)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Tests for ``grz-cli encrypt``, which loads the submitter private key with its configured passphrase."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import click.testing
|
|
6
|
+
import crypt4gh.keys
|
|
7
|
+
import crypt4gh.keys.c4gh
|
|
8
|
+
import pytest
|
|
9
|
+
import yaml
|
|
10
|
+
from grz_cli.cli import build_cli
|
|
11
|
+
from pytest_mock import MockerFixture
|
|
12
|
+
|
|
13
|
+
PASSPHRASE = "right-passphrase"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.fixture
|
|
17
|
+
def no_prompt(monkeypatch):
|
|
18
|
+
"""Fail the test if the passphrase prompt opens, and set a wrong ``C4GH_PASSPHRASE``.
|
|
19
|
+
|
|
20
|
+
The configured passphrase comes first, so the wrong one in the environment must not matter.
|
|
21
|
+
"""
|
|
22
|
+
monkeypatch.setenv("C4GH_PASSPHRASE", "wrong-passphrase")
|
|
23
|
+
|
|
24
|
+
def _fail(*args, **kwargs):
|
|
25
|
+
raise AssertionError("the passphrase prompt must not open")
|
|
26
|
+
|
|
27
|
+
monkeypatch.setattr("grz_common.utils.crypt.getpass", _fail)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@pytest.mark.parametrize("inline", [False, True], ids=["path", "inline"])
|
|
31
|
+
def test_encrypt_signs_with_the_submitter_key_and_its_configured_passphrase(
|
|
32
|
+
tmp_path: Path, submission_dir: Path, mocker: MockerFixture, no_prompt, inline: bool
|
|
33
|
+
):
|
|
34
|
+
crypt4gh.keys.c4gh.generate(tmp_path / "grz.sec", tmp_path / "grz.pub", None, comment=None)
|
|
35
|
+
private_key_path = tmp_path / "le.sec"
|
|
36
|
+
crypt4gh.keys.c4gh.generate(private_key_path, tmp_path / "le.pub", PASSPHRASE.encode(), comment=None)
|
|
37
|
+
keys = {"grz_public_key_path": str(tmp_path / "grz.pub"), "submitter_private_key_passphrase": PASSPHRASE}
|
|
38
|
+
if inline:
|
|
39
|
+
keys["submitter_private_key"] = private_key_path.read_text()
|
|
40
|
+
else:
|
|
41
|
+
keys["submitter_private_key_path"] = str(private_key_path)
|
|
42
|
+
config_path = tmp_path / "config.yaml"
|
|
43
|
+
config_path.write_text(yaml.dump({"keys": keys}))
|
|
44
|
+
worker_encrypt = mocker.patch("grz_cli.commands.encrypt.Worker.encrypt")
|
|
45
|
+
|
|
46
|
+
result = click.testing.CliRunner().invoke(
|
|
47
|
+
build_cli(),
|
|
48
|
+
["encrypt", "--submission-dir", str(submission_dir), "--config-file", str(config_path)],
|
|
49
|
+
catch_exceptions=False,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
assert result.exit_code == 0, result.output
|
|
53
|
+
signing_key = worker_encrypt.call_args.kwargs["submitter_private_key"]
|
|
54
|
+
assert signing_key.private_bytes_raw() == crypt4gh.keys.get_private_key(private_key_path, lambda: PASSPHRASE)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "2.0.0"
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
|
|
3
|
-
from grz_common.models.identifiers import IdentifiersConfigModel
|
|
4
|
-
from grz_common.models.keys import KeyConfigModel
|
|
5
|
-
from grz_common.models.s3 import S3ConfigModel
|
|
6
|
-
|
|
7
|
-
log = logging.getLogger(__name__)
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class UploadConfig(S3ConfigModel):
|
|
11
|
-
pass
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class EncryptConfig(KeyConfigModel):
|
|
15
|
-
pass
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class ValidateConfig(IdentifiersConfigModel):
|
|
19
|
-
pass
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|