prompt: We are currently solving the following issue within our repository. Here is the issue text:
  --- BEGIN ISSUE ---
  Protect against Git-repo jobs clobbering sys.modules
  ### As ...
  
  Patti - Platform Admin
  
  ### I want ...
  
  To add Jobs via Git repositories without worrying about them impacting the overall stability of the platform.
  
  After #3840 , there is a risk that a misnamed (or maliciously-named) Git repository could potentially clobber existing Python code. For example, creating a repository and assigning it the slug `nautobot` causes all sorts of havoc as it results in Nautobot unloading itself and then attempting to reimport all of Nautobot's code from the Git repository by that name.
  
  ### So that ...
  
  The application is resilient to user error as well as mischievous/malicious actions by authenticated users.
  
  ### I know this is done when...
  
  - Creating a GitRepository whose `slug` matches any currently loaded Python module in the Nautobot environment is rejected with an appropriate error message.
  
  ### Optional - Feature groups this request pertains to.
  
  - [X] Automation
  - [ ] Circuits
  - [ ] DCIM
  - [ ] IPAM
  - [ ] Misc (including Data Sources)
  - [ ] Organization
  - [ ] Plugins (and other Extensibility)
  - [X] Security (Secrets, etc)
  - [ ] Image Management
  - [ ] UI/UX
  - [ ] Documentation
  - [ ] Other (not directly a platform feature)
  
  ### Database Changes
  
  None
  
  ### External Dependencies
  
  None
  
  --- END ISSUE ---
  
  Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
  --- BEGIN FILES ---
  Path: `nautobot/extras/models/datasources.py`
  Content:
  ```
  1 """Models for representing external data sources."""
  2 import os
  3 
  4 from django.conf import settings
  5 from django.core.exceptions import ValidationError
  6 from django.core.serializers.json import DjangoJSONEncoder
  7 from django.core.validators import URLValidator
  8 from django.db import models
  9 
  10 from nautobot.core.models.fields import AutoSlugField, slugify_dashes_to_underscores
  11 from nautobot.core.models.generics import PrimaryModel
  12 from nautobot.extras.utils import extras_features, check_if_key_is_graphql_safe
  13 
  14 
  15 @extras_features(
  16     "config_context_owners",
  17     "export_template_owners",
  18     "job_results",
  19     "webhooks",
  20 )
  21 class GitRepository(PrimaryModel):
  22     """Representation of a Git repository used as an external data source."""
  23 
  24     name = models.CharField(
  25         max_length=100,
  26         unique=True,
  27     )
  28     slug = AutoSlugField(
  29         populate_from="name",
  30         help_text="Internal field name. Please use underscores rather than dashes in this key.",
  31         slugify_function=slugify_dashes_to_underscores,
  32     )
  33 
  34     remote_url = models.URLField(
  35         max_length=255,
  36         # For the moment we don't support ssh:// and git:// URLs
  37         help_text="Only HTTP and HTTPS URLs are presently supported",
  38         validators=[URLValidator(schemes=["http", "https"])],
  39     )
  40     branch = models.CharField(
  41         max_length=64,
  42         default="main",
  43     )
  44 
  45     current_head = models.CharField(
  46         help_text="Commit hash of the most recent fetch from the selected branch. Used for syncing between workers.",
  47         max_length=48,
  48         default="",
  49         blank=True,
  50     )
  51 
  52     secrets_group = models.ForeignKey(
  53         to="extras.SecretsGroup",
  54         on_delete=models.SET_NULL,
  55         default=None,
  56         blank=True,
  57         null=True,
  58         related_name="git_repositories",
  59     )
  60 
  61     # Data content types that this repo is a source of. Valid options are dynamically generated based on
  62     # the data types registered in registry['datasource_contents'].
  63     provided_contents = models.JSONField(encoder=DjangoJSONEncoder, default=list, blank=True)
  64 
  65     clone_fields = ["remote_url", "secrets_group", "provided_contents"]
  66 
  67     class Meta:
  68         ordering = ["name"]
  69         verbose_name = "Git repository"
  70         verbose_name_plural = "Git repositories"
  71 
  72     def __init__(self, *args, **kwargs):
  73         super().__init__(*args, **kwargs)
  74 
  75         # Store the initial repo slug so we can check for changes on save().
  76         self.__initial_slug = self.slug
  77 
  78     def __str__(self):
  79         return self.name
  80 
  81     def clean(self):
  82         super().clean()
  83 
  84         if self.slug != "":
  85             check_if_key_is_graphql_safe(self.__class__.__name__, self.slug, "slug")
  86 
  87             if self.present_in_database and self.slug != self.__initial_slug:
  88                 raise ValidationError(
  89                     f"Slug cannot be changed once set. Current slug is {self.__initial_slug}, "
  90                     f"requested slug is {self.slug}"
  91                 )
  92 
  93     def get_latest_sync(self):
  94         """
  95         Return a `JobResult` for the latest sync operation.
  96 
  97         Returns:
  98             JobResult
  99         """
  100         from nautobot.extras.models import JobResult
  101 
  102         # This will match all "GitRepository" jobs (pull/refresh, dry-run, etc.)
  103         prefix = "nautobot.core.jobs.GitRepository"
  104         return JobResult.objects.filter(task_name__startswith=prefix, task_kwargs__repository=self.pk).latest()
  105 
  106     def to_csv(self):
  107         return (
  108             self.name,
  109             self.slug,
  110             self.remote_url,
  111             self.branch,
  112             self.secrets_group.name if self.secrets_group else None,
  113             self.provided_contents,
  114         )
  115 
  116     @property
  117     def filesystem_path(self):
  118         return os.path.join(settings.GIT_ROOT, self.slug)
  119 
  120     def sync(self, user, dry_run=False):
  121         """
  122         Enqueue a Job to pull the Git repository from the remote and return the sync result.
  123 
  124         Args:
  125             user (User): The User that will perform the sync.
  126             dry_run (bool): If set, dry-run the Git sync.
  127 
  128         Returns:
  129             JobResult
  130         """
  131         from nautobot.extras.datasources import (
  132             enqueue_pull_git_repository_and_refresh_data,
  133             enqueue_git_repository_diff_origin_and_local,
  134         )
  135 
  136         if dry_run:
  137             return enqueue_git_repository_diff_origin_and_local(self, user)
  138         return enqueue_pull_git_repository_and_refresh_data(self, user)
  139
  ```
  --- END FILES ---
  
  Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.
  
  Here is an example:
  
  ```diff
  diff --git a/examples/server_async.py b/examples/server_async.py
  --- a/examples/server_async.py
  +++ b/examples/server_async.py
  @@ -313,4 +313,4 @@
   
   
   if __name__ == "__main__":
  -    asyncio.run(run_async_server("."), debug=True)
  +    asyncio.run(run_async_server(), debug=True)
  diff --git a/examples/server_sync.py b/examples/server_sync.py
  --- a/examples/server_sync.py
  +++ b/examples/server_sync.py
  @@ -313,5 +313,5 @@
   
   
   if __name__ == "__main__":
  -    server = run_sync_server(".")
  +    server = run_sync_server()
       server.shutdown()
  
  ```
  
golden_diff: diff --git a/nautobot/extras/models/datasources.py b/nautobot/extras/models/datasources.py
  --- a/nautobot/extras/models/datasources.py
  +++ b/nautobot/extras/models/datasources.py
  @@ -1,4 +1,5 @@
   """Models for representing external data sources."""
  +from importlib.util import find_spec
   import os
   
   from django.conf import settings
  @@ -81,13 +82,24 @@
       def clean(self):
           super().clean()
   
  -        if self.slug != "":
  -            check_if_key_is_graphql_safe(self.__class__.__name__, self.slug, "slug")
  +        # Autogenerate slug now, rather than in pre_save(), if not set already, as we need to check it below.
  +        if self.slug == "":
  +            self._meta.get_field("slug").create_slug(self, add=(not self.present_in_database))
  +
  +        if self.present_in_database and self.slug != self.__initial_slug:
  +            raise ValidationError(
  +                f"Slug cannot be changed once set. Current slug is {self.__initial_slug}, "
  +                f"requested slug is {self.slug}"
  +            )
   
  -            if self.present_in_database and self.slug != self.__initial_slug:
  +        if not self.present_in_database:
  +            check_if_key_is_graphql_safe(self.__class__.__name__, self.slug, "slug")
  +            # Check on create whether the proposed slug conflicts with a module name already in the Python environment.
  +            # Because we add GIT_ROOT to the end of sys.path, trying to import this repository will instead
  +            # import the earlier-found Python module in its place, which would be undesirable.
  +            if find_spec(self.slug) is not None:
                   raise ValidationError(
  -                    f"Slug cannot be changed once set. Current slug is {self.__initial_slug}, "
  -                    f"requested slug is {self.slug}"
  +                    f'Please choose a different slug, as "{self.slug}" is an installed Python package or module.'
                   )
   
       def get_latest_sync(self):
  
verification_info: {"golden_diff": "diff --git a/nautobot/extras/models/datasources.py b/nautobot/extras/models/datasources.py\n--- a/nautobot/extras/models/datasources.py\n+++ b/nautobot/extras/models/datasources.py\n@@ -1,4 +1,5 @@\n \"\"\"Models for representing external data sources.\"\"\"\n+from importlib.util import find_spec\n import os\n \n from django.conf import settings\n@@ -81,13 +82,24 @@\n     def clean(self):\n         super().clean()\n \n-        if self.slug != \"\":\n-            check_if_key_is_graphql_safe(self.__class__.__name__, self.slug, \"slug\")\n+        # Autogenerate slug now, rather than in pre_save(), if not set already, as we need to check it below.\n+        if self.slug == \"\":\n+            self._meta.get_field(\"slug\").create_slug(self, add=(not self.present_in_database))\n+\n+        if self.present_in_database and self.slug != self.__initial_slug:\n+            raise ValidationError(\n+                f\"Slug cannot be changed once set. Current slug is {self.__initial_slug}, \"\n+                f\"requested slug is {self.slug}\"\n+            )\n \n-            if self.present_in_database and self.slug != self.__initial_slug:\n+        if not self.present_in_database:\n+            check_if_key_is_graphql_safe(self.__class__.__name__, self.slug, \"slug\")\n+            # Check on create whether the proposed slug conflicts with a module name already in the Python environment.\n+            # Because we add GIT_ROOT to the end of sys.path, trying to import this repository will instead\n+            # import the earlier-found Python module in its place, which would be undesirable.\n+            if find_spec(self.slug) is not None:\n                 raise ValidationError(\n-                    f\"Slug cannot be changed once set. Current slug is {self.__initial_slug}, \"\n-                    f\"requested slug is {self.slug}\"\n+                    f'Please choose a different slug, as \"{self.slug}\" is an installed Python package or module.'\n                 )\n \n     def get_latest_sync(self):\n", "issue": "Protect against Git-repo jobs clobbering sys.modules\n### As ...\n\nPatti - Platform Admin\n\n### I want ...\n\nTo add Jobs via Git repositories without worrying about them impacting the overall stability of the platform.\r\n\r\nAfter #3840 , there is a risk that a misnamed (or maliciously-named) Git repository could potentially clobber existing Python code. For example, creating a repository and assigning it the slug `nautobot` causes all sorts of havoc as it results in Nautobot unloading itself and then attempting to reimport all of Nautobot's code from the Git repository by that name.\n\n### So that ...\n\nThe application is resilient to user error as well as mischievous/malicious actions by authenticated users.\n\n### I know this is done when...\n\n- Creating a GitRepository whose `slug` matches any currently loaded Python module in the Nautobot environment is rejected with an appropriate error message.\n\n### Optional - Feature groups this request pertains to.\n\n- [X] Automation\n- [ ] Circuits\n- [ ] DCIM\n- [ ] IPAM\n- [ ] Misc (including Data Sources)\n- [ ] Organization\n- [ ] Plugins (and other Extensibility)\n- [X] Security (Secrets, etc)\n- [ ] Image Management\n- [ ] UI/UX\n- [ ] Documentation\n- [ ] Other (not directly a platform feature)\n\n### Database Changes\n\nNone\n\n### External Dependencies\n\nNone\n", "before_files": [{"content": "\"\"\"Models for representing external data sources.\"\"\"\nimport os\n\nfrom django.conf import settings\nfrom django.core.exceptions import ValidationError\nfrom django.core.serializers.json import DjangoJSONEncoder\nfrom django.core.validators import URLValidator\nfrom django.db import models\n\nfrom nautobot.core.models.fields import AutoSlugField, slugify_dashes_to_underscores\nfrom nautobot.core.models.generics import PrimaryModel\nfrom nautobot.extras.utils import extras_features, check_if_key_is_graphql_safe\n\n\n@extras_features(\n    \"config_context_owners\",\n    \"export_template_owners\",\n    \"job_results\",\n    \"webhooks\",\n)\nclass GitRepository(PrimaryModel):\n    \"\"\"Representation of a Git repository used as an external data source.\"\"\"\n\n    name = models.CharField(\n        max_length=100,\n        unique=True,\n    )\n    slug = AutoSlugField(\n        populate_from=\"name\",\n        help_text=\"Internal field name. Please use underscores rather than dashes in this key.\",\n        slugify_function=slugify_dashes_to_underscores,\n    )\n\n    remote_url = models.URLField(\n        max_length=255,\n        # For the moment we don't support ssh:// and git:// URLs\n        help_text=\"Only HTTP and HTTPS URLs are presently supported\",\n        validators=[URLValidator(schemes=[\"http\", \"https\"])],\n    )\n    branch = models.CharField(\n        max_length=64,\n        default=\"main\",\n    )\n\n    current_head = models.CharField(\n        help_text=\"Commit hash of the most recent fetch from the selected branch. Used for syncing between workers.\",\n        max_length=48,\n        default=\"\",\n        blank=True,\n    )\n\n    secrets_group = models.ForeignKey(\n        to=\"extras.SecretsGroup\",\n        on_delete=models.SET_NULL,\n        default=None,\n        blank=True,\n        null=True,\n        related_name=\"git_repositories\",\n    )\n\n    # Data content types that this repo is a source of. Valid options are dynamically generated based on\n    # the data types registered in registry['datasource_contents'].\n    provided_contents = models.JSONField(encoder=DjangoJSONEncoder, default=list, blank=True)\n\n    clone_fields = [\"remote_url\", \"secrets_group\", \"provided_contents\"]\n\n    class Meta:\n        ordering = [\"name\"]\n        verbose_name = \"Git repository\"\n        verbose_name_plural = \"Git repositories\"\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n\n        # Store the initial repo slug so we can check for changes on save().\n        self.__initial_slug = self.slug\n\n    def __str__(self):\n        return self.name\n\n    def clean(self):\n        super().clean()\n\n        if self.slug != \"\":\n            check_if_key_is_graphql_safe(self.__class__.__name__, self.slug, \"slug\")\n\n            if self.present_in_database and self.slug != self.__initial_slug:\n                raise ValidationError(\n                    f\"Slug cannot be changed once set. Current slug is {self.__initial_slug}, \"\n                    f\"requested slug is {self.slug}\"\n                )\n\n    def get_latest_sync(self):\n        \"\"\"\n        Return a `JobResult` for the latest sync operation.\n\n        Returns:\n            JobResult\n        \"\"\"\n        from nautobot.extras.models import JobResult\n\n        # This will match all \"GitRepository\" jobs (pull/refresh, dry-run, etc.)\n        prefix = \"nautobot.core.jobs.GitRepository\"\n        return JobResult.objects.filter(task_name__startswith=prefix, task_kwargs__repository=self.pk).latest()\n\n    def to_csv(self):\n        return (\n            self.name,\n            self.slug,\n            self.remote_url,\n            self.branch,\n            self.secrets_group.name if self.secrets_group else None,\n            self.provided_contents,\n        )\n\n    @property\n    def filesystem_path(self):\n        return os.path.join(settings.GIT_ROOT, self.slug)\n\n    def sync(self, user, dry_run=False):\n        \"\"\"\n        Enqueue a Job to pull the Git repository from the remote and return the sync result.\n\n        Args:\n            user (User): The User that will perform the sync.\n            dry_run (bool): If set, dry-run the Git sync.\n\n        Returns:\n            JobResult\n        \"\"\"\n        from nautobot.extras.datasources import (\n            enqueue_pull_git_repository_and_refresh_data,\n            enqueue_git_repository_diff_origin_and_local,\n        )\n\n        if dry_run:\n            return enqueue_git_repository_diff_origin_and_local(self, user)\n        return enqueue_pull_git_repository_and_refresh_data(self, user)\n", "path": "nautobot/extras/models/datasources.py"}], "after_files": [{"content": "\"\"\"Models for representing external data sources.\"\"\"\nfrom importlib.util import find_spec\nimport os\n\nfrom django.conf import settings\nfrom django.core.exceptions import ValidationError\nfrom django.core.serializers.json import DjangoJSONEncoder\nfrom django.core.validators import URLValidator\nfrom django.db import models\n\nfrom nautobot.core.models.fields import AutoSlugField, slugify_dashes_to_underscores\nfrom nautobot.core.models.generics import PrimaryModel\nfrom nautobot.extras.utils import extras_features, check_if_key_is_graphql_safe\n\n\n@extras_features(\n    \"config_context_owners\",\n    \"export_template_owners\",\n    \"job_results\",\n    \"webhooks\",\n)\nclass GitRepository(PrimaryModel):\n    \"\"\"Representation of a Git repository used as an external data source.\"\"\"\n\n    name = models.CharField(\n        max_length=100,\n        unique=True,\n    )\n    slug = AutoSlugField(\n        populate_from=\"name\",\n        help_text=\"Internal field name. Please use underscores rather than dashes in this key.\",\n        slugify_function=slugify_dashes_to_underscores,\n    )\n\n    remote_url = models.URLField(\n        max_length=255,\n        # For the moment we don't support ssh:// and git:// URLs\n        help_text=\"Only HTTP and HTTPS URLs are presently supported\",\n        validators=[URLValidator(schemes=[\"http\", \"https\"])],\n    )\n    branch = models.CharField(\n        max_length=64,\n        default=\"main\",\n    )\n\n    current_head = models.CharField(\n        help_text=\"Commit hash of the most recent fetch from the selected branch. Used for syncing between workers.\",\n        max_length=48,\n        default=\"\",\n        blank=True,\n    )\n\n    secrets_group = models.ForeignKey(\n        to=\"extras.SecretsGroup\",\n        on_delete=models.SET_NULL,\n        default=None,\n        blank=True,\n        null=True,\n        related_name=\"git_repositories\",\n    )\n\n    # Data content types that this repo is a source of. Valid options are dynamically generated based on\n    # the data types registered in registry['datasource_contents'].\n    provided_contents = models.JSONField(encoder=DjangoJSONEncoder, default=list, blank=True)\n\n    clone_fields = [\"remote_url\", \"secrets_group\", \"provided_contents\"]\n\n    class Meta:\n        ordering = [\"name\"]\n        verbose_name = \"Git repository\"\n        verbose_name_plural = \"Git repositories\"\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n\n        # Store the initial repo slug so we can check for changes on save().\n        self.__initial_slug = self.slug\n\n    def __str__(self):\n        return self.name\n\n    def clean(self):\n        super().clean()\n\n        # Autogenerate slug now, rather than in pre_save(), if not set already, as we need to check it below.\n        if self.slug == \"\":\n            self._meta.get_field(\"slug\").create_slug(self, add=(not self.present_in_database))\n\n        if self.present_in_database and self.slug != self.__initial_slug:\n            raise ValidationError(\n                f\"Slug cannot be changed once set. Current slug is {self.__initial_slug}, \"\n                f\"requested slug is {self.slug}\"\n            )\n\n        if not self.present_in_database:\n            check_if_key_is_graphql_safe(self.__class__.__name__, self.slug, \"slug\")\n            # Check on create whether the proposed slug conflicts with a module name already in the Python environment.\n            # Because we add GIT_ROOT to the end of sys.path, trying to import this repository will instead\n            # import the earlier-found Python module in its place, which would be undesirable.\n            if find_spec(self.slug) is not None:\n                raise ValidationError(\n                    f'Please choose a different slug, as \"{self.slug}\" is an installed Python package or module.'\n                )\n\n    def get_latest_sync(self):\n        \"\"\"\n        Return a `JobResult` for the latest sync operation.\n\n        Returns:\n            JobResult\n        \"\"\"\n        from nautobot.extras.models import JobResult\n\n        # This will match all \"GitRepository\" jobs (pull/refresh, dry-run, etc.)\n        prefix = \"nautobot.core.jobs.GitRepository\"\n        return JobResult.objects.filter(task_name__startswith=prefix, task_kwargs__repository=self.pk).latest()\n\n    def to_csv(self):\n        return (\n            self.name,\n            self.slug,\n            self.remote_url,\n            self.branch,\n            self.secrets_group.name if self.secrets_group else None,\n            self.provided_contents,\n        )\n\n    @property\n    def filesystem_path(self):\n        return os.path.join(settings.GIT_ROOT, self.slug)\n\n    def sync(self, user, dry_run=False):\n        \"\"\"\n        Enqueue a Job to pull the Git repository from the remote and return the sync result.\n\n        Args:\n            user (User): The User that will perform the sync.\n            dry_run (bool): If set, dry-run the Git sync.\n\n        Returns:\n            JobResult\n        \"\"\"\n        from nautobot.extras.datasources import (\n            enqueue_pull_git_repository_and_refresh_data,\n            enqueue_git_repository_diff_origin_and_local,\n        )\n\n        if dry_run:\n            return enqueue_git_repository_diff_origin_and_local(self, user)\n        return enqueue_pull_git_repository_and_refresh_data(self, user)\n", "path": "nautobot/extras/models/datasources.py"}]}