forms.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. from Crypto.Cipher import PKCS1_OAEP
  2. from Crypto.PublicKey import RSA
  3. from django import forms
  4. from django.contrib.contenttypes.models import ContentType
  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, CSVModelForm, 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, forms.ModelForm):
  41. slug = SlugField()
  42. class Meta:
  43. model = SecretRole
  44. fields = ('name', 'slug', 'description')
  45. class SecretRoleCSVForm(CSVModelForm):
  46. slug = SlugField()
  47. class Meta:
  48. model = SecretRole
  49. fields = SecretRole.csv_headers
  50. #
  51. # Secrets
  52. #
  53. class SecretForm(BootstrapMixin, CustomFieldModelForm):
  54. device = DynamicModelChoiceField(
  55. queryset=Device.objects.all(),
  56. required=False,
  57. display_field='display_name'
  58. )
  59. virtual_machine = DynamicModelChoiceField(
  60. queryset=VirtualMachine.objects.all(),
  61. required=False
  62. )
  63. plaintext = forms.CharField(
  64. max_length=SECRET_PLAINTEXT_MAX_LENGTH,
  65. required=False,
  66. label='Plaintext',
  67. widget=forms.PasswordInput(
  68. attrs={
  69. 'class': 'requires-session-key',
  70. }
  71. )
  72. )
  73. plaintext2 = forms.CharField(
  74. max_length=SECRET_PLAINTEXT_MAX_LENGTH,
  75. required=False,
  76. label='Plaintext (verify)',
  77. widget=forms.PasswordInput()
  78. )
  79. role = DynamicModelChoiceField(
  80. queryset=SecretRole.objects.all()
  81. )
  82. tags = DynamicModelMultipleChoiceField(
  83. queryset=Tag.objects.all(),
  84. required=False
  85. )
  86. class Meta:
  87. model = Secret
  88. fields = [
  89. 'device', 'virtual_machine', 'role', 'name', 'plaintext', 'plaintext2', 'tags',
  90. ]
  91. def __init__(self, *args, **kwargs):
  92. # Initialize helper selectors
  93. instance = kwargs.get('instance')
  94. initial = kwargs.get('initial', {}).copy()
  95. if instance:
  96. if type(instance.assigned_object) is Device:
  97. initial['device'] = instance.assigned_object
  98. elif type(instance.assigned_object) is VirtualMachine:
  99. initial['virtual_machine'] = instance.assigned_object
  100. kwargs['initial'] = initial
  101. super().__init__(*args, **kwargs)
  102. # A plaintext value is required when creating a new Secret
  103. if not self.instance.pk:
  104. self.fields['plaintext'].required = True
  105. def clean(self):
  106. super().clean()
  107. if not self.cleaned_data['device'] and not self.cleaned_data['virtual_machine']:
  108. raise forms.ValidationError("Secrets must be assigned to a device or virtual machine.")
  109. if self.cleaned_data['device'] and self.cleaned_data['virtual_machine']:
  110. raise forms.ValidationError("Cannot select both a device and virtual machine for secret assignment.")
  111. # Verify that the provided plaintext values match
  112. if self.cleaned_data['plaintext'] != self.cleaned_data['plaintext2']:
  113. raise forms.ValidationError({
  114. 'plaintext2': "The two given plaintext values do not match. Please check your input."
  115. })
  116. def save(self, *args, **kwargs):
  117. # Set assigned object
  118. self.instance.assigned_object = self.cleaned_data.get('device') or self.cleaned_data.get('virtual_machine')
  119. return super().save(*args, **kwargs)
  120. class SecretCSVForm(CustomFieldModelCSVForm):
  121. role = CSVModelChoiceField(
  122. queryset=SecretRole.objects.all(),
  123. to_field_name='name',
  124. help_text='Assigned role'
  125. )
  126. device = CSVModelChoiceField(
  127. queryset=Device.objects.all(),
  128. required=False,
  129. to_field_name='name',
  130. help_text='Assigned device'
  131. )
  132. virtual_machine = CSVModelChoiceField(
  133. queryset=VirtualMachine.objects.all(),
  134. required=False,
  135. to_field_name='name',
  136. help_text='Assigned VM'
  137. )
  138. plaintext = forms.CharField(
  139. help_text='Plaintext secret data'
  140. )
  141. class Meta:
  142. model = Secret
  143. fields = ['role', 'name', 'plaintext', 'device', 'virtual_machine']
  144. help_texts = {
  145. 'name': 'Name or username',
  146. }
  147. def clean(self):
  148. super().clean()
  149. device = self.cleaned_data.get('device')
  150. virtual_machine = self.cleaned_data.get('virtual_machine')
  151. # Validate device OR VM is assigned
  152. if not device and not virtual_machine:
  153. raise forms.ValidationError("Secret must be assigned to a device or a virtual machine")
  154. if device and virtual_machine:
  155. raise forms.ValidationError("Secret cannot be assigned to both a device and a virtual machine")
  156. def save(self, *args, **kwargs):
  157. # Set device/VM assignment
  158. self.instance.assigned_object = self.cleaned_data['device'] or self.cleaned_data['virtual_machine']
  159. s = super().save(*args, **kwargs)
  160. # Set plaintext on instance
  161. s.plaintext = str(self.cleaned_data['plaintext'])
  162. return s
  163. class SecretBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
  164. pk = forms.ModelMultipleChoiceField(
  165. queryset=Secret.objects.all(),
  166. widget=forms.MultipleHiddenInput()
  167. )
  168. role = DynamicModelChoiceField(
  169. queryset=SecretRole.objects.all(),
  170. required=False
  171. )
  172. name = forms.CharField(
  173. max_length=100,
  174. required=False
  175. )
  176. class Meta:
  177. nullable_fields = [
  178. 'name',
  179. ]
  180. class SecretFilterForm(BootstrapMixin, CustomFieldFilterForm):
  181. model = Secret
  182. q = forms.CharField(
  183. required=False,
  184. label='Search'
  185. )
  186. role = DynamicModelMultipleChoiceField(
  187. queryset=SecretRole.objects.all(),
  188. to_field_name='slug',
  189. required=False
  190. )
  191. tag = TagFilterField(model)
  192. #
  193. # UserKeys
  194. #
  195. class UserKeyForm(BootstrapMixin, forms.ModelForm):
  196. class Meta:
  197. model = UserKey
  198. fields = ['public_key']
  199. help_texts = {
  200. 'public_key': "Enter your public RSA key. Keep the private one with you; you'll need it for decryption. "
  201. "Please note that passphrase-protected keys are not supported.",
  202. }
  203. labels = {
  204. 'public_key': ''
  205. }
  206. def clean_public_key(self):
  207. key = self.cleaned_data['public_key']
  208. # Validate the RSA key format.
  209. validate_rsa_key(key, is_secret=False)
  210. return key
  211. class ActivateUserKeyForm(forms.Form):
  212. _selected_action = forms.ModelMultipleChoiceField(
  213. queryset=UserKey.objects.all(),
  214. label='User Keys'
  215. )
  216. secret_key = forms.CharField(
  217. widget=forms.Textarea(
  218. attrs={
  219. 'class': 'vLargeTextField',
  220. }
  221. ),
  222. label='Your private key'
  223. )