Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions qgis-app/plugins/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from django.utils.translation import gettext_lazy as _
from plugins.models import *
from plugins.validator import validator
from plugins.views import plugin_notify
from plugins.views import plugin_notify, send_upload_confirmation_email
from rpc4django import rpcmethod
from taggit.models import Tag

Expand Down Expand Up @@ -120,7 +120,10 @@ def plugin_upload(package, **kwargs):
package.len,
"UTF-8",
),
"approved": request.user.has_perm("plugins.can_approve") or plugin.approved,
# Always start unapproved; async security checks will auto-approve
# trusted users after validation completes.
"approved": False,
"validation_status": VALIDATION_STATUS_VALIDATING,
}

# Optional version metadata
Expand All @@ -133,6 +136,13 @@ def plugin_upload(package, **kwargs):
new_version = PluginVersion(**version_data)
new_version.clean()
new_version.save()

# Send Stage 1 upload confirmation email
send_upload_confirmation_email(new_version)

# Queue async security scan task
from plugins.tasks.run_security_scan import run_security_scan_task
Comment thread
Xpirix marked this conversation as resolved.
Outdated
run_security_scan_task.delay(new_version.pk)
except IntegrityError as e:
# Avoids error: current transaction is aborted, commands ignored until
# end of transaction block
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 4.2.28 on 2026-03-12 02:16

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('plugins', '0019_remove_pluginversion_supports_qt6'),
]

operations = [
migrations.AddField(
model_name='pluginversion',
name='validation_status',
field=models.CharField(choices=[('pending', 'Pending'), ('validating', 'Validating'), ('validated', 'Validated'), ('blocked', 'Blocked')], db_index=True, default='pending', help_text="Validation status based on security and quality checks. New uploads start as 'validating' until checks complete. 'blocked' means critical issues were found and the version is not available for approval or download.", max_length=20, verbose_name='Validation status'),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import django.utils.timezone
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("plugins", "0020_pluginversion_validation_status"),
]

operations = [
migrations.AlterField(
model_name="pluginversionsecurityscan",
name="scanned_on",
field=models.DateTimeField(
default=django.utils.timezone.now,
verbose_name="Scanned on",
),
),
]
42 changes: 41 additions & 1 deletion qgis-app/plugins/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -786,6 +786,19 @@ def get_queryset(self):
)


VALIDATION_STATUS_PENDING = "pending"
VALIDATION_STATUS_VALIDATING = "validating"
VALIDATION_STATUS_VALIDATED = "validated"
VALIDATION_STATUS_BLOCKED = "blocked"

VALIDATION_STATUS_CHOICES = [
(VALIDATION_STATUS_PENDING, _("Pending")),
(VALIDATION_STATUS_VALIDATING, _("Validating")),
(VALIDATION_STATUS_VALIDATED, _("Validated")),
(VALIDATION_STATUS_BLOCKED, _("Blocked")),
]


def vjust(str, level=3, delim=".", bitsize=3, fillchar=" ", force_zero=False):
"""
Normalize a dotted version string.
Expand Down Expand Up @@ -934,6 +947,22 @@ class PluginVersion(models.Model):
null=True,
)
is_from_token = models.BooleanField(_("Is uploaded using token"), default=False)

# Validation status for security and QA checks
validation_status = models.CharField(
_("Validation status"),
max_length=20,
choices=VALIDATION_STATUS_CHOICES,
default=VALIDATION_STATUS_PENDING,
db_index=True,
help_text=_(
"Validation status based on security and quality checks. "
"New uploads start as 'validating' until checks complete. "
"'blocked' means critical issues were found and the version "
"is not available for approval or download."
),
)

# Link to the token if upload is using token
token = models.ForeignKey(
PluginOutstandingToken,
Expand All @@ -949,6 +978,17 @@ class PluginVersion(models.Model):
stable_objects = StablePluginVersions()
experimental_objects = ExperimentalPluginVersions()

@property
def is_available(self):
"""
Returns True if the version is available for download/approval
(not blocked by security checks).
"""
return self.validation_status not in [
Comment thread
Xpirix marked this conversation as resolved.
VALIDATION_STATUS_VALIDATING,
VALIDATION_STATUS_BLOCKED,
]

@property
def file_name(self):
return os.path.basename(self.package.file.name)
Expand Down Expand Up @@ -1185,7 +1225,7 @@ class PluginVersionSecurityScan(models.Model):
PluginVersion, on_delete=models.CASCADE, related_name="security_scan"
)
scanned_on = models.DateTimeField(
_("Scanned on"), auto_now_add=True, editable=False
_("Scanned on"), default=timezone.now, editable=False
)

# Summary statistics
Expand Down
3 changes: 3 additions & 0 deletions qgis-app/plugins/security_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ def run_security_scan(plugin_version):
PluginVersionSecurityScan instance or None if scan fails
"""
try:
from django.utils import timezone

# Get the package file path
package_path = plugin_version.package.path

Expand All @@ -32,6 +34,7 @@ def run_security_scan(plugin_version):
security_scan, created = PluginVersionSecurityScan.objects.update_or_create(
plugin_version=plugin_version,
defaults={
"scanned_on": timezone.now(),
"total_checks": report["summary"]["total_checks"],
"passed_checks": report["summary"]["passed"],
"warning_count": report["summary"]["warnings"],
Expand Down
9 changes: 5 additions & 4 deletions qgis-app/plugins/tasks/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from plugins.tasks.generate_plugins_xml import * # noqa
from plugins.tasks.update_qgis_versions import * # noqa
from plugins.tasks.rebuild_search_index import * # noqa
from plugins.tasks.get_sustaining_members import * # noqa
from plugins.tasks.generate_plugins_xml import generate_plugins_xml
from plugins.tasks.update_qgis_versions import update_qgis_versions
from plugins.tasks.rebuild_search_index import rebuild_search_index
from plugins.tasks.get_sustaining_members import get_sustaining_members
from plugins.tasks.run_security_scan import run_security_scan_task
Loading
Loading