diff --git a/backend/models.py b/backend/models.py index 8b189d931e8b626a89b1485f4bfa24351c88bb44..31bf28ff7d91f0653800e6e768c363961c0dcf3d 100644 --- a/backend/models.py +++ b/backend/models.py @@ -770,6 +770,16 @@ class Person(PermissionsMixin, models.Model): raise Http404 +class FingerprintManager(BaseUserManager): + def create_user(self, **fields): + audit_author = fields.pop("audit_author", None) + audit_notes = fields.pop("audit_notes", None) + audit_skip = fields.pop("audit_skip", False) + res = self.model(**fields) + res.save(using=self._db, audit_author=audit_author, audit_notes=audit_notes, audit_skip=audit_skip) + return res + + class Fingerprint(models.Model): """ A fingerprint for a person @@ -777,10 +787,51 @@ class Fingerprint(models.Model): class Meta: db_table = "fingerprints" + objects = FingerprintManager() + user = models.ForeignKey(Person, related_name="fprs") fpr = FingerprintField(verbose_name="OpenPGP key fingerprint", max_length=40, unique=True) is_active = models.BooleanField(default=False, help_text="whether this key is curently in use") + def save(self, *args, **kw): + """ + Save, and add an entry to the Person audit log. + + Extra arguments that can be passed: + + audit_author: Person instance of the person doing the change + audit_notes: free form text annotations for this change + audit_skip: skip audit logging, used only for tests + + """ + # Extract our own arguments, so that they are not passed to django + author = kw.pop("audit_author", None) + notes = kw.pop("audit_notes", "") + audit_skip = kw.pop("audit_skip", False) + + if audit_skip: + changes = None + else: + # Get the previous version of the Fingerprint object, so that + # PersonAuditLog can compute differences + if self.pk: + existing_fingerprint = Fingerprint.objects.get(pk=self.pk) + else: + existing_fingerprint = None + + changes = PersonAuditLog.diff_fingerprint(existing_fingerprint, self) + if changes and not author: + raise RuntimeError("Cannot save a Person instance without providing Author information") + + # Perform the save; if we are creating a new person, this will also + # fill in the id/pk field, so that PersonAuditLog can link to us + super(Fingerprint, self).save(*args, **kw) + + # Finally, create the audit log entry + if changes: + if existing_fingerprint is not None and existing_fingerprint.user.pk != self.user.pk: + PersonAuditLog.objects.create(person=existing_fingerprint.user, author=author, notes=notes, changes=PersonAuditLog.serialize_changes(changes)) + PersonAuditLog.objects.create(person=self.user, author=author, notes=notes, changes=PersonAuditLog.serialize_changes(changes)) class PersonAuditLog(models.Model): person = models.ForeignKey(Person, related_name="audit_log") @@ -809,6 +860,26 @@ class PersonAuditLog(models.Model): changes[k] = [ov, nv] return changes + @classmethod + def diff_fingerprint(cls, existing_fpr, new_fpr): + """ + Compute the changes between two different instances of a Fingerprint model + """ + exclude = [] + changes = {} + if existing_fpr is None: + for k, nv in model_to_dict(new_fpr, exclude=exclude).items(): + changes["fpr:{}:{}".format(new_fpr.fpr, k)] = [None, nv] + else: + old = model_to_dict(existing_fpr, exclude=exclude) + new = model_to_dict(new_fpr, exclude=exclude) + for k, nv in new.items(): + ov = old.get(k, None) + # Also ignore changes like None -> "" + if ov != nv and (ov or nv): + changes["fpr:{}:{}".format(old.fpr, k)] = [ov, nv] + return changes + @classmethod def serialize_changes(cls, changes): class Serializer(json.JSONEncoder): diff --git a/public/views.py b/public/views.py index ce8f8ea3c239d38454e9dc9259466388e08e30e9..fe0969944e0d7becf0ee257c81d34a5a16c46df3 100644 --- a/public/views.py +++ b/public/views.py @@ -649,7 +649,7 @@ class Findperson(VisitorMixin, FormView): person.save(audit_author=self.visitor, audit_notes="user created manually") fpr = form.cleaned_data["fpr"] if fpr: - bmodels.Fingerprint.objects.create(fpr=fpr, user=person) + bmodels.Fingerprint.objects.create(fpr=fpr, user=person, is_active=True, audit_author=self.visitor, audit_notes="user created manually") return redirect(person.get_absolute_url()) @@ -833,6 +833,9 @@ class Newnm(VisitorMixin, FormView): person.status_changed = now() person.make_pending(days_valid=self.DAYS_VALID) person.save(audit_author=person, audit_notes="new subscription to the site") + fpr = form.cleaned_data["fpr"] + bmodels.Fingerprint.objects.create(user=person, fpr=fpr, is_active=True, audit_author=person, audit_notes="new subscription to the site") + # Redirect to the send challenge page return redirect("public_newnm_resend_challenge", key=person.lookup_key)