-
-
Notifications
You must be signed in to change notification settings - Fork 302
[feature] Added X.509 Certificate Generator Templates #1378
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: gsoc26-x509-certificate-generator-templates
Are you sure you want to change the base?
Changes from 1 commit
25f1a21
a795e09
b946d26
ff1e8a1
0053bcf
01221d9
5c551c7
d6fd193
cae38a5
02912a0
3434719
298572f
5c522e9
052cebf
148bfd2
badf1c1
a692c77
16703f1
212b839
37ff9a9
ef90a6c
b8bc3e0
88af90a
bca3809
99cd151
7390020
ee79c93
c92cc5e
dfa650c
88003e2
b9db466
28f9cce
228e9bc
1e9e813
717c3e1
7f8dba9
e7a6b16
406e780
25ab4c3
4711fa3
a819721
8c32282
fb54051
b52fe63
0685dc5
b486eb5
52c7544
b9a9d2a
8efc405
8b3fb40
01a556c
147c4b5
4ded312
063c292
90c7415
1c6d532
0925720
7bab6fe
d66b067
2793599
74a1e8f
73b3283
bd68792
53494cd
c12aee9
375844d
1d117a2
8c6e35a
85c865d
dda2865
0a9c7d1
98c191b
bf08a9c
c803d75
6988562
db8c592
04bfa80
1df332c
217e574
3b2a661
5fc22c3
c62c583
164a97b
83134fc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,7 +22,11 @@ | |
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
| TYPE_CHOICES = (("generic", _("Generic")), ("vpn", _("VPN-client"))) | ||
| TYPE_CHOICES = ( | ||
| ("generic", _("Generic")), | ||
| ("vpn", _("VPN-client")), | ||
| ("cert", _("Certificate")), | ||
| ) | ||
|
|
||
|
|
||
| def default_auto_cert(): | ||
|
|
@@ -55,6 +59,28 @@ class AbstractTemplate(ShareableOrgMixinUniqueName, BaseConfig): | |
| null=True, | ||
| on_delete=models.CASCADE, | ||
| ) | ||
| ca = models.ForeignKey( | ||
| get_model_name("django_x509", "Ca"), | ||
| on_delete=models.CASCADE, | ||
| verbose_name=_("Certificate Authority"), | ||
| blank=True, | ||
| null=True, | ||
| help_text=_( | ||
| "The Certificate Authority that will sign certificates generated " | ||
| "by this template." | ||
| ), | ||
| ) | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| blueprint_cert = models.ForeignKey( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. SET_NULL allows the blueprint of an active template to be deleted without running Template.clean(). Existing devices keep certificates cloned from the old blueprint while later assignments silently fall back to CA defaults, which is exactly the inconsistency the active mutation lock is intended to prevent. Restrict direct deletion while a blueprint is referenced by an active template. RESTRICT may preserve the intended full CA cascade while preventing this direct deletion path. Cover active and unassigned templates.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. SET_NULL allows the blueprint of an active template to be deleted without running Template.clean(). Existing devices keep certificates cloned from the old blueprint while later assignments silently fall back to CA defaults, which is exactly the inconsistency the active mutation lock is intended to prevent. Restrict direct deletion while a blueprint is referenced by an active template. RESTRICT may preserve the intended full CA cascade while preventing this direct deletion path. Cover active and unassigned templates. |
||
| get_model_name("django_x509", "Cert"), | ||
| on_delete=models.SET_NULL, | ||
| verbose_name=_("Blueprint Certificate"), | ||
| blank=True, | ||
| null=True, | ||
| help_text=_( | ||
| "Optional: Select an unassigned certificate to copy extensions and " | ||
| "properties from." | ||
| ), | ||
| ) | ||
| type = models.CharField( | ||
| _("type"), | ||
| max_length=16, | ||
|
|
@@ -222,6 +248,8 @@ def clean(self, *args, **kwargs): | |
| * if flagged as required forces it also to be default | ||
| """ | ||
| self._validate_org_relation("vpn") | ||
| self._validate_org_relation("ca") | ||
| self._validate_org_relation("blueprint_cert") | ||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
| if not self.default_values: | ||
| self.default_values = {} | ||
| if not isinstance(self.default_values, dict): | ||
|
|
@@ -234,15 +262,53 @@ def clean(self, *args, **kwargs): | |
| ) | ||
| elif self.type != "vpn": | ||
| self.vpn = None | ||
| self.auto_cert = False | ||
| if self.type != "cert": | ||
| self.auto_cert = False | ||
| if self.type == "vpn" and not self.config: | ||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
| self.config = self.vpn.auto_client( | ||
| auto_cert=self.auto_cert, template_backend_class=self.backend_class | ||
| ) | ||
| if self.type == "cert": | ||
| if not self.ca: | ||
| raise ValidationError( | ||
| { | ||
| "ca": _( | ||
| "A Certificate Authority is required when the template " | ||
| "type is certificate." | ||
| ) | ||
| } | ||
| ) | ||
| if self.blueprint_cert and self.blueprint_cert.ca_id != self.ca_id: | ||
| raise ValidationError( | ||
| { | ||
| "blueprint_cert": _( | ||
| "The selected certificate must match the selected " | ||
| "Certificate Authority." | ||
| ) | ||
| } | ||
| ) | ||
| if self.blueprint_cert and hasattr( | ||
| self.blueprint_cert, "devicecertificate_set" | ||
| ): | ||
| if self.blueprint_cert.devicecertificate_set.exists(): | ||
| raise ValidationError( | ||
| { | ||
| "blueprint_cert": _( | ||
| "This certificate is already assigned to a device. " | ||
| "Please select an unassigned certificate to use as a " | ||
| "blueprint." | ||
| ) | ||
| } | ||
| ) | ||
|
stktyagi marked this conversation as resolved.
|
||
| if not self.config: | ||
| self.config = {} | ||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
| else: | ||
| self.ca = None | ||
| self.blueprint_cert = None | ||
| if self.required and not self.default: | ||
| self.default = True | ||
| super().clean(*args, **kwargs) | ||
| if not self.config: | ||
| if not self.config and self.type != "cert": | ||
| raise ValidationError(_("The configuration field cannot be empty.")) | ||
|
|
||
| def get_context(self, system=False): | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| # Generated by Django 5.2.14 on 2026-05-26 19:18 | ||
|
|
||
| import django.db.models.deletion | ||
| from django.conf import settings | ||
| from django.db import migrations, models | ||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
|
|
||
| dependencies = [ | ||
| ("config", "0063_replace_jsonfield_with_django_builtin"), | ||
| migrations.swappable_dependency(settings.DJANGO_X509_CA_MODEL), | ||
| migrations.swappable_dependency(settings.DJANGO_X509_CERT_MODEL), | ||
| ] | ||
|
|
||
| operations = [ | ||
| migrations.AddField( | ||
| model_name="template", | ||
| name="blueprint_cert", | ||
| field=models.ForeignKey( | ||
| blank=True, | ||
| help_text="Optional: Select an unassigned certificate to " | ||
| "copy extensions and properties from.", | ||
| null=True, | ||
| on_delete=django.db.models.deletion.SET_NULL, | ||
| to=settings.DJANGO_X509_CERT_MODEL, | ||
| verbose_name="Blueprint Certificate", | ||
| ), | ||
| ), | ||
| migrations.AddField( | ||
| model_name="template", | ||
| name="ca", | ||
| field=models.ForeignKey( | ||
| blank=True, | ||
| help_text="The Certificate Authority that will sign " | ||
| "certificates generated by this template.", | ||
| null=True, | ||
| on_delete=django.db.models.deletion.CASCADE, | ||
| to=settings.DJANGO_X509_CA_MODEL, | ||
| verbose_name="Certificate Authority", | ||
| ), | ||
| ), | ||
| migrations.AlterField( | ||
| model_name="template", | ||
| name="type", | ||
| field=models.CharField( | ||
| choices=[ | ||
| ("generic", "Generic"), | ||
| ("vpn", "VPN-client"), | ||
| ("cert", "Certificate"), | ||
| ], | ||
| db_index=True, | ||
| default="generic", | ||
| help_text="template type, determines which features are available", | ||
| max_length=16, | ||
| verbose_name="type", | ||
| ), | ||
| ), | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| # Generated by Django 5.2.14 on 2026-05-26 19:52 | ||
|
|
||
| import django.db.models.deletion | ||
| from django.db import migrations, models | ||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
|
|
||
| dependencies = [ | ||
| ("sample_config", "0009_replace_jsonfield_with_django_builtin"), | ||
| ("sample_pki", "0004_alter_ca_extensions_alter_ca_key_length_and_more"), | ||
| ] | ||
|
|
||
| operations = [ | ||
| migrations.AddField( | ||
| model_name="template", | ||
| name="blueprint_cert", | ||
| field=models.ForeignKey( | ||
| blank=True, | ||
| help_text="Optional: Select an unassigned certificate to copy " | ||
| "extensions and properties from.", | ||
| null=True, | ||
| on_delete=django.db.models.deletion.SET_NULL, | ||
| to="sample_pki.cert", | ||
| verbose_name="Blueprint Certificate", | ||
| ), | ||
| ), | ||
| migrations.AddField( | ||
| model_name="template", | ||
| name="ca", | ||
| field=models.ForeignKey( | ||
| blank=True, | ||
| help_text="The Certificate Authority that will sign certificates " | ||
| "generated by this template.", | ||
| null=True, | ||
| on_delete=django.db.models.deletion.CASCADE, | ||
| to="sample_pki.ca", | ||
| verbose_name="Certificate Authority", | ||
| ), | ||
| ), | ||
| migrations.AlterField( | ||
| model_name="template", | ||
| name="type", | ||
| field=models.CharField( | ||
| choices=[ | ||
| ("generic", "Generic"), | ||
| ("vpn", "VPN-client"), | ||
| ("cert", "Certificate"), | ||
| ], | ||
| db_index=True, | ||
| default="generic", | ||
| help_text="template type, determines which features are available", | ||
| max_length=16, | ||
| verbose_name="type", | ||
| ), | ||
| ), | ||
| ] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shouldn't we call this "Certificate generator" ?
I think "Certificate" here is too vague and bad for UX as it's confusing and I also think the same for the docs text which is not using the "Certificate generator" wording nor any equivalent meaning. We must indicate that these templates automate the creation of X509 certificates.