From 274bbfa22f6df16ad1e2626a51c3422a3d4a27a6 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 22 Feb 2024 13:54:14 -0500 Subject: [PATCH] Fix support for object-based permissions --- netbox/extras/views.py | 129 +++++++---------------- netbox/templates/extras/script.html | 13 ++- netbox/templates/extras/script/base.html | 2 +- netbox/templates/extras/script_list.html | 6 +- netbox/users/forms/model_forms.py | 5 - netbox/utilities/templatetags/perms.py | 6 ++ 6 files changed, 59 insertions(+), 102 deletions(-) diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 22da5c44b..56840f7a9 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -1038,63 +1038,29 @@ class ScriptListView(ContentTypePermissionRequiredMixin, View): }) -def get_script_module(module, request): - return get_object_or_404(ScriptModule.objects.restrict(request.user), file_path__regex=f"^{module}\\.") - - -class BaseScriptView(generic.ObjectView): +class ScriptView(generic.ObjectView): queryset = Script.objects.all() - script = None - script_class = None - jobs = None - def _init_vars(self, request): - if self.script.python_class: - self.script_class = self.script.python_class() - else: - self.script.delete(soft_delete=True) - messages.error(request, _("Script class has been deleted from module: ") + str(self.script.module)) - if not self.script.id: - return redirect('extras:script_list') - else: - return redirect('extras:script_jobs', pk=self.script.id) - - self.jobs = self.script.jobs.all() - return None - - def init_vars_or_redirect(self, request, pk): - self.script = get_object_or_404(Script.objects.all(), pk=pk) - return self._init_vars(request) - - -class ScriptView(BaseScriptView): - - def get(self, request, pk): - if ret := self.init_vars_or_redirect(request, pk): - return ret - - form = None - if self.script_class: - form = self.script_class.as_form(initial=normalize_querydict(request.GET)) + def get(self, request, **kwargs): + script = self.get_object(**kwargs) + script_class = script.python_class() + form = script_class.as_form(initial=normalize_querydict(request.GET)) return render(request, 'extras/script.html', { - 'job_count': self.jobs.count(), - 'module': self.script.module, - 'script': self.script, - 'script_class': self.script_class, + 'script': script, + 'script_class': script_class, 'form': form, + 'job_count': script.jobs.count(), }) - def post(self, request, pk): - if not request.user.has_perm('extras.run_script'): + def post(self, request, **kwargs): + script = self.get_object(**kwargs) + script_class = script.python_class() + + if not request.user.has_perm('extras.run_script', obj=script): return HttpResponseForbidden() - if ret := self.init_vars_or_redirect(request, pk): - return ret - - form = None - if self.script_class: - form = self.script_class.as_form(request.POST, request.FILES) + form = script_class.as_form(request.POST, request.FILES) # Allow execution only if RQ worker process is running if not get_workers_for_queue('default'): @@ -1102,77 +1068,58 @@ class ScriptView(BaseScriptView): elif form.is_valid(): job = Job.enqueue( run_script, - instance=self.script, - name=self.script_class.class_name, + instance=script, + name=script_class.class_name, user=request.user, schedule_at=form.cleaned_data.pop('_schedule_at'), interval=form.cleaned_data.pop('_interval'), data=form.cleaned_data, request=copy_safe_request(request), - job_timeout=self.script.python_class.job_timeout, + job_timeout=script.python_class.job_timeout, commit=form.cleaned_data.pop('_commit') ) return redirect('extras:script_result', job_pk=job.pk) return render(request, 'extras/script.html', { - 'job_count': self.jobs.count(), - 'module': self.script.module, - 'script': self.script, - 'script_class': self.script_class, + 'script': script, + 'script_class': script.python_class(), 'form': form, + 'job_count': script.jobs.count(), }) -class ScriptSourceView(BaseScriptView): +class ScriptSourceView(generic.ObjectView): + queryset = Script.objects.all() - def get(self, request, pk): - if ret := self.init_vars_or_redirect(request, pk): - return ret + def get(self, request, **kwargs): + script = self.get_object(**kwargs) return render(request, 'extras/script/source.html', { - 'job_count': self.jobs.count(), - 'module': self.script.module, - 'script': self.script, - 'script_class': self.script_class, + 'script': script, + 'script_class': script.python_class(), + 'job_count': script.jobs.count(), 'tab': 'source', }) class ScriptJobsView(generic.ObjectView): queryset = Script.objects.all() - script = None - script_class = None - jobs = None - def get_required_permission(self): - return 'extras.view_script' - - def get(self, request, pk): - self.script = get_object_or_404(Script.objects.all(), pk=pk) - - if self.script.python_class: - self.script_class = self.script.python_class() - else: - self.script.delete(soft_delete=True) - if not self.script.id: - messages.error(request, _("Script class has been deleted from module: ") + str(self.script.module)) - return redirect('extras:script_list') - - self.jobs = self.script.jobs.all() + def get(self, request, **kwargs): + script = self.get_object(**kwargs) jobs_table = JobTable( - data=self.jobs, + data=script.jobs.all(), orderable=False, user=request.user ) jobs_table.configure(request) return render(request, 'extras/script/jobs.html', { - 'job_count': self.jobs.count(), - 'module': self.script.module, - 'script': self.script, + 'script': script, 'table': jobs_table, + 'job_count': script.jobs.count(), 'tab': 'jobs', }) @@ -1196,19 +1143,17 @@ class LegacyScriptRedirectView(ContentTypePermissionRequiredMixin, View): return redirect(f'{url}{path}') -class ScriptResultView(BaseScriptView): +class ScriptResultView(generic.ObjectView): + queryset = Job.objects.all() def get_required_permission(self): return 'extras.view_script' - def get(self, request, job_pk): - object_type = ContentType.objects.get_by_natural_key(app_label='extras', model='script') - job = get_object_or_404(Job.objects.all(), pk=job_pk, object_type=object_type) - - script = job.object + def get(self, request, **kwargs): + job = get_object_or_404(Job.objects.all(), pk=kwargs.get('job_pk')) context = { - 'script': script, + 'script': job.object, 'job': job, } if job.data and 'log' in job.data: diff --git a/netbox/templates/extras/script.html b/netbox/templates/extras/script.html index 074d06fe5..8a4f86579 100644 --- a/netbox/templates/extras/script.html +++ b/netbox/templates/extras/script.html @@ -2,6 +2,7 @@ {% load helpers %} {% load form_helpers %} {% load log_levels %} +{% load perms %} {% load i18n %} {% block content %} @@ -32,9 +33,17 @@ {% endif %} {% endfor %} -
+
{% trans "Cancel" %} - + {% if not request.user|can_run:script or not script.is_executable %} + + {% else %} + + {% endif %}
diff --git a/netbox/templates/extras/script/base.html b/netbox/templates/extras/script/base.html index 151c3f0ef..7220b329d 100644 --- a/netbox/templates/extras/script/base.html +++ b/netbox/templates/extras/script/base.html @@ -12,7 +12,7 @@ {% block breadcrumbs %} - + {% endblock breadcrumbs %} {% block subtitle %} diff --git a/netbox/templates/extras/script_list.html b/netbox/templates/extras/script_list.html index f54238682..dc7a168f8 100644 --- a/netbox/templates/extras/script_list.html +++ b/netbox/templates/extras/script_list.html @@ -59,7 +59,9 @@ {{ script.name }} {% else %} {{ script.name }} - + + + {% endif %} {{ script.description|markdown|placeholder }} @@ -75,7 +77,7 @@ {{ ''|placeholder }} {% endif %} - {% if perms.extras.run_script %} + {% if request.user|can_run:script and script.is_executable %}
{% csrf_token %} diff --git a/netbox/users/forms/model_forms.py b/netbox/users/forms/model_forms.py index 5da059574..8875dc7f0 100644 --- a/netbox/users/forms/model_forms.py +++ b/netbox/users/forms/model_forms.py @@ -376,11 +376,6 @@ class ObjectPermissionForm(forms.ModelForm): for ct in object_types: model = ct.model_class() - if model._meta.model_name in ['script', 'report']: - raise forms.ValidationError({ - 'constraints': _('Constraints are not supported for this object type.') - }) - try: tokens = { CONSTRAINT_TOKEN_USER: 0, # Replace token with a null user ID diff --git a/netbox/utilities/templatetags/perms.py b/netbox/utilities/templatetags/perms.py index 7e804493c..0dcbc4ee7 100644 --- a/netbox/utilities/templatetags/perms.py +++ b/netbox/utilities/templatetags/perms.py @@ -6,6 +6,7 @@ __all__ = ( 'can_add', 'can_change', 'can_delete', + 'can_run', 'can_sync', 'can_view', ) @@ -42,3 +43,8 @@ def can_delete(user, instance): @register.filter() def can_sync(user, instance): return _check_permission(user, instance, 'sync') + + +@register.filter() +def can_run(user, instance): + return _check_permission(user, instance, 'run')