views.py 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. # coding: utf-8
  2. from __future__ import print_function
  3. from __future__ import absolute_import
  4. from __future__ import division
  5. from __future__ import unicode_literals
  6. from django.shortcuts import render
  7. from django.views.generic import TemplateView, View
  8. from django.views.generic.edit import FormView
  9. from django.core import signing
  10. from django.core.urlresolvers import reverse
  11. from django import forms
  12. from django.core.exceptions import PermissionDenied
  13. from backend.mixins import VisitorMixin
  14. import backend.models as bmodels
  15. from backend import const
  16. def is_valid_username(username):
  17. if username.endswith("@users.alioth.debian.org"): return True
  18. if username.endswith("@debian.org"): return True
  19. return False
  20. FORMER_ACTIVE = (const.STATUS_EMERITUS_DD, const.STATUS_EMERITUS_DM,
  21. const.STATUS_REMOVED_DD, const.STATUS_REMOVED_DM,
  22. const.STATUS_REMOVED_DC_GA)
  23. class ClaimForm(forms.Form):
  24. fpr = forms.CharField(label="Fingerprint", min_length=40, widget=forms.TextInput(attrs={"size": 60}))
  25. def clean_fpr(self):
  26. data = bmodels.FingerprintField.clean_fingerprint(self.cleaned_data['fpr'])
  27. try:
  28. fpr = bmodels.Fingerprint.objects.get(fpr=self.cleaned_data["fpr"])
  29. except bmodels.Fingerprint.DoesNotExist:
  30. raise forms.ValidationError("The GPG fingerprint is not known to this system. "
  31. "If you are a Debian Maintainer, and you entered the fingerprint that is in the DM keyring, "
  32. "please contact Front Desk to get this fixed.")
  33. if not fpr.is_active:
  34. raise forms.ValidationError("The GPG fingerprint corresponds to a key that is not currently the active key of the user.")
  35. if fpr.person.status not in FORMER_ACTIVE and is_valid_username(fpr.person.username):
  36. raise forms.ValidationError("The GPG fingerprint corresponds to a person that has a valid Single Sign-On username.")
  37. return data
  38. class Claim(VisitorMixin, FormView):
  39. """
  40. Validate and send an encrypted HMAC url to associate an alioth account with
  41. a DM key
  42. """
  43. template_name = "dm/claim.html"
  44. form_class = ClaimForm
  45. def pre_dispatch(self):
  46. super(Claim, self).pre_dispatch()
  47. if self.visitor is not None and not self.visitor.is_admin: raise PermissionDenied
  48. if self.request.sso_username is None: raise PermissionDenied
  49. if not is_valid_username(self.request.sso_username): raise PermissionDenied
  50. self.username = self.request.sso_username
  51. def get_context_data(self, fpr=None, **kw):
  52. ctx = super(Claim, self).get_context_data(**kw)
  53. ctx["username"] = self.username
  54. if fpr:
  55. ctx["fpr"] = fpr
  56. ctx["person"] = fpr.person
  57. key = fpr.get_key()
  58. if not key.key_is_fresh(): key.update_key()
  59. plaintext = self.request.build_absolute_uri(reverse("dm_claim_confirm", kwargs={
  60. "token": signing.dumps({
  61. "u": self.username,
  62. "f": fpr.fpr,
  63. })
  64. }))
  65. plaintext += "\n"
  66. # Add to context: it will not be rendered, but it can be picked up
  67. # by unit tests without the need to have access to the private key
  68. # to decode it
  69. ctx["plaintext"] = plaintext
  70. ctx["challenge"] = key.encrypt(plaintext.encode("utf8"))
  71. return ctx
  72. def form_valid(self, form):
  73. fpr = bmodels.Fingerprint.objects.get(fpr=form.cleaned_data["fpr"])
  74. return self.render_to_response(self.get_context_data(form=form, fpr=fpr))
  75. class ClaimConfirm(VisitorMixin, TemplateView):
  76. """
  77. Validate the claim confirmation links
  78. """
  79. template_name = "dm/claim_confirm.html"
  80. def pre_dispatch(self):
  81. super(ClaimConfirm, self).pre_dispatch()
  82. if self.request.sso_username is None: raise PermissionDenied
  83. if not is_valid_username(self.request.sso_username): raise PermissionDenied
  84. self.username = self.request.sso_username
  85. def validate_token(self, token):
  86. parsed = signing.loads(token)
  87. self.errors = []
  88. if self.visitor is not None:
  89. self.errors.append("Your SSO username is already associated with a person in the system")
  90. return False
  91. # Validate fingerprint
  92. try:
  93. self.fpr = bmodels.Fingerprint.objects.get(fpr=parsed["f"])
  94. except bmodels.Fingerprint.DoesNotExist:
  95. self.fpr = None
  96. self.errors.append("The GPG fingerprint is not known to this system")
  97. return False
  98. if not self.fpr.is_active:
  99. self.errors.append("The GPG fingerprint corresponds to a key that is not currently the active key of the user.")
  100. if self.fpr.person.status not in FORMER_ACTIVE and is_valid_username(self.fpr.person.username):
  101. self.errors.append("The GPG fingerprint corresponds to a person that has a valid Single Sign-On username.")
  102. if self.fpr.person.is_dd:
  103. self.errors.append("The GPG fingerprint corresponds to a Debian Developer.")
  104. # Validate username
  105. if self.username != parsed["u"]:
  106. self.errors.append("The token was not generated by you")
  107. try:
  108. existing_person = bmodels.Person.objects.get(username=self.username)
  109. except bmodels.Person.DoesNotExist:
  110. existing_person = None
  111. if existing_person is not None:
  112. self.errors.append("The SSO username is already associated with a different person in the system")
  113. return not self.errors
  114. def get_context_data(self, **kw):
  115. ctx = super(ClaimConfirm, self).get_context_data(**kw)
  116. if self.validate_token(self.kwargs["token"]):
  117. # Do the mapping
  118. self.fpr.person.username = self.username
  119. self.fpr.person.save(audit_author=self.fpr.person, audit_notes="claimed account via /dm/claim")
  120. ctx["mapped"] = True
  121. ctx["person"] = self.fpr.person
  122. ctx["fpr"] = self.fpr
  123. ctx["username"] = self.username
  124. ctx["errors"] = self.errors
  125. return ctx