Skip to content

Commit dc6a54e

Browse files
committed
Add filtering of Script objects based on object permissions with custom constraints
1 parent 586bc13 commit dc6a54e

File tree

3 files changed

+97
-71
lines changed

3 files changed

+97
-71
lines changed

docs/customization/custom-scripts.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,17 @@ They can also be used as a mechanism for validating the integrity of data within
1818
Custom scripts are Python code which exists outside the NetBox code base, so they can be updated and changed without interfering with the core NetBox installation. And because they're completely custom, there is no inherent limitation on what a script can accomplish.
1919

2020
!!! danger "Only install trusted scripts"
21-
Custom scripts have unrestricted access to change anything in the databse and are inherently unsafe and should only be installed and run from trusted sources. You should also review and set permissions for who can run scripts if the script can modify any data.
21+
Custom scripts have unrestricted access to change anything in the database and are inherently unsafe and should only be installed and run from trusted sources. You should also review and set permissions for who can run scripts if the script can modify any data.
22+
23+
!!! tip "Permissions for Custom Scripts"
24+
A user can be granted permissions on all Custom Scripts via the "Managed File" object-level permission. To further restrict a user to only be able to access certain scripts, create an additional permission on the "Script" object type, with appropriate queryset-style constraints matching fields available on Script. For example:
25+
```json
26+
{
27+
"name__in": [
28+
"MyScript"
29+
]
30+
}
31+
```
2232

2333
## Writing Custom Scripts
2434

netbox/extras/views.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,11 @@
2424
from netbox.object_actions import *
2525
from netbox.views import generic
2626
from netbox.views.generic.mixins import TableMixin
27+
from users.models import ObjectPermission
2728
from utilities.forms import ConfirmationForm, get_field_value
2829
from utilities.htmx import htmx_partial, htmx_maybe_redirect_current_page
2930
from utilities.paginator import EnhancedPaginator, get_paginate_count
31+
from utilities.permissions import qs_filter_from_constraints
3032
from utilities.query import count_related
3133
from utilities.querydict import normalize_querydict
3234
from utilities.request import copy_safe_request
@@ -1441,12 +1443,24 @@ def get_required_permission(self):
14411443
return 'extras.view_script'
14421444

14431445
def get(self, request):
1446+
# Permissions for the Scripts page are given via the "Managed File" object permission. To further restrict
1447+
# users to access only specified scripts, create permissions on the "Script" object with appropriate
1448+
# queryset-style constraints matching fields available on Script.
14441449
script_modules = ScriptModule.objects.restrict(request.user).prefetch_related(
14451450
'data_source', 'data_file', 'jobs'
14461451
)
1452+
script_ct = ContentType.objects.get_for_model(Script)
1453+
script_permissions = qs_filter_from_constraints(
1454+
ObjectPermission.objects.filter(
1455+
users=self.request.user, object_types=script_ct
1456+
).values_list("constraints", flat=True)
1457+
)
1458+
available_scripts = Script.objects.filter(script_permissions, module__in=script_modules)
1459+
14471460
context = {
14481461
'model': ScriptModule,
14491462
'script_modules': script_modules,
1463+
'available_scripts': available_scripts,
14501464
}
14511465

14521466
# Use partial template for dashboard widgets

netbox/templates/extras/inc/script_list_content.html

Lines changed: 72 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -38,81 +38,83 @@ <h2 class="card-header" id="module{{ module.pk }}">
3838
</thead>
3939
<tbody>
4040
{% for script in scripts %}
41-
{% with last_job=script.get_latest_jobs|first %}
42-
<tr>
43-
<td>
44-
{% if script.is_executable %}
45-
<a href="{% url 'extras:script' script.pk %}" id="{{ script.module }}.{{ script.class_name }}">{{ script.python_class.name }}</a>
46-
{% else %}
47-
<a href="{% url 'extras:script_jobs' script.pk %}" id="{{ script.module }}.{{ script.class_name }}">{{ script.python_class.name }}</a>
48-
<span class="text-danger">
49-
<i class="mdi mdi-alert" title="{% trans "Script is no longer present in the source file" %}"></i>
50-
</span>
51-
{% endif %}
52-
</td>
53-
<td>{{ script.python_class.description|markdown|placeholder }}</td>
54-
{% if last_job %}
41+
{% if script in available_scripts %}
42+
{% with last_job=script.get_latest_jobs|first %}
43+
<tr>
5544
<td>
56-
<a href="{% url 'extras:script_result' job_pk=last_job.pk %}">{{ last_job.created|isodatetime }}</a>
45+
{% if script.is_executable %}
46+
<a href="{% url 'extras:script' script.pk %}" id="{{ script.module }}.{{ script.class_name }}">{{ script.python_class.name }}</a>
47+
{% else %}
48+
<a href="{% url 'extras:script_jobs' script.pk %}" id="{{ script.module }}.{{ script.class_name }}">{{ script.python_class.name }}</a>
49+
<span class="text-danger">
50+
<i class="mdi mdi-alert" title="{% trans "Script is no longer present in the source file" %}"></i>
51+
</span>
52+
{% endif %}
5753
</td>
54+
<td>{{ script.python_class.description|markdown|placeholder }}</td>
55+
{% if last_job %}
56+
<td>
57+
<a href="{% url 'extras:script_result' job_pk=last_job.pk %}">{{ last_job.created|isodatetime }}</a>
58+
</td>
59+
<td>
60+
{% badge last_job.get_status_display last_job.get_status_color %}
61+
</td>
62+
{% else %}
63+
<td class="text-muted">{% trans "Never" %}</td>
64+
<td>{{ ''|placeholder }}</td>
65+
{% endif %}
5866
<td>
59-
{% badge last_job.get_status_display last_job.get_status_color %}
67+
{% if request.user|can_run:script and script.is_executable %}
68+
<div class="float-end d-print-none">
69+
<form action="{% url 'extras:script' script.pk %}" method="post">
70+
{% if script.python_class.commit_default %}
71+
<input type="checkbox" name="_commit" hidden checked>
72+
{% endif %}
73+
{% csrf_token %}
74+
<button type="submit" name="_run" class="btn btn-primary{% if embedded %} btn-sm{% endif %}">
75+
{% if last_job %}
76+
<i class="mdi mdi-replay"></i> {% if not embedded %}{% trans "Run Again" %}{% endif %}
77+
{% else %}
78+
<i class="mdi mdi-play"></i> {% if not embedded %}{% trans "Run Script" %}{% endif %}
79+
{% endif %}
80+
</button>
81+
</form>
82+
</div>
83+
{% endif %}
6084
</td>
61-
{% else %}
62-
<td class="text-muted">{% trans "Never" %}</td>
63-
<td>{{ ''|placeholder }}</td>
85+
</tr>
86+
{% if last_job and not embedded %}
87+
{% for test_name, data in last_job.data.tests.items %}
88+
<tr>
89+
<td colspan="4" class="method">
90+
<span class="ps-3">{{ test_name }}</span>
91+
</td>
92+
<td class="text-end text-nowrap script-stats">
93+
<span class="badge text-bg-success">{{ data.success }}</span>
94+
<span class="badge text-bg-info">{{ data.info }}</span>
95+
<span class="badge text-bg-warning">{{ data.warning }}</span>
96+
<span class="badge text-bg-danger">{{ data.failure }}</span>
97+
</td>
98+
</tr>
99+
{% endfor %}
100+
{% elif last_job and not last_job.data.log and not embedded %}
101+
{# legacy #}
102+
{% for method, stats in last_job.data.items %}
103+
<tr>
104+
<td colspan="4" class="method">
105+
<span class="ps-3">{{ method }}</span>
106+
</td>
107+
<td class="text-end text-nowrap report-stats">
108+
<span class="badge bg-success">{{ stats.success }}</span>
109+
<span class="badge bg-info">{{ stats.info }}</span>
110+
<span class="badge bg-warning">{{ stats.warning }}</span>
111+
<span class="badge bg-danger">{{ stats.failure }}</span>
112+
</td>
113+
</tr>
114+
{% endfor %}
64115
{% endif %}
65-
<td>
66-
{% if request.user|can_run:script and script.is_executable %}
67-
<div class="float-end d-print-none">
68-
<form action="{% url 'extras:script' script.pk %}" method="post">
69-
{% if script.python_class.commit_default %}
70-
<input type="checkbox" name="_commit" hidden checked>
71-
{% endif %}
72-
{% csrf_token %}
73-
<button type="submit" name="_run" class="btn btn-primary{% if embedded %} btn-sm{% endif %}">
74-
{% if last_job %}
75-
<i class="mdi mdi-replay"></i> {% if not embedded %}{% trans "Run Again" %}{% endif %}
76-
{% else %}
77-
<i class="mdi mdi-play"></i> {% if not embedded %}{% trans "Run Script" %}{% endif %}
78-
{% endif %}
79-
</button>
80-
</form>
81-
</div>
82-
{% endif %}
83-
</td>
84-
</tr>
85-
{% if last_job and not embedded %}
86-
{% for test_name, data in last_job.data.tests.items %}
87-
<tr>
88-
<td colspan="4" class="method">
89-
<span class="ps-3">{{ test_name }}</span>
90-
</td>
91-
<td class="text-end text-nowrap script-stats">
92-
<span class="badge text-bg-success">{{ data.success }}</span>
93-
<span class="badge text-bg-info">{{ data.info }}</span>
94-
<span class="badge text-bg-warning">{{ data.warning }}</span>
95-
<span class="badge text-bg-danger">{{ data.failure }}</span>
96-
</td>
97-
</tr>
98-
{% endfor %}
99-
{% elif last_job and not last_job.data.log and not embedded %}
100-
{# legacy #}
101-
{% for method, stats in last_job.data.items %}
102-
<tr>
103-
<td colspan="4" class="method">
104-
<span class="ps-3">{{ method }}</span>
105-
</td>
106-
<td class="text-end text-nowrap report-stats">
107-
<span class="badge bg-success">{{ stats.success }}</span>
108-
<span class="badge bg-info">{{ stats.info }}</span>
109-
<span class="badge bg-warning">{{ stats.warning }}</span>
110-
<span class="badge bg-danger">{{ stats.failure }}</span>
111-
</td>
112-
</tr>
113-
{% endfor %}
114-
{% endif %}
115-
{% endwith %}
116+
{% endwith %}
117+
{% endif %}
116118
{% endfor %}
117119
</tbody>
118120
</table>

0 commit comments

Comments
 (0)