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.
Files changed (28) hide show
  1. {grz_cli-2.0.0 → grz_cli-3.0.0}/CHANGELOG.md +29 -0
  2. {grz_cli-2.0.0 → grz_cli-3.0.0}/PKG-INFO +2 -2
  3. {grz_cli-2.0.0 → grz_cli-3.0.0}/pyproject.toml +2 -1
  4. grz_cli-3.0.0/src/grz_cli/__init__.py +1 -0
  5. {grz_cli-2.0.0 → grz_cli-3.0.0}/src/grz_cli/cli.py +7 -1
  6. {grz_cli-2.0.0 → grz_cli-3.0.0}/src/grz_cli/commands/encrypt.py +28 -24
  7. grz_cli-3.0.0/src/grz_cli/models/config.py +74 -0
  8. {grz_cli-2.0.0 → grz_cli-3.0.0}/src/grz_cli/utils/version_check.py +2 -14
  9. grz_cli-3.0.0/tests/test_config.py +109 -0
  10. grz_cli-3.0.0/tests/test_encrypt.py +54 -0
  11. grz_cli-2.0.0/src/grz_cli/__init__.py +0 -1
  12. grz_cli-2.0.0/src/grz_cli/models/config.py +0 -19
  13. {grz_cli-2.0.0 → grz_cli-3.0.0}/.gitignore +0 -0
  14. {grz_cli-2.0.0 → grz_cli-3.0.0}/LICENSE +0 -0
  15. {grz_cli-2.0.0 → grz_cli-3.0.0}/MANIFEST.in +0 -0
  16. {grz_cli-2.0.0 → grz_cli-3.0.0}/README.md +0 -0
  17. {grz_cli-2.0.0 → grz_cli-3.0.0}/docs/version-compatibility-check.md +0 -0
  18. {grz_cli-2.0.0 → grz_cli-3.0.0}/src/__init__.py +0 -0
  19. {grz_cli-2.0.0 → grz_cli-3.0.0}/src/grz_cli/commands/__init__.py +0 -0
  20. {grz_cli-2.0.0 → grz_cli-3.0.0}/src/grz_cli/commands/get_id.py +0 -0
  21. {grz_cli-2.0.0 → grz_cli-3.0.0}/src/grz_cli/commands/submit.py +0 -0
  22. {grz_cli-2.0.0 → grz_cli-3.0.0}/src/grz_cli/commands/upload.py +0 -0
  23. {grz_cli-2.0.0 → grz_cli-3.0.0}/src/grz_cli/commands/validate.py +0 -0
  24. {grz_cli-2.0.0 → grz_cli-3.0.0}/src/grz_cli/models/__init__.py +0 -0
  25. {grz_cli-2.0.0 → grz_cli-3.0.0}/src/grz_cli/py.typed +0 -0
  26. {grz_cli-2.0.0 → grz_cli-3.0.0}/tests/conftest.py +0 -0
  27. {grz_cli-2.0.0 → grz_cli-3.0.0}/tests/test_smoke.py +0 -0
  28. {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: 2.0.0
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,>=3
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 >=3,<4",
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
- cli()
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
- submitter_privkey_path = config.keys.submitter_private_key_path
38
- if submitter_privkey_path == "":
39
- submitter_privkey_path = None
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
- if pubkey := config.keys.grz_public_key:
52
- with NamedTemporaryFile("w") as f:
53
- f.write(pubkey)
54
- f.flush()
55
- worker_inst.encrypt(
56
- f.name,
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 = "version.json",
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