Skip to content

Commit 2bd8ec1

Browse files
committed
apps/secrets/services/revision.py: only preserve OTP fields if they existed previously
avoid adding explicit nulls, those would change the hash and create duplicate revisions for unchanged secrets
1 parent a84fcea commit 2bd8ec1

2 files changed

Lines changed: 45 additions & 3 deletions

File tree

teamvault/apps/secrets/services/revision.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -202,9 +202,15 @@ def _build_revision(
202202
# merge missing fields for PASSWORD type
203203
if content_type == ContentType.PASSWORD and secret.current_revision:
204204
prev = secret.current_revision.peek_data(actor)
205-
payload.setdefault('password', prev.get('password'))
206-
for fld in ('otp_key', 'digits', 'algorithm'):
207-
payload.setdefault(fld, prev.get(fld))
205+
if 'password' not in payload and 'password' in prev:
206+
payload['password'] = prev['password']
207+
208+
# Preserve OTP fields only when the previous revision actually had them.
209+
if 'otp_key' not in payload and 'otp_key' in prev:
210+
payload['otp_key'] = prev['otp_key']
211+
for fld in ('digits', 'algorithm'):
212+
if fld in prev:
213+
payload[fld] = prev[fld]
208214

209215
sha_src = dumps(payload, sort_keys=True)
210216
sha_sum = sha256(sha_src.encode()).hexdigest()

teamvault/apps/secrets/tests/models/test_secret_crud.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,42 @@ def test_update_file_payload_roundtrip(self):
150150
self.assertIsInstance(got, (bytes, bytearray))
151151
self.assertEqual(got, raw)
152152

153+
def test_otp_only_update_creates_new_revision(self):
154+
s: Secret = new_secret(self.owner, name="otp-add")
155+
rev_before = s.current_revision
156+
self.assertFalse(rev_before.otp_key_set)
157+
158+
changes_before = SecretChange.objects.filter(secret=s).count()
159+
160+
# OTP-only update: omit password, keep existing via merge logic.
161+
RevisionService.save_payload(
162+
secret=s,
163+
actor=self.owner,
164+
payload={
165+
"otp_key": "JBSWY3DPEHPK3PXP",
166+
"digits": "6",
167+
"algorithm": "SHA1",
168+
},
169+
)
170+
171+
s.refresh_from_db()
172+
rev_after = s.current_revision
173+
self.assertNotEqual(rev_after.id, rev_before.id)
174+
self.assertTrue(rev_after.otp_key_set)
175+
self.assertEqual(rev_after.get_data(self.owner)["otp_key"], "JBSWY3DPEHPK3PXP")
176+
177+
# Re-saving identical OTP-only payload should be a no-op.
178+
RevisionService.save_payload(
179+
secret=s,
180+
actor=self.owner,
181+
payload={
182+
"otp_key": "JBSWY3DPEHPK3PXP",
183+
"digits": "6",
184+
"algorithm": "SHA1",
185+
},
186+
)
187+
self.assertEqual(SecretChange.objects.filter(secret=s).count(), changes_before + 1)
188+
153189
def test_delete_marks_secret_and_denies_visibility_and_read(self):
154190
s: Secret = new_secret(self.owner, name="todelete", access_policy=AccessPolicy.ANY)
155191
s.status = SecretStatus.DELETED

0 commit comments

Comments
 (0)