netbox-secrets-manager 0.1.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 (66) hide show
  1. netbox_secrets_manager-0.1.0/PKG-INFO +117 -0
  2. netbox_secrets_manager-0.1.0/README.md +96 -0
  3. netbox_secrets_manager-0.1.0/netbox_secrets_manager/__init__.py +103 -0
  4. netbox_secrets_manager-0.1.0/netbox_secrets_manager/api/__init__.py +0 -0
  5. netbox_secrets_manager-0.1.0/netbox_secrets_manager/api/exceptions.py +28 -0
  6. netbox_secrets_manager-0.1.0/netbox_secrets_manager/api/reveal.py +30 -0
  7. netbox_secrets_manager-0.1.0/netbox_secrets_manager/api/serializers.py +191 -0
  8. netbox_secrets_manager-0.1.0/netbox_secrets_manager/api/urls.py +12 -0
  9. netbox_secrets_manager-0.1.0/netbox_secrets_manager/api/views.py +182 -0
  10. netbox_secrets_manager-0.1.0/netbox_secrets_manager/audit.py +99 -0
  11. netbox_secrets_manager-0.1.0/netbox_secrets_manager/backends/__init__.py +36 -0
  12. netbox_secrets_manager-0.1.0/netbox_secrets_manager/backends/aws.py +295 -0
  13. netbox_secrets_manager-0.1.0/netbox_secrets_manager/backends/base.py +91 -0
  14. netbox_secrets_manager-0.1.0/netbox_secrets_manager/backends/memory.py +158 -0
  15. netbox_secrets_manager-0.1.0/netbox_secrets_manager/choices.py +63 -0
  16. netbox_secrets_manager-0.1.0/netbox_secrets_manager/compat.py +61 -0
  17. netbox_secrets_manager-0.1.0/netbox_secrets_manager/config.py +117 -0
  18. netbox_secrets_manager-0.1.0/netbox_secrets_manager/constants.py +58 -0
  19. netbox_secrets_manager-0.1.0/netbox_secrets_manager/events.py +54 -0
  20. netbox_secrets_manager-0.1.0/netbox_secrets_manager/exceptions.py +80 -0
  21. netbox_secrets_manager-0.1.0/netbox_secrets_manager/filtersets.py +79 -0
  22. netbox_secrets_manager-0.1.0/netbox_secrets_manager/forms.py +315 -0
  23. netbox_secrets_manager-0.1.0/netbox_secrets_manager/graphql/__init__.py +0 -0
  24. netbox_secrets_manager-0.1.0/netbox_secrets_manager/graphql/filters.py +38 -0
  25. netbox_secrets_manager-0.1.0/netbox_secrets_manager/graphql/schema.py +26 -0
  26. netbox_secrets_manager-0.1.0/netbox_secrets_manager/graphql/types.py +135 -0
  27. netbox_secrets_manager-0.1.0/netbox_secrets_manager/graphql_extensions.py +65 -0
  28. netbox_secrets_manager-0.1.0/netbox_secrets_manager/management/__init__.py +0 -0
  29. netbox_secrets_manager-0.1.0/netbox_secrets_manager/management/commands/__init__.py +0 -0
  30. netbox_secrets_manager-0.1.0/netbox_secrets_manager/management/commands/nbsm_reconcile.py +154 -0
  31. netbox_secrets_manager-0.1.0/netbox_secrets_manager/migrations/0001_initial.py +129 -0
  32. netbox_secrets_manager-0.1.0/netbox_secrets_manager/migrations/__init__.py +0 -0
  33. netbox_secrets_manager-0.1.0/netbox_secrets_manager/models.py +291 -0
  34. netbox_secrets_manager-0.1.0/netbox_secrets_manager/naming.py +94 -0
  35. netbox_secrets_manager-0.1.0/netbox_secrets_manager/navigation.py +46 -0
  36. netbox_secrets_manager-0.1.0/netbox_secrets_manager/schema.py +456 -0
  37. netbox_secrets_manager-0.1.0/netbox_secrets_manager/search.py +36 -0
  38. netbox_secrets_manager-0.1.0/netbox_secrets_manager/services.py +207 -0
  39. netbox_secrets_manager-0.1.0/netbox_secrets_manager/signals.py +126 -0
  40. netbox_secrets_manager-0.1.0/netbox_secrets_manager/tables.py +76 -0
  41. netbox_secrets_manager-0.1.0/netbox_secrets_manager/template_content.py +119 -0
  42. netbox_secrets_manager-0.1.0/netbox_secrets_manager/templates/netbox_secrets_manager/inc/secrets_panel.html +41 -0
  43. netbox_secrets_manager-0.1.0/netbox_secrets_manager/templates/netbox_secrets_manager/object_secrets.html +4 -0
  44. netbox_secrets_manager-0.1.0/netbox_secrets_manager/templates/netbox_secrets_manager/secret.html +179 -0
  45. netbox_secrets_manager-0.1.0/netbox_secrets_manager/templates/netbox_secrets_manager/secrettype.html +69 -0
  46. netbox_secrets_manager-0.1.0/netbox_secrets_manager/tests/__init__.py +0 -0
  47. netbox_secrets_manager-0.1.0/netbox_secrets_manager/tests/test_api.py +358 -0
  48. netbox_secrets_manager-0.1.0/netbox_secrets_manager/tests/test_backend.py +353 -0
  49. netbox_secrets_manager-0.1.0/netbox_secrets_manager/tests/test_forms.py +281 -0
  50. netbox_secrets_manager-0.1.0/netbox_secrets_manager/tests/test_models.py +313 -0
  51. netbox_secrets_manager-0.1.0/netbox_secrets_manager/tests/test_naming.py +67 -0
  52. netbox_secrets_manager-0.1.0/netbox_secrets_manager/tests/test_no_plaintext.py +250 -0
  53. netbox_secrets_manager-0.1.0/netbox_secrets_manager/tests/test_permissions.py +265 -0
  54. netbox_secrets_manager-0.1.0/netbox_secrets_manager/tests/test_reconcile.py +109 -0
  55. netbox_secrets_manager-0.1.0/netbox_secrets_manager/tests/test_schema.py +257 -0
  56. netbox_secrets_manager-0.1.0/netbox_secrets_manager/tests/test_services.py +243 -0
  57. netbox_secrets_manager-0.1.0/netbox_secrets_manager/tests/utils.py +154 -0
  58. netbox_secrets_manager-0.1.0/netbox_secrets_manager/urls.py +27 -0
  59. netbox_secrets_manager-0.1.0/netbox_secrets_manager/views.py +276 -0
  60. netbox_secrets_manager-0.1.0/netbox_secrets_manager.egg-info/PKG-INFO +117 -0
  61. netbox_secrets_manager-0.1.0/netbox_secrets_manager.egg-info/SOURCES.txt +64 -0
  62. netbox_secrets_manager-0.1.0/netbox_secrets_manager.egg-info/dependency_links.txt +1 -0
  63. netbox_secrets_manager-0.1.0/netbox_secrets_manager.egg-info/requires.txt +5 -0
  64. netbox_secrets_manager-0.1.0/netbox_secrets_manager.egg-info/top_level.txt +1 -0
  65. netbox_secrets_manager-0.1.0/pyproject.toml +64 -0
  66. netbox_secrets_manager-0.1.0/setup.cfg +4 -0
@@ -0,0 +1,117 @@
1
+ Metadata-Version: 2.4
2
+ Name: netbox-secrets-manager
3
+ Version: 0.1.0
4
+ Summary: Attach secrets to NetBox objects, backed by AWS Secrets Manager
5
+ Author: SF Compute
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/sfcompute/netbox-secrets-manager
8
+ Project-URL: Source, https://github.com/sfcompute/netbox-secrets-manager
9
+ Keywords: netbox,netbox-plugin,secrets,aws,secrets-manager
10
+ Classifier: Framework :: Django
11
+ Classifier: Intended Audience :: System Administrators
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Topic :: Security
15
+ Requires-Python: >=3.12
16
+ Description-Content-Type: text/markdown
17
+ Requires-Dist: boto3>=1.34
18
+ Provides-Extra: dev
19
+ Requires-Dist: moto[secretsmanager,sts]>=5.0; extra == "dev"
20
+ Requires-Dist: ruff>=0.6; extra == "dev"
21
+
22
+ # netbox-secrets-manager
23
+
24
+ Attach secrets to NetBox objects — BMC credentials on a device, a private
25
+ certificate on a virtual machine — while keeping **AWS Secrets Manager as the
26
+ system of record**. NetBox stores only metadata: the ARN, which object the
27
+ secret belongs to, and which shape it has.
28
+
29
+ No field on any model in this plugin ever holds plaintext or ciphertext.
30
+
31
+ ## Why not netbox-secrets?
32
+
33
+ [netbox-secrets](https://github.com/Onemind-Services-LLC/netbox-secrets)
34
+ encrypts secrets inside NetBox: a per-user RSA keypair wraps a per-install
35
+ master key, which wraps a session key, which encrypts each value. Decryption is
36
+ a *cryptographic* capability, held by whoever has the private key.
37
+
38
+ That makes one common arrangement impossible to express: an operator who may
39
+ rotate a BMC password but may not read it. This plugin moves storage to AWS and
40
+ makes decryption a *permission* instead, so `view` (list without revealing),
41
+ `decrypt` (reveal), `add`/`change` (write) and `delete` are granted separately.
42
+
43
+ The trade-off is explicit and you should read
44
+ [docs/security.md](docs/security.md) before deploying: the application's IAM
45
+ role holds `GetSecretValue` at all times, so `decrypt` is an application-layer
46
+ control, not an IAM one.
47
+
48
+ ## What it does
49
+
50
+ - **Typed secrets.** A `SecretType` carries a JSON Schema, so "BMC Credentials"
51
+ (`username`, `password`) and "Private Cert" (one large `certificate` field)
52
+ are data an admin edits, not code.
53
+ - **Four separable permissions**, including a custom `decrypt` action with
54
+ object-level constraints — scope reveal to one secret type, or one app.
55
+ - **Partial writes without reading.** An operator with `change` but not
56
+ `decrypt` can rotate one field; the server merges server-side and returns
57
+ nothing.
58
+ - **REST and GraphQL**, with plaintext reachable only through explicit,
59
+ separately-permissioned endpoints.
60
+ - **Lifecycle management.** The plugin names, creates and deletes the AWS
61
+ secret, and `nbsm_reconcile` reports drift in both directions.
62
+
63
+ ## Requirements
64
+
65
+ - NetBox **4.6.0** or later (4.7 supported)
66
+ - Python 3.12+
67
+ - An AWS account, and credentials reachable by the default boto3 chain
68
+
69
+ ## Installation
70
+
71
+ ```bash
72
+ pip install netbox-secrets-manager
73
+ ```
74
+
75
+ Add to `configuration.py`:
76
+
77
+ ```python
78
+ PLUGINS = ['netbox_secrets_manager']
79
+
80
+ PLUGINS_CONFIG = {
81
+ 'netbox_secrets_manager': {
82
+ 'apps': ['dcim.device', 'virtualization.virtualmachine'],
83
+ 'aws_region': 'us-east-1',
84
+ 'instance_id': 'prod-netbox',
85
+ },
86
+ }
87
+ ```
88
+
89
+ Then run migrations and collect static files. Full options are documented in
90
+ [docs/configuration.md](docs/configuration.md).
91
+
92
+ ## Development
93
+
94
+ Tasks run through [just](https://github.com/casey/just); `just` on its own lists
95
+ everything.
96
+
97
+ ```bash
98
+ just up # NetBox + Postgres + Redis + LocalStack
99
+ just test # the suite
100
+ just check # lint, migration drift and tests, as CI runs them
101
+ ```
102
+
103
+ NetBox comes up on <http://localhost:8000> as `admin` / `admin`, with LocalStack
104
+ standing in for AWS.
105
+
106
+ The suite runs inside the stack rather than a local virtualenv, because it is
107
+ built on NetBox's own test utilities and needs NetBox's settings, database and
108
+ app registry.
109
+
110
+ `just test-integration` additionally runs the backend contract against
111
+ LocalStack. It is worth running before a release: moto is not faithful on two
112
+ behaviours this plugin depends on, so those tests are the only coverage of the
113
+ adopt and restore-previous paths against a real Secrets Manager implementation.
114
+
115
+ ## License
116
+
117
+ Apache-2.0
@@ -0,0 +1,96 @@
1
+ # netbox-secrets-manager
2
+
3
+ Attach secrets to NetBox objects — BMC credentials on a device, a private
4
+ certificate on a virtual machine — while keeping **AWS Secrets Manager as the
5
+ system of record**. NetBox stores only metadata: the ARN, which object the
6
+ secret belongs to, and which shape it has.
7
+
8
+ No field on any model in this plugin ever holds plaintext or ciphertext.
9
+
10
+ ## Why not netbox-secrets?
11
+
12
+ [netbox-secrets](https://github.com/Onemind-Services-LLC/netbox-secrets)
13
+ encrypts secrets inside NetBox: a per-user RSA keypair wraps a per-install
14
+ master key, which wraps a session key, which encrypts each value. Decryption is
15
+ a *cryptographic* capability, held by whoever has the private key.
16
+
17
+ That makes one common arrangement impossible to express: an operator who may
18
+ rotate a BMC password but may not read it. This plugin moves storage to AWS and
19
+ makes decryption a *permission* instead, so `view` (list without revealing),
20
+ `decrypt` (reveal), `add`/`change` (write) and `delete` are granted separately.
21
+
22
+ The trade-off is explicit and you should read
23
+ [docs/security.md](docs/security.md) before deploying: the application's IAM
24
+ role holds `GetSecretValue` at all times, so `decrypt` is an application-layer
25
+ control, not an IAM one.
26
+
27
+ ## What it does
28
+
29
+ - **Typed secrets.** A `SecretType` carries a JSON Schema, so "BMC Credentials"
30
+ (`username`, `password`) and "Private Cert" (one large `certificate` field)
31
+ are data an admin edits, not code.
32
+ - **Four separable permissions**, including a custom `decrypt` action with
33
+ object-level constraints — scope reveal to one secret type, or one app.
34
+ - **Partial writes without reading.** An operator with `change` but not
35
+ `decrypt` can rotate one field; the server merges server-side and returns
36
+ nothing.
37
+ - **REST and GraphQL**, with plaintext reachable only through explicit,
38
+ separately-permissioned endpoints.
39
+ - **Lifecycle management.** The plugin names, creates and deletes the AWS
40
+ secret, and `nbsm_reconcile` reports drift in both directions.
41
+
42
+ ## Requirements
43
+
44
+ - NetBox **4.6.0** or later (4.7 supported)
45
+ - Python 3.12+
46
+ - An AWS account, and credentials reachable by the default boto3 chain
47
+
48
+ ## Installation
49
+
50
+ ```bash
51
+ pip install netbox-secrets-manager
52
+ ```
53
+
54
+ Add to `configuration.py`:
55
+
56
+ ```python
57
+ PLUGINS = ['netbox_secrets_manager']
58
+
59
+ PLUGINS_CONFIG = {
60
+ 'netbox_secrets_manager': {
61
+ 'apps': ['dcim.device', 'virtualization.virtualmachine'],
62
+ 'aws_region': 'us-east-1',
63
+ 'instance_id': 'prod-netbox',
64
+ },
65
+ }
66
+ ```
67
+
68
+ Then run migrations and collect static files. Full options are documented in
69
+ [docs/configuration.md](docs/configuration.md).
70
+
71
+ ## Development
72
+
73
+ Tasks run through [just](https://github.com/casey/just); `just` on its own lists
74
+ everything.
75
+
76
+ ```bash
77
+ just up # NetBox + Postgres + Redis + LocalStack
78
+ just test # the suite
79
+ just check # lint, migration drift and tests, as CI runs them
80
+ ```
81
+
82
+ NetBox comes up on <http://localhost:8000> as `admin` / `admin`, with LocalStack
83
+ standing in for AWS.
84
+
85
+ The suite runs inside the stack rather than a local virtualenv, because it is
86
+ built on NetBox's own test utilities and needs NetBox's settings, database and
87
+ app registry.
88
+
89
+ `just test-integration` additionally runs the backend contract against
90
+ LocalStack. It is worth running before a release: moto is not faithful on two
91
+ behaviours this plugin depends on, so those tests are the only coverage of the
92
+ adopt and restore-previous paths against a real Secrets Manager implementation.
93
+
94
+ ## License
95
+
96
+ Apache-2.0
@@ -0,0 +1,103 @@
1
+ """NetBox plugin for attaching secrets to objects, backed by AWS Secrets Manager.
2
+
3
+ NetBox stores only metadata: the ARN, which object a secret belongs to, and
4
+ which shape it has. AWS Secrets Manager is the system of record. No field on
5
+ any model in this package ever holds plaintext or ciphertext.
6
+ """
7
+
8
+ from netbox.plugins import PluginConfig
9
+
10
+ __version__ = '0.1.0'
11
+
12
+
13
+ class SecretsManagerConfig(PluginConfig):
14
+ name = 'netbox_secrets_manager'
15
+ verbose_name = 'Secrets Manager'
16
+ description = 'Attach secrets to NetBox objects, backed by AWS Secrets Manager'
17
+ version = __version__
18
+ author = 'SF Compute'
19
+ base_url = 'secrets-manager'
20
+
21
+ # 4.6.0 is the floor because custom model actions (the `decrypt` permission)
22
+ # were introduced there. The upper bound is deliberate rather than open:
23
+ # this plugin reaches into a handful of undocumented internals (see
24
+ # compat.py), so a new major should be tested before it is allowed.
25
+ min_version = '4.6.0'
26
+ max_version = '4.7.99'
27
+
28
+ menu = 'navigation.menu'
29
+ search_indexes = 'search.indexes'
30
+ # Points at the list inside the module, not the module itself.
31
+ graphql_schema = 'graphql.schema.schema'
32
+ # Ignored on 4.6, which has no collection point for it. The module guards
33
+ # itself as well, so it is inert there even if something force-imports it.
34
+ graphql_type_extensions = 'graphql_extensions.type_extensions'
35
+ graphql_filter_extensions = 'graphql_extensions.filter_extensions'
36
+
37
+ required_settings = ['apps']
38
+
39
+ default_settings = {
40
+ # --- AWS -----------------------------------------------------------
41
+ 'aws_region': 'us-east-1',
42
+ # Left None to use the default boto3 credential chain: IRSA, instance
43
+ # profile, environment, ~/.aws. Set to assume a role instead.
44
+ 'assume_role_arn': None,
45
+ 'assume_role_session_name': 'netbox-secrets-manager',
46
+ 'assume_role_external_id': None,
47
+ # LocalStack or a VPC endpoint.
48
+ 'endpoint_url': None,
49
+ # None means AWS's own aws/secretsmanager key.
50
+ 'kms_key_id': None,
51
+ # Merged into the botocore Config; see backends/aws.py for the defaults
52
+ # this overrides.
53
+ 'boto_config': {},
54
+ # Tagged onto every secret this install owns. Two NetBox instances
55
+ # sharing one AWS account MUST set different values, or each will treat
56
+ # the other's secrets as its own and may overwrite them.
57
+ 'instance_id': 'default',
58
+ # --- Naming and lifecycle -------------------------------------------
59
+ 'secret_name_template': 'netbox/{app_label}/{model}/{object_id}/{secret_type}/{name}',
60
+ 'secret_tags': {'ManagedBy': 'netbox'},
61
+ # 7-30 schedules deletion with that recovery window; 0 force-deletes
62
+ # immediately; None leaves the AWS secret alone and only drops NetBox's
63
+ # metadata row.
64
+ 'deletion_recovery_days': 30,
65
+ # --- Limits ---------------------------------------------------------
66
+ # Each decrypt is one GetSecretValue call. Without a cap, one bulk
67
+ # request or one GraphQL query can turn into thousands.
68
+ 'bulk_decrypt_max': 100,
69
+ 'graphql_decrypt_max': 25,
70
+ # --- UI -------------------------------------------------------------
71
+ # Vocabulary matches netbox-secrets so the migration is familiar.
72
+ 'display_default': 'right_page',
73
+ 'display_setting': {},
74
+ 'top_level_menu': True,
75
+ # --- Backend --------------------------------------------------------
76
+ # Swap for 'netbox_secrets_manager.backends.memory.MemoryBackend' in
77
+ # tests, or point at your own SecretBackend subclass.
78
+ 'backend': 'netbox_secrets_manager.backends.aws.AWSSecretsManagerBackend',
79
+ }
80
+
81
+ def ready(self):
82
+ super().ready()
83
+
84
+ from .config import validate_config
85
+
86
+ # A bad `apps` entry or an illegal recovery window is a deployment
87
+ # mistake. Surface it at startup, not at first write.
88
+ validate_config()
89
+
90
+ # Imported for their registration side effects only:
91
+ # events - registers the event types webhooks and event rules fire on.
92
+ # signals - AWS deletion on row delete, plus the post_delete receivers
93
+ # that clean up secrets orphaned by a deleted parent object
94
+ # (a GenericForeignKey does not cascade).
95
+ from . import ( # noqa: F401
96
+ events,
97
+ signals,
98
+ )
99
+
100
+ signals.connect_orphan_receivers()
101
+
102
+
103
+ config = SecretsManagerConfig
@@ -0,0 +1,28 @@
1
+ """Mapping backend failures onto HTTP statuses.
2
+
3
+ Without this, every AWS problem - throttling, an expired role, a network blip -
4
+ surfaces as a bare 500 that says nothing useful and looks like a plugin bug.
5
+ """
6
+
7
+ from rest_framework.response import Response
8
+ from rest_framework.views import exception_handler as drf_exception_handler
9
+
10
+ from ..exceptions import SchemaValidationError, SecretBackendError
11
+
12
+
13
+ def secrets_exception_handler(exc, context):
14
+ """DRF exception handler covering our own exception families.
15
+
16
+ Wire up with ``REST_FRAMEWORK['EXCEPTION_HANDLER']``, or rely on the
17
+ viewset's ``handle_exception`` override, which calls this directly.
18
+ """
19
+ if isinstance(exc, SchemaValidationError):
20
+ return Response(exc.errors, status=400)
21
+
22
+ if isinstance(exc, SecretBackendError):
23
+ return Response(
24
+ {'detail': str(exc), 'aws_code': exc.aws_code},
25
+ status=exc.http_status,
26
+ )
27
+
28
+ return drf_exception_handler(exc, context)
@@ -0,0 +1,30 @@
1
+ """The only serializers in this package that can represent a secret value.
2
+
3
+ Kept in its own module, imported solely by the decrypt actions, and never set
4
+ as a viewset's ``serializer_class``. That last point matters for more than
5
+ tidiness: it keeps drf-spectacular from attaching this shape to any CRUD
6
+ operation in the generated schema.
7
+ """
8
+
9
+ from rest_framework import serializers
10
+
11
+
12
+ class SecretValueSerializer(serializers.Serializer):
13
+ """A decrypted secret. Output only."""
14
+
15
+ id = serializers.IntegerField(read_only=True)
16
+ name = serializers.CharField(read_only=True)
17
+ secret_type = serializers.CharField(read_only=True)
18
+ data = serializers.DictField(read_only=True)
19
+ retrieved_at = serializers.DateTimeField(read_only=True)
20
+
21
+
22
+ class BulkDecryptSerializer(serializers.Serializer):
23
+ """Input for the bulk decrypt endpoint."""
24
+
25
+ ids = serializers.ListField(
26
+ child=serializers.IntegerField(),
27
+ allow_empty=False,
28
+ help_text='Secret IDs to decrypt. IDs the caller may not decrypt are omitted from the response '
29
+ 'rather than raising, so a partial grant still returns what it can.',
30
+ )
@@ -0,0 +1,191 @@
1
+ """Serializers that cannot emit a secret value.
2
+
3
+ This module contains **no serializer capable of representing plaintext**. The
4
+ one that can lives in ``reveal.py`` and is imported only by the decrypt
5
+ actions. That split is the first and strongest of the guarantees here, because
6
+ it means no amount of editing this file can accidentally expose a value through
7
+ the CRUD endpoints.
8
+
9
+ The rest, in descending order of strength:
10
+
11
+ * ``fields`` is enumerated, never ``__all__``, so a new model field cannot
12
+ appear in the API by accident.
13
+ * ``data``/``clear``/``merge`` are ``write_only``, which DRF enforces
14
+ structurally: ``to_representation`` iterates ``_readable_fields``, which
15
+ excludes them. There is no code path that includes them.
16
+ * A defensive strip in ``to_representation``, which a test asserts is dead code.
17
+ """
18
+
19
+ from core.models import ObjectType
20
+ from netbox.api.fields import ContentTypeField
21
+ from netbox.api.gfk_fields import GFKSerializerField
22
+ from netbox.api.serializers import NetBoxModelSerializer
23
+ from rest_framework import serializers
24
+
25
+ from ..constants import WRITE_ONLY_FIELDS
26
+ from ..models import Secret, SecretType
27
+
28
+
29
+ class SecretTypeSerializer(NetBoxModelSerializer):
30
+ url = serializers.HyperlinkedIdentityField(view_name='plugins-api:netbox_secrets_manager-api:secrettype-detail')
31
+ secret_count = serializers.IntegerField(read_only=True)
32
+
33
+ class Meta:
34
+ model = SecretType
35
+ fields = (
36
+ 'id',
37
+ 'url',
38
+ 'display_url',
39
+ 'display',
40
+ 'name',
41
+ 'slug',
42
+ 'description',
43
+ 'schema',
44
+ 'name_template',
45
+ 'kms_key_id',
46
+ 'secret_count',
47
+ 'comments',
48
+ 'tags',
49
+ 'custom_fields',
50
+ 'created',
51
+ 'last_updated',
52
+ )
53
+ brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description')
54
+
55
+
56
+ class SecretSerializer(NetBoxModelSerializer):
57
+ url = serializers.HyperlinkedIdentityField(view_name='plugins-api:netbox_secrets_manager-api:secret-detail')
58
+ object_type = ContentTypeField(queryset=ObjectType.objects.all())
59
+ assigned_object = GFKSerializerField(read_only=True)
60
+ secret_type = SecretTypeSerializer(nested=True)
61
+
62
+ # Declared explicitly because editable=False keeps ModelSerializer from
63
+ # generating them at all, and read_only so they can never be written: the
64
+ # ARN is the plugin's own bookkeeping, not user input.
65
+ arn = serializers.CharField(read_only=True)
66
+ aws_secret_name = serializers.CharField(read_only=True)
67
+ version_id = serializers.CharField(read_only=True)
68
+ has_previous_version = serializers.BooleanField(read_only=True)
69
+ last_written = serializers.DateTimeField(read_only=True)
70
+
71
+ # The write path. write_only means DRF never includes these in output.
72
+ data = serializers.DictField(
73
+ write_only=True,
74
+ required=False,
75
+ help_text='Field values to write. On PATCH, fields omitted here keep their current value.',
76
+ )
77
+ clear = serializers.ListField(
78
+ child=serializers.CharField(),
79
+ write_only=True,
80
+ required=False,
81
+ help_text='Field names to set to an empty value. Distinct from omitting them, which leaves them alone.',
82
+ )
83
+ merge = serializers.BooleanField(
84
+ write_only=True,
85
+ default=True,
86
+ help_text='When false, the submitted data replaces the stored value wholesale and must satisfy '
87
+ 'every required field.',
88
+ )
89
+
90
+ class Meta:
91
+ model = Secret
92
+ fields = (
93
+ 'id',
94
+ 'url',
95
+ 'display_url',
96
+ 'display',
97
+ 'object_type',
98
+ 'object_id',
99
+ 'assigned_object',
100
+ 'secret_type',
101
+ 'name',
102
+ 'arn',
103
+ 'aws_secret_name',
104
+ 'version_id',
105
+ 'has_previous_version',
106
+ 'last_written',
107
+ 'data',
108
+ 'clear',
109
+ 'merge',
110
+ 'description',
111
+ 'comments',
112
+ 'tags',
113
+ 'custom_fields',
114
+ 'created',
115
+ 'last_updated',
116
+ )
117
+ brief_fields = ('id', 'url', 'display', 'name', 'secret_type')
118
+
119
+ def validate(self, data):
120
+ """Keep the write-only keys away from the model constructor.
121
+
122
+ NetBox's serializer base instantiates ``Meta.model(**attrs)`` to run
123
+ model-level validation. ``data``/``clear``/``merge`` are serializer
124
+ inputs, not model fields, so they have to be lifted out first and put
125
+ back afterwards for create()/update() to consume.
126
+ """
127
+ carried = {key: data.pop(key) for key in list(WRITE_ONLY_FIELDS) if key in data}
128
+ data = super().validate(data)
129
+ data.update(carried)
130
+ return data
131
+
132
+ def to_representation(self, instance):
133
+ representation = super().to_representation(instance)
134
+ # Belt and braces. write_only already excludes these; if this loop ever
135
+ # removes anything, DRF's contract has changed underneath us and
136
+ # test_no_plaintext will say so.
137
+ for key in WRITE_ONLY_FIELDS & representation.keys():
138
+ del representation[key]
139
+ return representation
140
+
141
+ def create(self, validated_data):
142
+ from .. import services
143
+
144
+ data = validated_data.pop('data', None)
145
+ validated_data.pop('clear', None)
146
+ validated_data.pop('merge', None)
147
+
148
+ if not data:
149
+ raise serializers.ValidationError({'data': 'This field is required when creating a secret.'})
150
+
151
+ tags = validated_data.pop('tags', None)
152
+ custom_fields = validated_data.pop('custom_field_data', None)
153
+
154
+ secret = Secret(**validated_data)
155
+ if custom_fields is not None:
156
+ secret.custom_field_data = custom_fields
157
+
158
+ secret = services.create_secret(secret, data, user=self._user(), request=self._request())
159
+ if tags is not None:
160
+ secret.tags.set(tags)
161
+ return secret
162
+
163
+ def update(self, instance, validated_data):
164
+ from .. import services
165
+
166
+ data = validated_data.pop('data', {})
167
+ clear = set(validated_data.pop('clear', ()) or ())
168
+ merge = validated_data.pop('merge', True)
169
+ tags = validated_data.pop('tags', None)
170
+
171
+ for key, value in validated_data.items():
172
+ setattr(instance, key, value)
173
+ # Runs the set-once check on object_type/object_id/secret_type.
174
+ instance.full_clean(exclude=('arn', 'aws_secret_name', 'version_id'))
175
+ instance.save()
176
+
177
+ if tags is not None:
178
+ instance.tags.set(tags)
179
+
180
+ if data or clear:
181
+ instance = services.update_secret(
182
+ instance, data, clear=clear, merge=merge, user=self._user(), request=self._request()
183
+ )
184
+ return instance
185
+
186
+ def _request(self):
187
+ return self.context.get('request')
188
+
189
+ def _user(self):
190
+ request = self._request()
191
+ return getattr(request, 'user', None) if request else None
@@ -0,0 +1,12 @@
1
+ """API routing. Mounts at /api/plugins/secrets-manager/."""
2
+
3
+ from netbox.api.routers import NetBoxRouter
4
+
5
+ from . import views
6
+
7
+ router = NetBoxRouter()
8
+ router.register('secrets', views.SecretViewSet)
9
+ router.register('secret-types', views.SecretTypeViewSet)
10
+
11
+ app_name = 'netbox_secrets_manager-api'
12
+ urlpatterns = router.urls