netbox-passwork 1.3.1__py3-none-any.whl

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 (35) hide show
  1. netbox_passwork/__init__.py +13 -0
  2. netbox_passwork/admin.py +71 -0
  3. netbox_passwork/api/__init__.py +0 -0
  4. netbox_passwork/api/serializers.py +11 -0
  5. netbox_passwork/config.py +29 -0
  6. netbox_passwork/exceptions.py +54 -0
  7. netbox_passwork/gateway.py +156 -0
  8. netbox_passwork/migrations/0001_initial.py +141 -0
  9. netbox_passwork/migrations/0002_alter_ids.py +25 -0
  10. netbox_passwork/migrations/0003_changelog.py +80 -0
  11. netbox_passwork/migrations/__init__.py +0 -0
  12. netbox_passwork/models.py +111 -0
  13. netbox_passwork/passwork_client.py +263 -0
  14. netbox_passwork/permissions.py +68 -0
  15. netbox_passwork/serializers.py +54 -0
  16. netbox_passwork/static/netbox_passwork/passwork.js +845 -0
  17. netbox_passwork/static/netbox_passwork/styles.css +0 -0
  18. netbox_passwork/template_extensions.py +59 -0
  19. netbox_passwork/templates/netbox_passwork/device_passwork.html +6 -0
  20. netbox_passwork/templates/netbox_passwork/login_modal.html +30 -0
  21. netbox_passwork/templates/netbox_passwork/picker_modal.html +47 -0
  22. netbox_passwork/templates/netbox_passwork/secret_detail.html +0 -0
  23. netbox_passwork/templates/netbox_passwork/secret_row.html +2 -0
  24. netbox_passwork/templates/netbox_passwork/secrets_tab.html +59 -0
  25. netbox_passwork/templates/netbox_passwork/service_passwork.html +6 -0
  26. netbox_passwork/templates/netbox_passwork/totp_modal.html +36 -0
  27. netbox_passwork/templates/netbox_passwork/vm_passwork.html +6 -0
  28. netbox_passwork/urls.py +21 -0
  29. netbox_passwork/utils.py +9 -0
  30. netbox_passwork/views.py +426 -0
  31. netbox_passwork-1.3.1.dist-info/METADATA +271 -0
  32. netbox_passwork-1.3.1.dist-info/RECORD +35 -0
  33. netbox_passwork-1.3.1.dist-info/WHEEL +5 -0
  34. netbox_passwork-1.3.1.dist-info/licenses/LICENSE +202 -0
  35. netbox_passwork-1.3.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,13 @@
1
+ from importlib.metadata import PackageNotFoundError, version
2
+
3
+ from .config import config
4
+
5
+ try:
6
+ __version__ = version("netbox-passwork")
7
+ except PackageNotFoundError:
8
+ __version__ = "dev"
9
+
10
+
11
+ __all__ = [
12
+ "config",
13
+ ]
@@ -0,0 +1,71 @@
1
+ from django.contrib import admin
2
+
3
+ from netbox_passwork.models import PassworkAuditLog, PassworkBinding
4
+
5
+
6
+ @admin.register(PassworkBinding)
7
+ class PassworkBindingAdmin(admin.ModelAdmin):
8
+ list_display = [
9
+ "id",
10
+ "object_type",
11
+ "object_id",
12
+ "passwork_item_id",
13
+ "created",
14
+ "created_by",
15
+ ]
16
+ list_filter = ["object_type"]
17
+ search_fields = ["passwork_item_id", "object_id"]
18
+ readonly_fields = [
19
+ "id",
20
+ "object_type",
21
+ "object_id",
22
+ "passwork_item_id",
23
+ "created",
24
+ "created_by",
25
+ ]
26
+ ordering = ["-created"]
27
+
28
+ def has_add_permission(self, request):
29
+ return False
30
+
31
+ def has_change_permission(self, request, obj=None):
32
+ return False
33
+
34
+ def has_delete_permission(self, request, obj=None):
35
+ return False
36
+
37
+
38
+ @admin.register(PassworkAuditLog)
39
+ class PassworkAuditLogAdmin(admin.ModelAdmin):
40
+ list_display = [
41
+ "id",
42
+ "timestamp",
43
+ "netbox_user",
44
+ "passwork_item_id",
45
+ "object_type",
46
+ "object_id",
47
+ "action",
48
+ "ip_address",
49
+ ]
50
+ list_filter = ["action", "object_type"]
51
+ search_fields = ["passwork_item_id", "netbox_user__username", "object_id"]
52
+ readonly_fields = [
53
+ "id",
54
+ "timestamp",
55
+ "netbox_user",
56
+ "passwork_item_id",
57
+ "object_type",
58
+ "object_id",
59
+ "action",
60
+ "ip_address",
61
+ ]
62
+ ordering = ["-timestamp"]
63
+
64
+ def has_add_permission(self, request):
65
+ return False
66
+
67
+ def has_change_permission(self, request, obj=None):
68
+ return False
69
+
70
+ def has_delete_permission(self, request, obj=None):
71
+ return False
File without changes
@@ -0,0 +1,11 @@
1
+ """API serializers following NetBox convention (<plugin>.api.serializers).
2
+
3
+ The NetBox events pipeline (webhooks/event rules, also triggered on changelog
4
+ operations) serializes the object via
5
+ utilities.api.get_serializer_for_model(), which looks up the serializer
6
+ specifically in this module by the name <Model>Serializer.
7
+ """
8
+
9
+ from netbox_passwork.serializers import PassworkBindingSerializer
10
+
11
+ __all__ = ("PassworkBindingSerializer",)
@@ -0,0 +1,29 @@
1
+ from importlib.metadata import version as _pkg_version
2
+
3
+ from netbox.plugins import PluginConfig
4
+
5
+
6
+ class NetboxPassworkConfig(PluginConfig):
7
+ name = "netbox_passwork"
8
+ verbose_name = "NetBox Passwork Integration"
9
+ version = _pkg_version("netbox-passwork")
10
+ author = "Pavel Krasotin"
11
+ author_email = "krasotinpa@gmail.com"
12
+ description = "Passwork secrets integration for NetBox devices, VMs and services"
13
+ base_url = "passwork"
14
+ required_settings = ["PASSWORK_URL", "SESSION_ENCRYPT_KEY"]
15
+ default_settings = {
16
+ "PASSWORK_VERIFY_SSL": True,
17
+ "TOKEN_REFRESH_MARGIN": 60,
18
+ "PASSWORK_REQUEST_TIMEOUT": 5,
19
+ "SECRET_REVEAL_TIMEOUT": 30,
20
+ }
21
+ min_version = "4.5"
22
+
23
+ def ready(self):
24
+ from netbox_passwork.template_extensions import _register_views
25
+
26
+ _register_views()
27
+
28
+
29
+ config = NetboxPassworkConfig
@@ -0,0 +1,54 @@
1
+ class PassworkError(Exception):
2
+ """
3
+ Base exception of the Passwork gateway: a Passwork failure carrying HTTP meaning.
4
+
5
+ ``code`` and ``http_status`` are what goes into the plugin's JSON response; ``detail``
6
+ is a human-readable explanation. Subclasses set the defaults, and an operation that
7
+ knows the failure context (login, TOTP, reading a secret) may override them when
8
+ raising (ADR-0001).
9
+ """
10
+
11
+ code = "pw_error"
12
+ http_status = 502
13
+ detail = "Passwork error"
14
+
15
+ def __init__(self, detail: str | None = None, *, code: str | None = None, http_status: int | None = None):
16
+ if detail is not None:
17
+ self.detail = detail
18
+ if code is not None:
19
+ self.code = code
20
+ if http_status is not None:
21
+ self.http_status = http_status
22
+ super().__init__(self.detail)
23
+
24
+
25
+ class PassworkSessionExpired(PassworkError):
26
+ """Refresh token expired or was rejected — a new login is required."""
27
+
28
+ code = "pw_session_expired"
29
+ http_status = 401
30
+ detail = "Passwork session expired"
31
+
32
+
33
+ class PassworkAccessDenied(PassworkError):
34
+ """Passwork denied access (403 for a secret; invalid credentials on login/TOTP)."""
35
+
36
+ code = "pw_access_denied"
37
+ http_status = 403
38
+ detail = "Passwork access denied"
39
+
40
+
41
+ class PassworkTimeout(PassworkError):
42
+ """Passwork did not respond within PASSWORK_REQUEST_TIMEOUT seconds."""
43
+
44
+ code = "pw_timeout"
45
+ http_status = 504
46
+ detail = "Passwork timeout"
47
+
48
+
49
+ class PassworkBadResponse(PassworkError):
50
+ """Passwork returned a response that isn't JSON (e.g. a proxy error HTML page)."""
51
+
52
+ code = "pw_bad_response"
53
+ http_status = 502
54
+ detail = "Passwork returned a non-JSON response"
@@ -0,0 +1,156 @@
1
+ """
2
+ Passwork gateway — the single point through which the plugin talks to Passwork on
3
+ behalf of the Passwork session (see docs/adr/0001-passwork-gateway-not-middleware.md).
4
+
5
+ The module owns everything related to the Passwork session: it is the only place that
6
+ reads the plugin config, the only place that Fernet-encrypts its fields, reads/writes/
7
+ deletes its record in storage (in production, the request's Django session), refreshes
8
+ the access token, and provides six domain operations. Passwork failures are raised as
9
+ ``PassworkError`` with ``code``/``http_status`` — the failure context is known to the operation.
10
+ """
11
+
12
+ import logging
13
+
14
+ from cryptography.fernet import Fernet, InvalidToken
15
+ from django.conf import settings
16
+
17
+ from netbox_passwork.exceptions import PassworkError, PassworkSessionExpired
18
+ from netbox_passwork.passwork_client import PassworkAuthClient
19
+
20
+ logger = logging.getLogger("netbox_passwork")
21
+
22
+ # The storage key and record shape are byte-for-byte compatible with pre-1.3.0
23
+ # versions: existing users don't get logged out on upgrade.
24
+ _STORAGE_KEY = "pw_session"
25
+ _ENCRYPTED_FIELDS = ("access_token", "refresh_token", "csrf_token")
26
+
27
+
28
+ def _not_authenticated(detail: str) -> PassworkError:
29
+ return PassworkError(detail, code="pw_not_authenticated", http_status=401)
30
+
31
+
32
+ class PassworkGateway:
33
+ """
34
+ Six operations on behalf of the Passwork session, over storage with a ``dict`` interface.
35
+
36
+ ``storage`` — where the Passwork session record lives (``request.session`` in
37
+ production, a plain ``dict`` in tests); ``client`` — the HTTP implementation of the
38
+ Passwork API; ``config`` — the plugin config (``PLUGINS_CONFIG["netbox_passwork"]``),
39
+ from which the gateway takes ``SESSION_ENCRYPT_KEY``.
40
+ """
41
+
42
+ def __init__(self, storage, client: PassworkAuthClient, config):
43
+ self._storage = storage
44
+ self._client = client
45
+ key = config["SESSION_ENCRYPT_KEY"]
46
+ self._fernet = Fernet(key.encode() if isinstance(key, str) else key)
47
+ # The gateway lives for a single HTTP request: the Passwork session that's been
48
+ # read (and refreshed if needed) is reused by all operations of the request —
49
+ # without re-decrypting the record
50
+ self._passwork_session: dict | None = None
51
+
52
+ # ------------------------------------------------------------------
53
+ # Operations
54
+ # ------------------------------------------------------------------
55
+
56
+ def login(self, username: str, password: str) -> bool:
57
+ """Log in to Passwork; saves the Passwork session. Returns whether TOTP is required."""
58
+ passwork_session = self._client.login(username, password)
59
+ self._write(passwork_session)
60
+ return bool(passwork_session["requires_totp"])
61
+
62
+ def confirm_totp(self, code: str) -> None:
63
+ """Confirms TOTP for the saved Passwork session."""
64
+ passwork_session = self._client.confirm_totp(code, self._load())
65
+ self._write(passwork_session)
66
+
67
+ def get_item(self, pw_id: str) -> dict:
68
+ """Passwork item with the password and custom fields decoded."""
69
+ return self._client.get_item(pw_id, self._load())
70
+
71
+ def list_vaults(self) -> list:
72
+ """List of Passwork vaults."""
73
+ return self._client.list_vaults(self._load())
74
+
75
+ def search_items(self, query: str) -> list:
76
+ """Search Passwork items (the query is URL-encoded by the client)."""
77
+ return self._client.search_items(query, self._load())
78
+
79
+ def require_session(self) -> None:
80
+ """Guarantees the Passwork session exists and hasn't expired (refreshing it if needed)."""
81
+ self._load()
82
+
83
+ # ------------------------------------------------------------------
84
+ # Passwork session in storage
85
+ # ------------------------------------------------------------------
86
+
87
+ def _load(self) -> dict:
88
+ """
89
+ Reads and, if needed, refreshes the Passwork session.
90
+
91
+ Known failures: no record → 401 ``pw_not_authenticated``; refresh token expired
92
+ or rejected → record is deleted, 401 ``pw_session_expired``; ``InvalidToken``
93
+ (``SESSION_ENCRYPT_KEY`` changed) → record is deleted, warning, 401
94
+ ``pw_not_authenticated``; timeout/non-JSON during refresh → 504/502.
95
+ Other exceptions are not swallowed. The result is cached for the gateway's lifetime.
96
+ """
97
+ if self._passwork_session is not None:
98
+ return self._passwork_session
99
+ record = self._storage.get(_STORAGE_KEY)
100
+ if record is None:
101
+ raise _not_authenticated("Passwork session not found")
102
+ try:
103
+ passwork_session = self._decrypt(record)
104
+ except InvalidToken:
105
+ self._delete()
106
+ logger.warning(
107
+ "Passwork session record could not be decrypted (SESSION_ENCRYPT_KEY changed?); record dropped"
108
+ )
109
+ raise _not_authenticated("Passwork session could not be decrypted") from None
110
+
111
+ before = dict(passwork_session)
112
+ try:
113
+ passwork_session = self._client.refresh_if_needed(passwork_session)
114
+ except PassworkSessionExpired:
115
+ self._delete()
116
+ raise
117
+ # Rewrite the record only if the refresh changed it — avoids unnecessary session saves
118
+ if passwork_session != before:
119
+ self._write(passwork_session)
120
+ self._passwork_session = passwork_session
121
+ return passwork_session
122
+
123
+ def _write(self, passwork_session: dict) -> None:
124
+ # Assigning the key by itself marks the Django session as modified
125
+ self._storage[_STORAGE_KEY] = self._encrypt(passwork_session)
126
+ self._passwork_session = passwork_session
127
+
128
+ def _delete(self) -> None:
129
+ self._storage.pop(_STORAGE_KEY, None)
130
+ self._passwork_session = None
131
+
132
+ def _encrypt(self, passwork_session: dict) -> dict:
133
+ return {
134
+ k: self._fernet.encrypt(str(v).encode()).decode() if k in _ENCRYPTED_FIELDS else v
135
+ for k, v in passwork_session.items()
136
+ }
137
+
138
+ def _decrypt(self, record: dict) -> dict:
139
+ return {
140
+ k: self._fernet.decrypt(v.encode()).decode() if k in _ENCRYPTED_FIELDS else v for k, v in record.items()
141
+ }
142
+
143
+
144
+ def build_gateway(request) -> PassworkGateway:
145
+ """
146
+ Builds the gateway from an HTTP request — the single point of composition and the
147
+ only place that reads the plugin config.
148
+ """
149
+ config = settings.PLUGINS_CONFIG["netbox_passwork"]
150
+ client = PassworkAuthClient(
151
+ base_url=config["PASSWORK_URL"],
152
+ verify_ssl=config.get("PASSWORK_VERIFY_SSL", True),
153
+ timeout=config.get("PASSWORK_REQUEST_TIMEOUT", 5),
154
+ refresh_margin=config.get("TOKEN_REFRESH_MARGIN", 60),
155
+ )
156
+ return PassworkGateway(request.session, client, config)
@@ -0,0 +1,141 @@
1
+ import django.db.models.deletion
2
+ import django.db.models.expressions
3
+ from django.conf import settings
4
+ from django.db import migrations, models
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+ initial = True
9
+
10
+ dependencies = [
11
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
12
+ ]
13
+
14
+ operations = [
15
+ migrations.CreateModel(
16
+ name="PassworkBinding",
17
+ fields=[
18
+ ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False)),
19
+ (
20
+ "object_type",
21
+ models.CharField(
22
+ choices=[("device", "Device"), ("vm", "Virtual Machine"), ("service", "Service")],
23
+ max_length=32,
24
+ ),
25
+ ),
26
+ ("object_id", models.PositiveIntegerField()),
27
+ ("passwork_item_id", models.CharField(db_index=True, max_length=128)),
28
+ ("created_at", models.DateTimeField(auto_now_add=True)),
29
+ ("deleted_at", models.DateTimeField(blank=True, null=True)),
30
+ (
31
+ "created_by",
32
+ models.ForeignKey(
33
+ null=True,
34
+ on_delete=django.db.models.deletion.SET_NULL,
35
+ related_name="+",
36
+ to=settings.AUTH_USER_MODEL,
37
+ ),
38
+ ),
39
+ (
40
+ "deleted_by",
41
+ models.ForeignKey(
42
+ blank=True,
43
+ null=True,
44
+ on_delete=django.db.models.deletion.SET_NULL,
45
+ related_name="+",
46
+ to=settings.AUTH_USER_MODEL,
47
+ ),
48
+ ),
49
+ ],
50
+ options={
51
+ "permissions": [
52
+ ("view_secrets", "Can view Passwork secrets"),
53
+ ("reveal_secret", "Can reveal Passwork secret value"),
54
+ ("add_binding", "Can create Passwork binding"),
55
+ ("delete_binding", "Can delete Passwork binding"),
56
+ ("view_auditlog", "Can view Passwork audit log"),
57
+ ],
58
+ },
59
+ ),
60
+ migrations.CreateModel(
61
+ name="PassworkBindingHistory",
62
+ fields=[
63
+ ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False)),
64
+ ("timestamp", models.DateTimeField(auto_now_add=True)),
65
+ (
66
+ "action",
67
+ models.CharField(
68
+ choices=[("created", "Created"), ("deleted", "Deleted"), ("restored", "Restored")],
69
+ max_length=16,
70
+ ),
71
+ ),
72
+ ("ip_address", models.GenericIPAddressField(null=True)),
73
+ (
74
+ "binding",
75
+ models.ForeignKey(
76
+ on_delete=django.db.models.deletion.PROTECT,
77
+ related_name="history",
78
+ to="netbox_passwork.passworkbinding",
79
+ ),
80
+ ),
81
+ (
82
+ "netbox_user",
83
+ models.ForeignKey(
84
+ null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL
85
+ ),
86
+ ),
87
+ ],
88
+ options={
89
+ "ordering": ["-timestamp"],
90
+ },
91
+ ),
92
+ migrations.CreateModel(
93
+ name="PassworkAuditLog",
94
+ fields=[
95
+ ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False)),
96
+ ("timestamp", models.DateTimeField(auto_now_add=True)),
97
+ ("passwork_item_id", models.CharField(max_length=128)),
98
+ ("object_type", models.CharField(max_length=32)),
99
+ ("object_id", models.PositiveIntegerField()),
100
+ ("action", models.CharField(choices=[("reveal", "Reveal"), ("copy", "Copy")], max_length=16)),
101
+ ("ip_address", models.GenericIPAddressField(null=True)),
102
+ (
103
+ "netbox_user",
104
+ models.ForeignKey(
105
+ null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL
106
+ ),
107
+ ),
108
+ ],
109
+ options={
110
+ "ordering": ["-timestamp"],
111
+ },
112
+ ),
113
+ migrations.AddIndex(
114
+ model_name="passworkbinding",
115
+ index=models.Index(
116
+ condition=models.Q(deleted_at__isnull=True),
117
+ fields=["object_type", "object_id"],
118
+ name="pb_active_object_idx",
119
+ ),
120
+ ),
121
+ migrations.AddConstraint(
122
+ model_name="passworkbinding",
123
+ constraint=models.UniqueConstraint(
124
+ condition=models.Q(deleted_at__isnull=True),
125
+ fields=["object_type", "object_id", "passwork_item_id"],
126
+ name="pb_unique_active_binding",
127
+ ),
128
+ ),
129
+ migrations.AddIndex(
130
+ model_name="passworkauditlog",
131
+ index=models.Index(fields=["netbox_user"], name="pal_user_idx"),
132
+ ),
133
+ migrations.AddIndex(
134
+ model_name="passworkauditlog",
135
+ index=models.Index(fields=["passwork_item_id"], name="pal_item_idx"),
136
+ ),
137
+ migrations.AddIndex(
138
+ model_name="passworkauditlog",
139
+ index=models.Index(fields=["-timestamp"], name="pal_ts_idx"),
140
+ ),
141
+ ]
@@ -0,0 +1,25 @@
1
+ from django.db import migrations, models
2
+
3
+
4
+ class Migration(migrations.Migration):
5
+ dependencies = [
6
+ ("netbox_passwork", "0001_initial"),
7
+ ]
8
+
9
+ operations = [
10
+ migrations.AlterField(
11
+ model_name="passworkauditlog",
12
+ name="id",
13
+ field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
14
+ ),
15
+ migrations.AlterField(
16
+ model_name="passworkbinding",
17
+ name="id",
18
+ field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
19
+ ),
20
+ migrations.AlterField(
21
+ model_name="passworkbindinghistory",
22
+ name="id",
23
+ field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
24
+ ),
25
+ ]
@@ -0,0 +1,80 @@
1
+ """Move binding history over to the standard NetBox changelog.
2
+
3
+ - The PassworkBindingHistory table is dropped (accumulated records are
4
+ deliberately not migrated).
5
+ - Soft delete (deleted_at/deleted_by) is replaced with hard delete: soft-deleted
6
+ records are physically removed, the partial index/constraint become regular ones.
7
+ - PassworkBinding gains the created/last_updated fields from ChangeLoggedModel
8
+ (created is populated from the former created_at via RenameField).
9
+
10
+ WARNING: this migration is irreversible without a database backup — the history
11
+ table and soft-deleted bindings are removed permanently.
12
+ """
13
+
14
+ from django.conf import settings
15
+ from django.db import migrations, models
16
+
17
+
18
+ def purge_soft_deleted_bindings(apps, schema_editor):
19
+ # Raw SQL, not ORM .delete(): by this point DeleteModel has already dropped
20
+ # the PassworkBindingHistory table, but the PassworkBinding model still has
21
+ # a PROTECT back-reference to it. An ORM cascade (collector) on .delete()
22
+ # would hit the now-missing history table and fail with
23
+ # UndefinedTable. A direct DELETE bypasses the collector; history rows are
24
+ # already gone along with the table. The deleted_at column still exists
25
+ # at this point (RemoveField comes later).
26
+ schema_editor.execute('DELETE FROM "netbox_passwork_passworkbinding" WHERE "deleted_at" IS NOT NULL')
27
+
28
+
29
+ class Migration(migrations.Migration):
30
+ dependencies = [
31
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
32
+ ("netbox_passwork", "0002_alter_ids"),
33
+ ]
34
+
35
+ operations = [
36
+ # History table first (FK PROTECT on PassworkBinding), then the
37
+ # physical removal of soft-deleted bindings — before applying the
38
+ # unconditional unique constraint.
39
+ migrations.DeleteModel(name="PassworkBindingHistory"),
40
+ migrations.RunPython(purge_soft_deleted_bindings, migrations.RunPython.noop),
41
+ # The partial index/constraint reference deleted_at — drop them
42
+ # before removing the field itself.
43
+ migrations.RemoveConstraint(
44
+ model_name="passworkbinding",
45
+ name="pb_unique_active_binding",
46
+ ),
47
+ migrations.RemoveIndex(
48
+ model_name="passworkbinding",
49
+ name="pb_active_object_idx",
50
+ ),
51
+ migrations.RemoveField(model_name="passworkbinding", name="deleted_at"),
52
+ migrations.RemoveField(model_name="passworkbinding", name="deleted_by"),
53
+ # created_at -> created (ChangeLoggedModel field), values are preserved.
54
+ migrations.RenameField(
55
+ model_name="passworkbinding",
56
+ old_name="created_at",
57
+ new_name="created",
58
+ ),
59
+ migrations.AlterField(
60
+ model_name="passworkbinding",
61
+ name="created",
62
+ field=models.DateTimeField(auto_now_add=True, blank=True, null=True, verbose_name="created"),
63
+ ),
64
+ migrations.AddField(
65
+ model_name="passworkbinding",
66
+ name="last_updated",
67
+ field=models.DateTimeField(auto_now=True, blank=True, null=True, verbose_name="last updated"),
68
+ ),
69
+ migrations.AddIndex(
70
+ model_name="passworkbinding",
71
+ index=models.Index(fields=["object_type", "object_id"], name="pb_object_idx"),
72
+ ),
73
+ migrations.AddConstraint(
74
+ model_name="passworkbinding",
75
+ constraint=models.UniqueConstraint(
76
+ fields=["object_type", "object_id", "passwork_item_id"],
77
+ name="pb_unique_binding",
78
+ ),
79
+ ),
80
+ ]
File without changes