Browse Source

Merge "Added document wrapping command"

Zuul 2 weeks ago
parent
commit
6348b83e3c
4 changed files with 238 additions and 28 deletions
  1. 79
    28
      doc/source/cli/cli.rst
  2. 57
    0
      pegleg/cli.py
  3. 45
    0
      pegleg/engine/secrets.py
  4. 57
    0
      tests/unit/test_cli.py

+ 79
- 28
doc/source/cli/cli.rst View File

@@ -421,12 +421,36 @@ Usage:
421 421
     ./pegleg.sh site <options> upload <site_name> --context-marker=<uuid>
422 422
 
423 423
 Site Secrets Group
424
-==================
424
+------------------
425 425
 
426 426
 Subgroup of :ref:`site-group`.
427 427
 
428
+A sub-group of site command group, which allows you to perform secrets
429
+level operations for secrets documents of a site.
430
+
431
+.. note::
432
+
433
+  For the CLI commands ``encrypt``, ``decrypt``, ``generate-pki``, and ``wrap``
434
+  in the ``secrets`` command
435
+  group, which encrypt or decrypt site secrets, two  environment variables,
436
+  ``PEGLEG_PASSPHRASE``, and ``PEGLEG_SALT``, are  used to capture the
437
+  master passphrase, and the salt needed for encryption and decryption of the
438
+  site secrets. The contents of ``PEGLEG_PASSPHRASE``, and ``PEGLEG_SALT``
439
+  are not generated by Pegleg, but are created externally, and set by
440
+  deployment engineers or tooling.
441
+
442
+  A minimum length of 24 for master passphrases will be checked by all CLI
443
+  commands, which use the ``PEGLEG_PASSPHRASE`` and ``PEGLEG_SALT``.
444
+  All other criteria around master passphrase strength are assumed to be
445
+  enforced elsewhere.
446
+
447
+::
448
+
449
+  ./pegleg.sh site -r <site_repo> -e <extra_repo> secrets <command> <options>
450
+
451
+
428 452
 Generate PKI
429
-------------
453
+^^^^^^^^^^^^
430 454
 
431 455
 Generate certificates and keys according to all PKICatalog documents in the
432 456
 site using the PKI module. Regenerating certificates can be
@@ -454,7 +478,7 @@ Dashes in the document names will be converted to underscores for consistency.
454 478
 Name of site.
455 479
 
456 480
 Examples
457
-^^^^^^^^
481
+""""""""
458 482
 
459 483
 ::
460 484
 
@@ -472,31 +496,6 @@ Examples
472 496
 
473 497
 .. _command-line-repository-overrides:
474 498
 
475
-Secrets
476
--------
477
-
478
-A sub-group of site command group, which allows you to perform secrets
479
-level operations for secrets documents of a site.
480
-
481
-.. note::
482
-
483
-  For the CLI commands ``encrypt`` and ``decrypt`` in the ``secrets`` command
484
-  group, which encrypt or decrypt site secrets, two  environment variables,
485
-  ``PEGLEG_PASSPHRASE``, and ``PEGLEG_SALT``, are  used to capture the
486
-  master passphrase, and the salt needed for encryption and decryption of the
487
-  site secrets. The contents of ``PEGLEG_PASSPHRASE``, and ``PEGLEG_SALT``
488
-  are not generated by Pegleg, but are created externally, and set by a
489
-  deployment engineers or tooling.
490
-
491
-  A minimum length of 24 for master passphrases will be checked by all CLI
492
-  commands, which use the ``PEGLEG_PASSPHRASE``. All other criteria around
493
-  master passphrase strength are assumed to be enforced elsewhere.
494
-
495
-::
496
-
497
-  ./pegleg.sh site -r <site_repo> -e <extra_repo> secrets <command> <options>
498
-
499
-
500 499
 Encrypt
501 500
 ^^^^^^^
502 501
 
@@ -612,6 +611,58 @@ Example:
612 611
       secrets decrypt site1 -f \
613 612
       /opt/security-manifests/site/site1/passwords/password1.yaml
614 613
 
614
+Wrap
615
+^^^^
616
+
617
+Wrap bare files (e.g. pem or crt) in a PeglegManagedDocument and optionally encrypt them.
618
+
619
+**site_name** (Required).
620
+
621
+Name of site.
622
+
623
+**-a / --author**
624
+
625
+Identifying name of the author generating new certificates. Used
626
+for tracking provenance information in the PeglegManagedDocuments.
627
+An attempt is made to automatically determine this value,
628
+but should be provided.
629
+
630
+**-f / --filename**
631
+
632
+The relative path to the file to be wrapped.
633
+
634
+**-o / --output-path**
635
+
636
+The output path for the wrapped file. (default: input path with the extension
637
+replaced with .yaml)
638
+
639
+**-s / --schema**
640
+
641
+The schema for the document to be wrapped, e.g. deckhand/Certificate/v1
642
+
643
+**-n / --name**
644
+
645
+The name for the document to be wrapped, e.g. new-cert.
646
+
647
+**-l / --layer**
648
+
649
+The layer for the document to be wrapped, e.g. site.
650
+
651
+**--encrypt / --no-encrypt**
652
+
653
+A flag specifying whether to encrypt the output file. (default: True)
654
+
655
+Examples
656
+""""""""
657
+
658
+::
659
+
660
+  ./pegleg.sh site -r /home/myuser/myrepo \
661
+    secrets wrap -a myuser -f secrets/certificates/new_cert.crt \
662
+    -o secrets/certificates/new_cert.yaml -s "deckhand/Certificate/v1" \
663
+    -n "new-cert" -l site mysite
664
+
665
+
615 666
 genesis_bundle
616 667
 --------------
617 668
 

+ 57
- 0
pegleg/cli.py View File

@@ -23,6 +23,7 @@ from pegleg import config
23 23
 from pegleg import engine
24 24
 from pegleg.engine import bundle
25 25
 from pegleg.engine import catalog
26
+from pegleg.engine.secrets import wrap_secret
26 27
 from pegleg.engine.util.pegleg_secret_management import PeglegSecretManagement
27 28
 from pegleg.engine.util.shipyard_helper import ShipyardHelper
28 29
 
@@ -415,6 +416,62 @@ def generate_pki(site_name, author):
415 416
     click.echo("Generated PKI files written to:\n%s" % '\n'.join(output_paths))
416 417
 
417 418
 
419
+@secrets.command(
420
+    'wrap',
421
+    help='Wrap bare files (e.g. pem or crt) in a PeglegManagedDocument '
422
+         'and encrypt them (by default).')
423
+@click.option(
424
+    '-a',
425
+    '--author',
426
+    'author',
427
+    help='Author for the new wrapped file.')
428
+@click.option(
429
+    '-f',
430
+    '--filename',
431
+    'file_name',
432
+    help='The relative file path for the file to be wrapped.')
433
+@click.option(
434
+    '-o',
435
+    '--output-path',
436
+    'output_path',
437
+    required=False,
438
+    help='The output path for the wrapped file. (default: input path with '
439
+         '.yaml)')
440
+@click.option(
441
+    '-s',
442
+    '--schema',
443
+    'schema',
444
+    help='The schema for the document to be wrapped, e.g. '
445
+         'deckhand/Certificate/v1')
446
+@click.option(
447
+    '-n',
448
+    '--name',
449
+    'name',
450
+    help='The name for the document to be wrapped, e.g. new-cert')
451
+@click.option(
452
+    '-l',
453
+    '--layer',
454
+    'layer',
455
+    help='The layer for the document to be wrapped., e.g. site.')
456
+@click.option(
457
+    '--encrypt/--no-encrypt',
458
+    'encrypt',
459
+    is_flag=True,
460
+    default=True,
461
+    help='Whether to encrypt the wrapped file (default: True).')
462
+@click.argument('site_name')
463
+def wrap_secret_cli(*, site_name, author, file_name, output_path, schema,
464
+                    name, layer, encrypt):
465
+    """Wrap a bare secrets file in a YAML and ManagedDocument.
466
+
467
+    """
468
+
469
+    engine.repository.process_repositories(site_name,
470
+                                           overwrite_existing=True)
471
+    wrap_secret(author, file_name, output_path, schema,
472
+                name, layer, encrypt)
473
+
474
+
418 475
 @site.command(
419 476
     'genesis_bundle',
420 477
     help='Construct the genesis deployment bundle.')

+ 45
- 0
pegleg/engine/secrets.py View File

@@ -14,11 +14,14 @@
14 14
 
15 15
 import logging
16 16
 import os
17
+import yaml
17 18
 
18 19
 from pegleg.engine.generators.passphrase_generator import PassphraseGenerator
19 20
 from pegleg.engine.util.cryptostring import CryptoString
20 21
 from pegleg.engine.util import definition
21 22
 from pegleg.engine.util import files
23
+from pegleg.engine.util.pegleg_managed_document import \
24
+    PeglegManagedSecretsDocument as PeglegManagedSecret
22 25
 from pegleg.engine.util.pegleg_secret_management import PeglegSecretManagement
23 26
 
24 27
 __all__ = ('encrypt', 'decrypt', 'generate_passphrases')
@@ -141,3 +144,45 @@ def generate_crypto_string(length):
141 144
     """
142 145
 
143 146
     return CryptoString().get_crypto_string(length)
147
+
148
+
149
+def wrap_secret(author, file_name, output_path, schema,
150
+                name, layer, encrypt):
151
+    """Wrap a bare secrets file in a YAML and ManagedDocument.
152
+
153
+    :param author: author for ManagedDocument
154
+    :param file_name: file path for input file
155
+    :param output_path: file path for output file
156
+    :param schema: schema for wrapped document
157
+    :param name: name for wrapped document
158
+    :param layer: layer for wrapped document
159
+    :param encrypt: whether to encrypt the output doc
160
+    """
161
+
162
+    if not output_path:
163
+        output_path = os.path.splitext(file_name)[0] + ".yaml"
164
+
165
+    with open(file_name, "r") as in_fi:
166
+        data = in_fi.read()
167
+
168
+    inner_doc = {
169
+        "schema": schema,
170
+        "data": data,
171
+        "metadata": {
172
+            "layeringDefinition": {
173
+                "abstract": False,
174
+                "layer": layer
175
+            },
176
+            "name": name,
177
+            "schema": "metadata/Document/v1",
178
+            "storagePolicy": "encrypted" if encrypt else "cleartext"
179
+        }
180
+    }
181
+    managed_secret = PeglegManagedSecret(inner_doc, author=author)
182
+    if encrypt:
183
+        psm = PeglegSecretManagement(docs=[inner_doc], author=author)
184
+        output_doc = psm.get_encrypted_secrets()[0][0]
185
+    else:
186
+        output_doc = managed_secret.pegleg_document
187
+    with open(output_path, "w") as output_fi:
188
+        yaml.safe_dump(output_doc, output_fi)

+ 57
- 0
tests/unit/test_cli.py View File

@@ -36,6 +36,16 @@ TEST_PARAMS = {
36 36
     "repo_url": "https://github.com/openstack/airship-treasuremap.git",
37 37
 }
38 38
 
39
+test_cert = """
40
+-----BEGIN CERTIFICATE-----
41
+
42
+DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF
43
+DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF
44
+DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF
45
+DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF
46
+
47
+-----END CERTIFICATE-----
48
+"""
39 49
 
40 50
 @pytest.mark.skipif(
41 51
     not test_utils.is_connected(),
@@ -552,6 +562,53 @@ class TestSiteSecretsActions(BaseCLIActionTest):
552 562
         result = self.runner.invoke(cli.site, ['-r', repo_path] + secrets_opts)
553 563
         assert result.exit_code == 0, result.output
554 564
 
565
+    @mock.patch.dict(os.environ, {
566
+        "PEGLEG_PASSPHRASE": "123456789012345678901234567890",
567
+        "PEGLEG_SALT": "123456"
568
+    })
569
+    def test_site_secrets_wrap(self):
570
+        """Validates ``generate-pki`` action using local repo path."""
571
+        # Scenario:
572
+        #
573
+        # 1) Encrypt a file in a local repo
574
+
575
+        repo_path = self.treasuremap_path
576
+        file_dir = os.path.join(repo_path, "site", "airship-seaworthy",
577
+                                "secrets", "certificates")
578
+        file_path = os.path.join(file_dir, "test.crt")
579
+        output_path = os.path.join(file_dir, "test.yaml")
580
+
581
+        with open(file_path, "w") as test_crt_fi:
582
+            test_crt_fi.write(test_cert)
583
+        secrets_opts = ['secrets', 'wrap', "-a", "lm734y", "-f", file_path,
584
+                        "-s", "deckhand/Certificate/v1",
585
+                        "-n", "test-certificate", "-l", "site", "--no-encrypt",
586
+                       self.site_name]
587
+        result = self.runner.invoke(cli.site, ["-r", repo_path] + secrets_opts)
588
+        assert result.exit_code == 0
589
+
590
+        with open(output_path, "r") as output_fi:
591
+            doc = yaml.safe_load(output_fi)
592
+            assert doc["data"]["managedDocument"]["data"] == test_cert
593
+            assert doc["data"]["managedDocument"]["schema"] == "deckhand/Certificate/v1"
594
+            assert doc["data"]["managedDocument"]["metadata"]["name"] == "test-certificate"
595
+            assert doc["data"]["managedDocument"]["metadata"]["layeringDefinition"]["layer"] == "site"
596
+            assert doc["data"]["managedDocument"]["metadata"]["storagePolicy"] == "cleartext"
597
+
598
+        os.remove(output_path)
599
+        secrets_opts = ['secrets', 'wrap', "-a", "lm734y", "-f", file_path,
600
+                        "-o", output_path, "-s", "deckhand/Certificate/v1",
601
+                        "-n", "test-certificate", "-l", "site",
602
+                       self.site_name]
603
+        result = self.runner.invoke(cli.site, ["-r", repo_path] + secrets_opts)
604
+        assert result.exit_code == 0
605
+
606
+        with open(output_path, "r") as output_fi:
607
+            doc = yaml.safe_load(output_fi)
608
+            assert "encrypted" in doc["data"]
609
+            assert "managedDocument" in doc["data"]
610
+
611
+
555 612
 class TestTypeCliActions(BaseCLIActionTest):
556 613
     """Tests type-level CLI actions."""
557 614
 

Loading…
Cancel
Save