forms.py 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. from Crypto.Cipher import PKCS1_OAEP
  2. from Crypto.PublicKey import RSA
  3. from django import forms
  4. from django.utils.translation import gettext as _
  5. from dcim.models import Device
  6. from extras.forms import (
  7. AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldFilterForm, CustomFieldModelForm, CustomFieldModelCSVForm,
  8. )
  9. from extras.models import Tag
  10. from utilities.forms import (
  11. BootstrapMixin, CSVModelChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
  12. SlugField, TagFilterField,
  13. )
  14. from virtualization.models import VirtualMachine
  15. from .constants import *
  16. from .models import Secret, SecretRole, UserKey
  17. def validate_rsa_key(key, is_secret=True):
  18. """
  19. Validate the format and type of an RSA key.
  20. """
  21. if key.startswith('ssh-rsa '):
  22. raise forms.ValidationError("OpenSSH line format is not supported. Please ensure that your public is in PEM (base64) format.")
  23. try:
  24. key = RSA.importKey(key)
  25. except ValueError:
  26. raise forms.ValidationError("Invalid RSA key. Please ensure that your key is in PEM (base64) format.")
  27. except Exception as e:
  28. raise forms.ValidationError("Invalid key detected: {}".format(e))
  29. if is_secret and not key.has_private():
  30. raise forms.ValidationError("This looks like a public key. Please provide your private RSA key.")
  31. elif not is_secret and key.has_private():
  32. raise forms.ValidationError("This looks like a private key. Please provide your public RSA key.")
  33. try:
  34. PKCS1_OAEP.new(key)
  35. except Exception:
  36. raise forms.ValidationError("Error validating RSA key. Please ensure that your key supports PKCS#1 OAEP.")
  37. #
  38. # Secret roles
  39. #
  40. class SecretRoleForm(BootstrapMixin, CustomFieldModelForm):
  41. slug = SlugField()
  42. class Meta:
  43. model = SecretRole
  44. fields = ('name', 'slug', 'description')
  45. class SecretRoleCSVForm(CustomFieldModelCSVForm):
  46. slug = SlugField()
  47. class Meta:
  48. model = SecretRole
  49. fields = SecretRole.csv_headers
  50. class SecretRoleBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
  51. pk = forms.ModelMultipleChoiceField(
  52. queryset=SecretRole.objects.all(),
  53. widget=forms.MultipleHiddenInput
  54. )
  55. description = forms.CharField(
  56. max_length=200,
  57. required=False
  58. )
  59. class Meta:
  60. nullable_fields = ['description']
  61. #
  62. # Secrets
  63. #
  64. class SecretForm(BootstrapMixin, CustomFieldModelForm):
  65. device = DynamicModelChoiceField(
  66. queryset=Device.objects.all(),
  67. required=False
  68. )
  69. virtual_machine = DynamicModelChoiceField(
  70. queryset=VirtualMachine.objects.all(),
  71. required=False
  72. )
  73. plaintext = forms.CharField(
  74. max_length=SECRET_PLAINTEXT_MAX_LENGTH,
  75. required=False,
  76. label='Plaintext',
  77. widget=forms.PasswordInput(
  78. attrs={
  79. 'class': 'requires-session-key',
  80. }
  81. )
  82. )
  83. plaintext2 = forms.CharField(
  84. max_length=SECRET_PLAINTEXT_MAX_LENGTH,
  85. required=False,
  86. label='Plaintext (verify)',
  87. widget=forms.PasswordInput()
  88. )
  89. role = DynamicModelChoiceField(
  90. queryset=SecretRole.objects.all()
  91. )
  92. tags = DynamicModelMultipleChoiceField(
  93. queryset=Tag.objects.all(),
  94. required=False
  95. )
  96. class Meta:
  97. model = Secret
  98. fields = [
  99. 'device', 'virtual_machine', 'role', 'name', 'plaintext', 'plaintext2', 'tags',
  100. ]
  101. def __init__(self, *args, **kwargs):
  102. # Initialize helper selectors
  103. instance = kwargs.get('instance')
  104. initial = kwargs.get('initial', {}).copy()
  105. if instance:
  106. if type(instance.assigned_object) is Device:
  107. initial['device'] = instance.assigned_object
  108. elif type(instance.assigned_object) is VirtualMachine:
  109. initial['virtual_machine'] = instance.assigned_object
  110. kwargs['initial'] = initial
  111. super().__init__(*args, **kwargs)
  112. # A plaintext value is required when creating a new Secret
  113. if not self.instance.pk:
  114. self.fields['plaintext'].required = True
  115. def clean(self):
  116. super().clean()
  117. if not self.cleaned_data['device'] and not self.cleaned_data['virtual_machine']:
  118. raise forms.ValidationError("Secrets must be assigned to a device or virtual machine.")
  119. if self.cleaned_data['device'] and self.cleaned_data['virtual_machine']:
  120. raise forms.ValidationError("Cannot select both a device and virtual machine for secret assignment.")
  121. # Verify that the provided plaintext values match
  122. if self.cleaned_data['plaintext'] != self.cleaned_data['plaintext2']:
  123. raise forms.ValidationError({
  124. 'plaintext2': "The two given plaintext values do not match. Please check your input."
  125. })
  126. def save(self, *args, **kwargs):
  127. # Set assigned object
  128. self.instance.assigned_object = self.cleaned_data.get('device') or self.cleaned_data.get('virtual_machine')
  129. return super().save(*args, **kwargs)
  130. class SecretCSVForm(CustomFieldModelCSVForm):
  131. role = CSVModelChoiceField(
  132. queryset=SecretRole.objects.all(),
  133. to_field_name='name',
  134. help_text='Assigned role'
  135. )
  136. device = CSVModelChoiceField(
  137. queryset=Device.objects.all(),
  138. required=False,
  139. to_field_name='name',
  140. help_text='Assigned device'
  141. )
  142. virtual_machine = CSVModelChoiceField(
  143. queryset=VirtualMachine.objects.all(),
  144. required=False,
  145. to_field_name='name',
  146. help_text='Assigned VM'
  147. )
  148. plaintext = forms.CharField(
  149. help_text='Plaintext secret data'
  150. )
  151. class Meta:
  152. model = Secret
  153. fields = ['role', 'name', 'plaintext', 'device', 'virtual_machine']
  154. help_texts = {
  155. 'name': 'Name or username',
  156. }
  157. def clean(self):
  158. super().clean()
  159. device = self.cleaned_data.get('device')
  160. virtual_machine = self.cleaned_data.get('virtual_machine')
  161. # Validate device OR VM is assigned
  162. if not device and not virtual_machine:
  163. raise forms.ValidationError("Secret must be assigned to a device or a virtual machine")
  164. if device and virtual_machine:
  165. raise forms.ValidationError("Secret cannot be assigned to both a device and a virtual machine")
  166. def save(self, *args, **kwargs):
  167. # Set device/VM assignment
  168. self.instance.assigned_object = self.cleaned_data['device'] or self.cleaned_data['virtual_machine']
  169. s = super().save(*args, **kwargs)
  170. # Set plaintext on instance
  171. s.plaintext = str(self.cleaned_data['plaintext'])
  172. return s
  173. class SecretBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
  174. pk = forms.ModelMultipleChoiceField(
  175. queryset=Secret.objects.all(),
  176. widget=forms.MultipleHiddenInput()
  177. )
  178. role = DynamicModelChoiceField(
  179. queryset=SecretRole.objects.all(),
  180. required=False
  181. )
  182. name = forms.CharField(
  183. max_length=100,
  184. required=False
  185. )
  186. class Meta:
  187. nullable_fields = [
  188. 'name',
  189. ]
  190. class SecretFilterForm(BootstrapMixin, CustomFieldFilterForm):
  191. model = Secret
  192. q = forms.CharField(
  193. required=False,
  194. label=_('Search')
  195. )
  196. role_id = DynamicModelMultipleChoiceField(
  197. queryset=SecretRole.objects.all(),
  198. required=False,
  199. label=_('Role')
  200. )
  201. tag = TagFilterField(model)
  202. #
  203. # UserKeys
  204. #
  205. class UserKeyForm(BootstrapMixin, forms.ModelForm):
  206. class Meta:
  207. model = UserKey
  208. fields = ['public_key']
  209. help_texts = {
  210. 'public_key': "Enter your public RSA key. Keep the private one with you; you'll need it for decryption. "
  211. "Please note that passphrase-protected keys are not supported.",
  212. }
  213. labels = {
  214. 'public_key': ''
  215. }
  216. def clean_public_key(self):
  217. key = self.cleaned_data['public_key']
  218. # Validate the RSA key format.
  219. validate_rsa_key(key, is_secret=False)
  220. return key
  221. class ActivateUserKeyForm(forms.Form):
  222. _selected_action = forms.ModelMultipleChoiceField(
  223. queryset=UserKey.objects.all(),
  224. label='User Keys'
  225. )
  226. secret_key = forms.CharField(
  227. widget=forms.Textarea(
  228. attrs={
  229. 'class': 'vLargeTextField',
  230. }
  231. ),
  232. label='Your private key'
  233. )