|
14 | 14 | """Django form fields related to django-ca."""
|
15 | 15 |
|
16 | 16 | import abc
|
| 17 | +import json |
17 | 18 | import typing
|
18 |
| -from typing import Any, Dict, Iterable, List, Optional, Sequence, Set, Tuple, Type, Union |
| 19 | +from typing import Any, Dict, Iterable, List, Optional, Tuple, Type, Union |
| 20 | + |
| 21 | +from pydantic import ValidationError as PydanticValidationError |
19 | 22 |
|
20 | 23 | from cryptography import x509
|
21 | 24 | from cryptography.x509.oid import AuthorityInformationAccessOID
|
22 | 25 |
|
23 | 26 | from django import forms
|
| 27 | +from django.core.exceptions import ValidationError |
24 | 28 | from django.utils.safestring import mark_safe
|
25 | 29 | from django.utils.translation import gettext_lazy as _
|
26 | 30 |
|
27 |
| -from django_ca import constants, utils, widgets |
| 31 | +from django_ca import widgets |
28 | 32 | from django_ca.constants import (
|
29 | 33 | EXTENDED_KEY_USAGE_HUMAN_READABLE_NAMES,
|
30 | 34 | EXTENDED_KEY_USAGE_NAMES,
|
31 | 35 | KEY_USAGE_NAMES,
|
32 | 36 | REVOCATION_REASONS,
|
33 | 37 | )
|
34 | 38 | from django_ca.extensions import get_extension_name
|
35 |
| -from django_ca.pydantic.general_name import GeneralNameModel |
| 39 | +from django_ca.pydantic.general_name import GeneralNameModelList |
| 40 | +from django_ca.pydantic.name import NameModel |
36 | 41 | from django_ca.typehints import AlternativeNameTypeVar, CRLExtensionTypeTypeVar, ExtensionTypeTypeVar
|
37 |
| -from django_ca.utils import parse_general_name |
38 |
| -from django_ca.widgets import GeneralNameKeyValueWidget, KeyValueWidget, SubjectWidget |
| 42 | +from django_ca.widgets import GeneralNameKeyValueWidget, KeyValueWidget, NameWidget |
39 | 43 |
|
40 | 44 | if typing.TYPE_CHECKING:
|
41 | 45 | from django_ca.modelfields import LazyCertificateSigningRequest
|
@@ -120,128 +124,54 @@ def to_python(self, value: str) -> Optional[x509.ObjectIdentifier]: # type: ign
|
120 | 124 | ) from ex
|
121 | 125 |
|
122 | 126 |
|
123 |
| -class KeyValueField(forms.MultiValueField): |
| 127 | +class KeyValueField(forms.CharField): |
124 | 128 | """Dynamic Key/Value field."""
|
125 | 129 |
|
126 |
| - widget_class = KeyValueWidget |
127 |
| - |
128 |
| - def __init__(self, key_choices: Sequence[Tuple[str, str]], **kwargs: Any) -> None: |
129 |
| - fields = [ |
130 |
| - forms.JSONField(required=True), |
131 |
| - # These two just serve as template for key/value rows |
132 |
| - forms.ChoiceField(choices=key_choices, required=False), |
133 |
| - forms.CharField(required=False), |
134 |
| - ] |
| 130 | + widget = KeyValueWidget |
135 | 131 |
|
136 |
| - kwargs["require_all_fields"] = False # ... or we'd require values in the template |
| 132 | + def to_python(self, value: Optional[str]) -> List[Dict[str, Any]]: # type: ignore[override] |
| 133 | + value = super().to_python(value) |
| 134 | + if not value: |
| 135 | + return [] |
| 136 | + return json.loads(value) # type: ignore[no-any-return] |
137 | 137 |
|
138 |
| - super().__init__(fields, **kwargs) |
139 |
| - self.widget = self.widget_class(key_choices=key_choices, attrs={"class": "key-value-input"}) |
| 138 | + def pydantic_validation_error(self, ex: PydanticValidationError) -> typing.NoReturn: |
| 139 | + """Transform Pydantic ValidationError exceptions into Django ValidationError. |
140 | 140 |
|
141 |
| - def compress( |
142 |
| - self, data_list: Tuple[List[Dict[str, str]], str, str] |
143 |
| - ) -> Optional[List[Dict[str, str]]]: # pragma: no cover # This class is never used directly so far |
144 |
| - if not data_list or not data_list[0]: |
145 |
| - return None |
146 |
| - return data_list[0] |
| 141 | + This method assumes that the error occurs in `value`, as the keys are a select field and should work |
| 142 | + properly. |
| 143 | + """ |
| 144 | + raise ValidationError([error["msg"] for error in ex.errors()]) from ex |
147 | 145 |
|
148 | 146 |
|
149 |
| -class SubjectField(KeyValueField): |
150 |
| - """Specialized version of KeyValue field for a certificate subject.""" |
| 147 | +class NameField(KeyValueField): |
| 148 | + """Specialized version of KeyValue field for a x509 name.""" |
151 | 149 |
|
152 |
| - widget_class = SubjectWidget |
| 150 | + widget = NameWidget |
153 | 151 |
|
154 |
| - def __init__(self, **kwargs: Any) -> None: |
155 |
| - key_choices = [(oid.dotted_string, name) for oid, name in constants.NAME_OID_DISPLAY_NAMES.items()] |
156 |
| - super().__init__(key_choices=key_choices, **kwargs) |
157 |
| - |
158 |
| - def compress( # type: ignore[override] |
159 |
| - self, data_list: Tuple[List[Dict[str, str]], str, str] |
160 |
| - ) -> x509.Name: |
161 |
| - # Empty list happens when you press submit on a completely empty form |
162 |
| - if not data_list or not data_list[0]: |
163 |
| - return x509.Name([]) |
164 |
| - |
165 |
| - values = data_list[0] |
166 |
| - errors = [] |
167 |
| - attributes: List[x509.NameAttribute] = [] |
168 |
| - found_oids: Set[x509.ObjectIdentifier] = set() |
169 |
| - for oid_dict in values: |
170 |
| - try: |
171 |
| - oid = x509.ObjectIdentifier(oid_dict["key"]) |
172 |
| - |
173 |
| - # Check for duplicate OIDs that should not occur more than once |
174 |
| - if oid in found_oids and oid not in utils.MULTIPLE_OIDS: |
175 |
| - oid_name = constants.NAME_OID_DISPLAY_NAMES[oid] |
176 |
| - errors.append(_("%(attr)s: Attribute cannot occur more then once.") % {"attr": oid_name}) |
177 |
| - else: |
178 |
| - found_oids.add(oid) |
179 |
| - |
180 |
| - attr = x509.NameAttribute(oid=oid, value=oid_dict["value"]) |
181 |
| - except Exception as ex: # pylint: disable=broad-exception-caught # docs don't specify class |
182 |
| - # Creating the NameAttribute failed (e.g. a country code that does *not* have two characters) |
183 |
| - errors.append(str(ex)) |
184 |
| - else: |
185 |
| - attributes.append(attr) |
186 |
| - |
187 |
| - if errors: |
188 |
| - raise forms.ValidationError(errors) |
189 |
| - |
190 |
| - name = x509.Name(attributes) |
191 |
| - return name |
| 152 | + def to_python(self, value: Optional[str]) -> x509.Name: # type: ignore[override] |
| 153 | + parsed_value = super().to_python(value) |
| 154 | + converted_value = [{"oid": v["key"], "value": v["value"]} for v in parsed_value] |
| 155 | + try: |
| 156 | + model = NameModel.model_validate(converted_value) |
| 157 | + except PydanticValidationError as ex: |
| 158 | + self.pydantic_validation_error(ex) |
| 159 | + return model.cryptography |
192 | 160 |
|
193 | 161 |
|
194 | 162 | class GeneralNameKeyValueField(KeyValueField):
|
195 |
| - """test.""" |
196 |
| - |
197 |
| - widget_class = GeneralNameKeyValueWidget |
198 |
| - |
199 |
| - def __init__(self, **kwargs: Any) -> None: |
200 |
| - choices = [(key, key) for key in constants.GENERAL_NAME_TYPES] |
201 |
| - super().__init__(key_choices=choices, **kwargs) |
202 |
| - self.widget = self.widget_class(key_choices=choices, attrs={"class": "key-value-input"}) |
203 |
| - |
204 |
| - def compress( # type: ignore[override] |
205 |
| - self, data_list: Tuple[List[Dict[str, str]], str, str] |
206 |
| - ) -> List[x509.GeneralName]: |
207 |
| - # Empty list happens when you press submit on a completely empty form |
208 |
| - if not data_list or not data_list[0]: |
209 |
| - return [] |
210 |
| - |
211 |
| - names = [GeneralNameModel(type=name["key"], value=name["value"]) for name in data_list[0]] |
212 |
| - return [name.cryptography for name in names] |
| 163 | + """Specialized version of KeyValue field for a list of general names.""" |
213 | 164 |
|
| 165 | + widget = GeneralNameKeyValueWidget |
214 | 166 |
|
215 |
| -class GeneralNamesField(forms.CharField): |
216 |
| - """MultipleChoice field for :py:class:`~cg:cryptography.x509.RelativeDistinguishedName`.""" |
217 |
| - |
218 |
| - widget = widgets.GeneralNamesWidget |
219 |
| - default_error_messages = { # noqa: RUF012 # defined in base class |
220 |
| - "invalid": _("Unparsable General Name: %(error)s"), |
221 |
| - } |
222 |
| - |
223 |
| - def to_python( # type: ignore[override] # superclass uses Any for str, violates inheritance (in theory) |
224 |
| - self, value: str |
225 |
| - ) -> Optional[List[x509.GeneralName]]: |
226 |
| - if not value: |
227 |
| - return None |
228 |
| - |
229 |
| - values = [] |
230 |
| - for line in value.splitlines(): |
231 |
| - line = line.strip() |
232 |
| - if not line: |
233 |
| - continue |
234 |
| - |
235 |
| - try: |
236 |
| - values.append(parse_general_name(line)) |
237 |
| - except ValueError as ex: |
238 |
| - raise forms.ValidationError( |
239 |
| - self.error_messages["invalid"], params={"error": ex}, code="invalid" |
240 |
| - ) from ex |
241 |
| - if not values: |
242 |
| - return None |
243 |
| - |
244 |
| - return values |
| 167 | + def to_python(self, value: Optional[str]) -> List[x509.GeneralName]: # type: ignore[override] |
| 168 | + parsed_value = super().to_python(value) |
| 169 | + converted_value = [{"type": v["key"], "value": v["value"]} for v in parsed_value] |
| 170 | + try: |
| 171 | + models = GeneralNameModelList.validate_python(converted_value) |
| 172 | + except PydanticValidationError as ex: |
| 173 | + self.pydantic_validation_error(ex) |
| 174 | + return [model.cryptography for model in models] |
245 | 175 |
|
246 | 176 |
|
247 | 177 | class RelativeDistinguishedNameField(forms.CharField):
|
@@ -355,9 +285,9 @@ class DistributionPointField(ExtensionField[CRLExtensionTypeTypeVar]):
|
355 | 285 | "no-dp-or-issuer": _("A DistributionPoint needs at least a full or relative name or a crl issuer."),
|
356 | 286 | }
|
357 | 287 | fields = (
|
358 |
| - GeneralNamesField(required=False), # full_name |
| 288 | + GeneralNameKeyValueField(required=False), # full_name |
359 | 289 | RelativeDistinguishedNameField(required=False), # relative_name
|
360 |
| - GeneralNamesField(required=False), # crl_issuer |
| 290 | + GeneralNameKeyValueField(required=False), # crl_issuer |
361 | 291 | ReasonsField(required=False), # reasons
|
362 | 292 | )
|
363 | 293 |
|
@@ -405,7 +335,7 @@ class AuthorityInformationAccessField(ExtensionField[x509.AuthorityInformationAc
|
405 | 335 | """Form field for a :py:class:`~cg:cryptography.x509.AuthorityInformationAccess` extension."""
|
406 | 336 |
|
407 | 337 | extension_type = x509.AuthorityInformationAccess
|
408 |
| - fields = (GeneralNamesField(required=False), GeneralNamesField(required=False)) |
| 338 | + fields = (GeneralNameKeyValueField(required=False), GeneralNameKeyValueField(required=False)) |
409 | 339 | widget = widgets.AuthorityInformationAccessWidget
|
410 | 340 |
|
411 | 341 | def get_value(
|
|
0 commit comments