grz-cli 2.0.0__tar.gz → 3.0.1__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 (29) hide show
  1. {grz_cli-2.0.0 → grz_cli-3.0.1}/CHANGELOG.md +36 -0
  2. {grz_cli-2.0.0 → grz_cli-3.0.1}/PKG-INFO +4 -11
  3. {grz_cli-2.0.0 → grz_cli-3.0.1}/README.md +2 -9
  4. grz_cli-3.0.1/docs/version-compatibility-check.md +63 -0
  5. {grz_cli-2.0.0 → grz_cli-3.0.1}/pyproject.toml +2 -1
  6. grz_cli-3.0.1/src/grz_cli/__init__.py +1 -0
  7. {grz_cli-2.0.0 → grz_cli-3.0.1}/src/grz_cli/cli.py +7 -1
  8. {grz_cli-2.0.0 → grz_cli-3.0.1}/src/grz_cli/commands/encrypt.py +28 -24
  9. grz_cli-3.0.1/src/grz_cli/models/config.py +74 -0
  10. {grz_cli-2.0.0 → grz_cli-3.0.1}/src/grz_cli/utils/version_check.py +2 -14
  11. grz_cli-3.0.1/tests/test_config.py +109 -0
  12. grz_cli-3.0.1/tests/test_encrypt.py +54 -0
  13. grz_cli-2.0.0/docs/version-compatibility-check.md +0 -56
  14. grz_cli-2.0.0/src/grz_cli/__init__.py +0 -1
  15. grz_cli-2.0.0/src/grz_cli/models/config.py +0 -19
  16. {grz_cli-2.0.0 → grz_cli-3.0.1}/.gitignore +0 -0
  17. {grz_cli-2.0.0 → grz_cli-3.0.1}/LICENSE +0 -0
  18. {grz_cli-2.0.0 → grz_cli-3.0.1}/MANIFEST.in +0 -0
  19. {grz_cli-2.0.0 → grz_cli-3.0.1}/src/__init__.py +0 -0
  20. {grz_cli-2.0.0 → grz_cli-3.0.1}/src/grz_cli/commands/__init__.py +0 -0
  21. {grz_cli-2.0.0 → grz_cli-3.0.1}/src/grz_cli/commands/get_id.py +0 -0
  22. {grz_cli-2.0.0 → grz_cli-3.0.1}/src/grz_cli/commands/submit.py +0 -0
  23. {grz_cli-2.0.0 → grz_cli-3.0.1}/src/grz_cli/commands/upload.py +0 -0
  24. {grz_cli-2.0.0 → grz_cli-3.0.1}/src/grz_cli/commands/validate.py +0 -0
  25. {grz_cli-2.0.0 → grz_cli-3.0.1}/src/grz_cli/models/__init__.py +0 -0
  26. {grz_cli-2.0.0 → grz_cli-3.0.1}/src/grz_cli/py.typed +0 -0
  27. {grz_cli-2.0.0 → grz_cli-3.0.1}/tests/conftest.py +0 -0
  28. {grz_cli-2.0.0 → grz_cli-3.0.1}/tests/test_smoke.py +0 -0
  29. {grz_cli-2.0.0 → grz_cli-3.0.1}/tests/test_submit.py +0 -0
@@ -1,5 +1,41 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.0.1](https://github.com/BfArM-MVH/grz-tools/compare/grz-cli-v3.0.0...grz-cli-v3.0.1) (2026-10-22)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **grz-cli,grzctl:** require grz-common 4.0.2 ([#737](https://github.com/BfArM-MVH/grz-tools/issues/737)) ([6bc5d89](https://github.com/BfArM-MVH/grz-tools/commit/6bc5d8942bf3568ba1037a52f8fa0a97ace71b50))
9
+
10
+ ## [3.0.0](https://github.com/BfArM-MVH/grz-tools/compare/grz-cli-v2.0.0...grz-cli-v3.0.0) (2026-09-26)
11
+
12
+
13
+ ### ⚠ BREAKING CHANGES
14
+
15
+ * **grzctl:** the grzctl config has no `keys` section. Each inbox names its private key. An inbox's `private_key_passphrase` comes before `C4GH_PASSPHRASE`.
16
+ * **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.
17
+
18
+ ### Features
19
+
20
+ * **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))
21
+ * **grz-cli:** accept the submitter private key inline ([06f12e6](https://github.com/BfArM-MVH/grz-tools/commit/06f12e625878c87413fbc1afa77ce8708c03a3df))
22
+ * **grz-cli:** add keys.submitter_private_key_passphrase ([06f12e6](https://github.com/BfArM-MVH/grz-tools/commit/06f12e625878c87413fbc1afa77ce8708c03a3df))
23
+ * **grz-common:** load crypt4gh keys in memory ([06f12e6](https://github.com/BfArM-MVH/grz-tools/commit/06f12e625878c87413fbc1afa77ce8708c03a3df))
24
+ * **grz-common:** recommend grz-cli 3.0.0 in the bundled version policy ([bf3e6bf](https://github.com/BfArM-MVH/grz-tools/commit/bf3e6bf4f2ed21cd1c7bdf4d6aa8710ece4f60d3))
25
+ * **grzctl:** accept every crypt4gh key in the config inline ([06f12e6](https://github.com/BfArM-MVH/grz-tools/commit/06f12e625878c87413fbc1afa77ce8708c03a3df))
26
+ * **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))
27
+ * **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))
28
+
29
+
30
+ ### Bug Fixes
31
+
32
+ * **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))
33
+ * **grz-common:** keep crypt4gh private keys out of DEBUG logs ([06f12e6](https://github.com/BfArM-MVH/grz-tools/commit/06f12e625878c87413fbc1afa77ce8708c03a3df))
34
+ * **grz-common:** leave the raw input out of config validation errors ([06f12e6](https://github.com/BfArM-MVH/grz-tools/commit/06f12e625878c87413fbc1afa77ce8708c03a3df))
35
+ * **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))
36
+ * **grzctl:** keep the stored upload date in db submission populate ([f1d3fc8](https://github.com/BfArM-MVH/grz-tools/commit/f1d3fc804387c4fb6de861cccb6a99b521ac0056))
37
+ * **grzctl:** mask db.author.private_key in dump-config ([06f12e6](https://github.com/BfArM-MVH/grz-tools/commit/06f12e625878c87413fbc1afa77ce8708c03a3df))
38
+
3
39
  ## [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
40
 
5
41
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: grz-cli
3
- Version: 2.0.0
3
+ Version: 3.0.1
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.0.2
17
17
  Requires-Dist: packaging<27,>=23
18
18
  Description-Content-Type: text/markdown
19
19
 
@@ -21,7 +21,6 @@ Description-Content-Type: text/markdown
21
21
 
22
22
  A command-line tool for validating, encrypting, and uploading submissions to a genomDE Model Project GDC (Genome Data Center).
23
23
 
24
-
25
24
  ## Table of Contents
26
25
 
27
26
  - [Installation](#installation)
@@ -38,7 +37,6 @@ A command-line tool for validating, encrypting, and uploading submissions to a g
38
37
  - [License](#license)
39
38
  - [Acknowledgements](#acknowledgements)
40
39
 
41
-
42
40
  ## Installation
43
41
 
44
42
  ### Requirements
@@ -53,7 +51,6 @@ For example:
53
51
  - Docker container
54
52
  - Windows Subsystem for Linux
55
53
 
56
-
57
54
  ### Using [Conda](https://conda.io) (recommended)
58
55
 
59
56
  If Conda is not yet available on your system, we recommend to install it through the [Miniforge Conda installer](https://github.com/conda-forge/miniforge) by running the following commands:
@@ -79,7 +76,6 @@ Use the following command to update the tool:
79
76
  conda update -n grz-tools -c conda-forge -c bioconda grz-cli
80
77
  ```
81
78
 
82
-
83
79
  ### Using pip (not recommended)
84
80
 
85
81
  While installation via `pip` is possible, it is not recommended because users should create/manage a Python virtual environment and must ensure that the correct Python version is being used.
@@ -96,7 +92,6 @@ Use the following command to update the tool:
96
92
  pip upgrade grz-cli
97
93
  ```
98
94
 
99
-
100
95
  ### Using Docker
101
96
 
102
97
  Docker images are available via biocontainers at [https://biocontainers.pro/tools/grz-cli](https://biocontainers.pro/tools/grz-cli).
@@ -120,7 +115,7 @@ The S3 secrets can either be directly within the config file or as defined with
120
115
 
121
116
  It is recommended to have the following folder structure for a single submission:
122
117
 
123
- ```
118
+ ```text
124
119
  EXAMPLE_SUBMISSION
125
120
  ├── files
126
121
  │ ├── aaaaaaaa00000000aaaaaaaa00000000_blood_normal.read1.fastq.gz
@@ -138,7 +133,6 @@ The only requirements are that `metadata/metadata.json` exists and the `files/`
138
133
  Data files may be nested under subfolders inside `files/` for better organization.
139
134
  For example, each donor could have their own folder for files.
140
135
 
141
-
142
136
  ### Example submission procedure
143
137
 
144
138
  After preparing your submission as outlined above, you can use the following command to validate, encrypt and upload the submission:
@@ -167,7 +161,6 @@ It combines the `validate`, `encrypt`, and `upload` commands (see below).
167
161
  grz-cli submit --submission-dir foo
168
162
  ```
169
163
 
170
-
171
164
  ### validate
172
165
 
173
166
  It is recommended to run this command before continuing with encryption and upload.
@@ -228,7 +221,7 @@ We recommend using Conda or [Pixi](https://pixi.sh/latest).
228
221
 
229
222
  After cloning the desired branch of the `grz-tools` repo locally, you can run `grz-cli` directly from the repo using:
230
223
 
231
- ```
224
+ ```text
232
225
  uv run --project path/to/cloned/grz-tools grz-cli --help
233
226
  ```
234
227
 
@@ -2,7 +2,6 @@
2
2
 
3
3
  A command-line tool for validating, encrypting, and uploading submissions to a genomDE Model Project GDC (Genome Data Center).
4
4
 
5
-
6
5
  ## Table of Contents
7
6
 
8
7
  - [Installation](#installation)
@@ -19,7 +18,6 @@ A command-line tool for validating, encrypting, and uploading submissions to a g
19
18
  - [License](#license)
20
19
  - [Acknowledgements](#acknowledgements)
21
20
 
22
-
23
21
  ## Installation
24
22
 
25
23
  ### Requirements
@@ -34,7 +32,6 @@ For example:
34
32
  - Docker container
35
33
  - Windows Subsystem for Linux
36
34
 
37
-
38
35
  ### Using [Conda](https://conda.io) (recommended)
39
36
 
40
37
  If Conda is not yet available on your system, we recommend to install it through the [Miniforge Conda installer](https://github.com/conda-forge/miniforge) by running the following commands:
@@ -60,7 +57,6 @@ Use the following command to update the tool:
60
57
  conda update -n grz-tools -c conda-forge -c bioconda grz-cli
61
58
  ```
62
59
 
63
-
64
60
  ### Using pip (not recommended)
65
61
 
66
62
  While installation via `pip` is possible, it is not recommended because users should create/manage a Python virtual environment and must ensure that the correct Python version is being used.
@@ -77,7 +73,6 @@ Use the following command to update the tool:
77
73
  pip upgrade grz-cli
78
74
  ```
79
75
 
80
-
81
76
  ### Using Docker
82
77
 
83
78
  Docker images are available via biocontainers at [https://biocontainers.pro/tools/grz-cli](https://biocontainers.pro/tools/grz-cli).
@@ -101,7 +96,7 @@ The S3 secrets can either be directly within the config file or as defined with
101
96
 
102
97
  It is recommended to have the following folder structure for a single submission:
103
98
 
104
- ```
99
+ ```text
105
100
  EXAMPLE_SUBMISSION
106
101
  ├── files
107
102
  │ ├── aaaaaaaa00000000aaaaaaaa00000000_blood_normal.read1.fastq.gz
@@ -119,7 +114,6 @@ The only requirements are that `metadata/metadata.json` exists and the `files/`
119
114
  Data files may be nested under subfolders inside `files/` for better organization.
120
115
  For example, each donor could have their own folder for files.
121
116
 
122
-
123
117
  ### Example submission procedure
124
118
 
125
119
  After preparing your submission as outlined above, you can use the following command to validate, encrypt and upload the submission:
@@ -148,7 +142,6 @@ It combines the `validate`, `encrypt`, and `upload` commands (see below).
148
142
  grz-cli submit --submission-dir foo
149
143
  ```
150
144
 
151
-
152
145
  ### validate
153
146
 
154
147
  It is recommended to run this command before continuing with encryption and upload.
@@ -209,7 +202,7 @@ We recommend using Conda or [Pixi](https://pixi.sh/latest).
209
202
 
210
203
  After cloning the desired branch of the `grz-tools` repo locally, you can run `grz-cli` directly from the repo using:
211
204
 
212
- ```
205
+ ```text
213
206
  uv run --project path/to/cloned/grz-tools grz-cli --help
214
207
  ```
215
208
 
@@ -0,0 +1,63 @@
1
+ # Version compatibility check
2
+
3
+ When you run `grz-cli`, it automatically checks whether your installed version is compatible with the current requirements.
4
+ If your version is too old, too new, or simply behind the recommended release, you will be informed before any submission processing tasks begin.
5
+
6
+ ## What happens when you run grz-cli
7
+
8
+ On startup, the tool fetches a version policy from a central location and compares it against your installed version.
9
+ Depending on the result, one of four things will happen:
10
+
11
+ | Situation | What you'll see | Does execution continue? |
12
+ | --------------------------------------------------- | -------------------- | ------------------------ |
13
+ | Your version is too old | Error message | No |
14
+ | Your version works but is behind the recommendation | Warning message | Yes |
15
+ | Your version is newer than the supported maximum | Error message | No |
16
+ | Your version is within the supported range | Confirmation message | Yes |
17
+
18
+ If no policy is currently in effect, the check is silently skipped and execution continues normally.
19
+
20
+ ## What the messages mean
21
+
22
+ **"Your grz-cli version is not supported"**
23
+
24
+ Your installed version is below the minimum required.
25
+ You must upgrade before you can continue.
26
+
27
+ ```text
28
+ Example message: Your grz-cli version (1.2.0) is not supported. Minimum required version is 1.5.0.
29
+ ```
30
+
31
+ **"Upgrading is strongly recommended"**
32
+
33
+ Your version meets the minimum requirement but is behind the recommended release.
34
+ You can continue, but upgrading is advised.
35
+
36
+ ```text
37
+ Example message: You are using grz-cli 1.5.0, but the recommended version is 1.7.0.
38
+ Upgrading is strongly recommended.
39
+ ```
40
+
41
+ **"grz-cli is within the supported and tested range"**
42
+
43
+ Your version is fully compatible.
44
+ No action is needed.
45
+
46
+ ```text
47
+ Example message: grz-cli 1.7.0 is within the supported and tested range.
48
+ ```
49
+
50
+ **"Version is newer than the maximum supported version"**
51
+
52
+ Your version is ahead of what has been tested and approved.
53
+ You must downgrade before you can continue.
54
+
55
+ ```text
56
+ Example message: grz-cli version 2.1.0 is newer than the maximum supported version (2.0.0).
57
+ ```
58
+
59
+ ## Why is there a maximum version limit?
60
+
61
+ Most tools only enforce a minimum version.
62
+ `grz-cli` also enforces an upper bound to prevent untested releases from being used in regulated submission workflows, where the exact toolchain version may need to match what was validated.
63
+ If you have installed a pre-release or a newer version than your organisation currently supports, downgrade to the approved version before submitting.
@@ -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.0.2,<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.1"
@@ -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,56 +0,0 @@
1
- # Version compatibility check
2
-
3
- When you run `grz-cli`, it automatically checks whether your installed version is compatible with the current requirements. If your version is too old, too new, or simply behind the recommended release, you will be informed before any submission processing tasks begin.
4
-
5
- ## What happens when you run grz-cli
6
-
7
- On startup, the tool fetches a version policy from a central location and compares it against your installed version. Depending on the result, one of four things will happen:
8
-
9
- | Situation | What you'll see | Does execution continue? |
10
- |---|---|---|
11
- | Your version is too old | Error message | No |
12
- | Your version works but is behind the recommendation | Warning message | Yes |
13
- | Your version is newer than the supported maximum | Error message | No |
14
- | Your version is within the supported range | Confirmation message | Yes |
15
-
16
- If no policy is currently in effect, the check is silently skipped and execution continues normally.
17
-
18
- ## What the messages mean
19
-
20
- **"Your grz-cli version is not supported"**
21
-
22
- Your installed version is below the minimum required. You must upgrade before you can continue.
23
-
24
- ```
25
- Example message: Your grz-cli version (1.2.0) is not supported. Minimum required version is 1.5.0.
26
- ```
27
-
28
- **"Upgrading is strongly recommended"**
29
-
30
- Your version meets the minimum requirement but is behind the recommended release. You can continue, but upgrading is advised.
31
-
32
- ```
33
- Example message: You are using grz-cli 1.5.0, but the recommended version is 1.7.0.
34
- Upgrading is strongly recommended.
35
- ```
36
-
37
- **"grz-cli is within the supported and tested range"**
38
-
39
- Your version is fully compatible. No action is needed.
40
-
41
- ```
42
- Example message: grz-cli 1.7.0 is within the supported and tested range.
43
- ```
44
-
45
- **"Version is newer than the maximum supported version"**
46
-
47
- Your version is ahead of what has been tested and approved. You must downgrade before you can continue.
48
-
49
- ```
50
- Example message: grz-cli version 2.1.0 is newer than the maximum supported version (2.0.0).
51
- ```
52
-
53
-
54
- ## Why is there a maximum version limit?
55
-
56
- Most tools only enforce a minimum version. `grz-cli` also enforces an upper bound to prevent untested releases from being used in regulated submission workflows, where the exact toolchain version may need to match what was validated. If you have installed a pre-release or a newer version than your organisation currently supports, downgrade to the approved version before submitting.
@@ -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