Skip to content

Commit

Permalink
Release 0.19.4
Browse files Browse the repository at this point in the history
  • Loading branch information
wh1te909 committed Oct 23, 2024
2 parents efb0748 + e5c355e commit 59c880d
Show file tree
Hide file tree
Showing 23 changed files with 296 additions and 62 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ Demo database resets every hour. A lot of features are disabled for obvious reas

## Mac agent versions supported

- 64 bit Intel and Apple Silicon (M1, M2)
- 64 bit Intel and Apple Silicon (M-Series)

## Installation / Backup / Restore / Usage

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 4.2.16 on 2024-10-06 05:44

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("accounts", "0037_role_can_run_server_scripts_role_can_use_webterm"),
]

operations = [
migrations.AddField(
model_name="role",
name="can_edit_global_keystore",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="role",
name="can_view_global_keystore",
field=models.BooleanField(default=False),
),
]
2 changes: 2 additions & 0 deletions api/tacticalrmm/accounts/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ class Role(BaseAuditModel):
can_manage_customfields = models.BooleanField(default=False)
can_run_server_scripts = models.BooleanField(default=False)
can_use_webterm = models.BooleanField(default=False)
can_view_global_keystore = models.BooleanField(default=False)
can_edit_global_keystore = models.BooleanField(default=False)

# checks
can_list_checks = models.BooleanField(default=False)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Generated by Django 4.2.16 on 2024-10-05 20:39

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
("core", "0047_alter_coresettings_notify_on_warning_alerts"),
("agents", "0059_alter_agenthistory_id"),
]

operations = [
migrations.AddField(
model_name="agenthistory",
name="collector_all_output",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="agenthistory",
name="custom_field",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="history",
to="core.customfield",
),
),
migrations.AddField(
model_name="agenthistory",
name="save_to_agent_note",
field=models.BooleanField(default=False),
),
]
9 changes: 9 additions & 0 deletions api/tacticalrmm/agents/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1122,6 +1122,15 @@ class AgentHistory(models.Model):
on_delete=models.SET_NULL,
)
script_results = models.JSONField(null=True, blank=True)
custom_field = models.ForeignKey(
"core.CustomField",
null=True,
blank=True,
related_name="history",
on_delete=models.SET_NULL,
)
collector_all_output = models.BooleanField(default=False)
save_to_agent_note = models.BooleanField(default=False)

def __str__(self) -> str:
return f"{self.agent.hostname} - {self.type}"
63 changes: 62 additions & 1 deletion api/tacticalrmm/agents/tests/test_agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import os
from itertools import cycle
from typing import TYPE_CHECKING
from unittest.mock import patch
from unittest.mock import PropertyMock, patch
from zoneinfo import ZoneInfo

from django.conf import settings
Expand Down Expand Up @@ -768,6 +768,67 @@ def test_run_script(self, run_script, email_task):

self.assertEqual(Note.objects.get(agent=self.agent).note, "ok")

# test run on server
with patch("core.utils.run_server_script") as mock_run_server_script:
mock_run_server_script.return_value = ("output", "error", 1.23456789, 0)
data = {
"script": script.pk,
"output": "wait",
"args": ["arg1", "arg2"],
"timeout": 15,
"run_as_user": False,
"env_vars": ["key1=val1", "key2=val2"],
"run_on_server": True,
}

r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 200)
hist = AgentHistory.objects.filter(agent=self.agent, script=script).last()
if not hist:
raise AgentHistory.DoesNotExist

mock_run_server_script.assert_called_with(
body=script.script_body,
args=script.parse_script_args(self.agent, script.shell, data["args"]),
env_vars=script.parse_script_env_vars(
self.agent, script.shell, data["env_vars"]
),
shell=script.shell,
timeout=18,
)

expected_ret = {
"stdout": "output",
"stderr": "error",
"execution_time": "1.2346",
"retcode": 0,
}

self.assertEqual(r.data, expected_ret)

hist.refresh_from_db()
expected_script_results = {**expected_ret, "id": hist.pk}
self.assertEqual(hist.script_results, expected_script_results)

# test run on server with server scripts disabled
with patch(
"core.models.CoreSettings.server_scripts_enabled",
new_callable=PropertyMock,
) as server_scripts_enabled:
server_scripts_enabled.return_value = False

data = {
"script": script.pk,
"output": "wait",
"args": ["arg1", "arg2"],
"timeout": 15,
"run_as_user": False,
"env_vars": ["key1=val1", "key2=val2"],
"run_on_server": True,
}
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 400)

def test_get_notes(self):
url = f"{base_url}/notes/"

Expand Down
40 changes: 40 additions & 0 deletions api/tacticalrmm/agents/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -768,6 +768,10 @@ def run_script(request, agent_id):
run_as_user: bool = request.data["run_as_user"]
env_vars: list[str] = request.data["env_vars"]
req_timeout = int(request.data["timeout"]) + 3
run_on_server: bool | None = request.data.get("run_on_server")

if run_on_server and not get_core_settings().server_scripts_enabled:
return notify_error("This feature is disabled.")

AuditLog.audit_script_run(
username=request.user.username,
Expand All @@ -784,6 +788,29 @@ def run_script(request, agent_id):
)
history_pk = hist.pk

if run_on_server:
from core.utils import run_server_script

r = run_server_script(
body=script.script_body,
args=script.parse_script_args(agent, script.shell, args),
env_vars=script.parse_script_env_vars(agent, script.shell, env_vars),
shell=script.shell,
timeout=req_timeout,
)

ret = {
"stdout": r[0],
"stderr": r[1],
"execution_time": "{:.4f}".format(r[2]),
"retcode": r[3],
}

hist.script_results = {**ret, "id": history_pk}
hist.save(update_fields=["script_results"])

return Response(ret)

if output == "wait":
r = agent.run_script(
scriptpk=script.pk,
Expand Down Expand Up @@ -1008,6 +1035,16 @@ def bulk(request):
elif request.data["mode"] == "script":
script = get_object_or_404(Script, pk=request.data["script"])

# prevent API from breaking for those who haven't updated payload
try:
custom_field_pk = request.data["custom_field"]
collector_all_output = request.data["collector_all_output"]
save_to_agent_note = request.data["save_to_agent_note"]
except KeyError:
custom_field_pk = None
collector_all_output = False
save_to_agent_note = False

bulk_script_task.delay(
script_pk=script.pk,
agent_pks=agents,
Expand All @@ -1016,6 +1053,9 @@ def bulk(request):
username=request.user.username[:50],
run_as_user=request.data["run_as_user"],
env_vars=request.data["env_vars"],
custom_field_pk=custom_field_pk,
collector_all_output=collector_all_output,
save_to_agent_note=save_to_agent_note,
)

return Response(f"{script.name} will now be run on {len(agents)} agents. {ht}")
Expand Down
33 changes: 31 additions & 2 deletions api/tacticalrmm/apiv3/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from rest_framework.views import APIView

from accounts.models import User
from agents.models import Agent, AgentHistory
from agents.models import Agent, AgentHistory, Note
from agents.serializers import AgentHistorySerializer
from alerts.tasks import cache_agents_alert_template
from apiv3.utils import get_agent_config
Expand Down Expand Up @@ -40,6 +40,7 @@
AuditActionType,
AuditObjType,
CheckStatus,
CustomFieldModel,
DebugLogType,
GoArch,
MeshAgentIdent,
Expand Down Expand Up @@ -581,11 +582,39 @@ def patch(self, request, agentid, pk):
request.data["script_results"]["retcode"] = 1

hist = get_object_or_404(
AgentHistory.objects.filter(agent__agent_id=agentid), pk=pk
AgentHistory.objects.select_related("custom_field").filter(
agent__agent_id=agentid
),
pk=pk,
)
s = AgentHistorySerializer(instance=hist, data=request.data, partial=True)
s.is_valid(raise_exception=True)
s.save()

if hist.custom_field:
if hist.custom_field.model == CustomFieldModel.AGENT:
field = hist.custom_field.get_or_create_field_value(hist.agent)
elif hist.custom_field.model == CustomFieldModel.CLIENT:
field = hist.custom_field.get_or_create_field_value(hist.agent.client)
elif hist.custom_field.model == CustomFieldModel.SITE:
field = hist.custom_field.get_or_create_field_value(hist.agent.site)

r = request.data["script_results"]["stdout"]
value = (
r.strip()
if hist.collector_all_output
else r.strip().split("\n")[-1].strip()
)

field.save_to_field(value)

if hist.save_to_agent_note:
Note.objects.create(
agent=hist.agent,
user=request.user,
note=request.data["script_results"]["stdout"],
)

return Response("ok")


Expand Down
4 changes: 3 additions & 1 deletion api/tacticalrmm/checks/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -365,9 +365,11 @@ def handle_check(self, data, check: "Check", agent: "Agent"):
if len(self.history) > 15:
self.history = self.history[-15:]

update_fields.extend(["history"])
update_fields.extend(["history", "more_info"])

avg = int(mean(self.history))
txt = "Memory Usage" if check.check_type == CheckType.MEMORY else "CPU Load"
self.more_info = f"Average {txt}: {avg}%"

if check.error_threshold and avg > check.error_threshold:
self.status = CheckStatus.FAILING
Expand Down
1 change: 1 addition & 0 deletions api/tacticalrmm/clients/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ def save(self, *args, **kwargs):
old_site.alert_template != self.alert_template
or old_site.workstation_policy != self.workstation_policy
or old_site.server_policy != self.server_policy
or old_site.client != self.client
):
cache_agents_alert_template.delay()

Expand Down
8 changes: 8 additions & 0 deletions api/tacticalrmm/core/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ def has_permission(self, r, view) -> bool:
return _has_perm(r, "can_edit_core_settings")


class GlobalKeyStorePerms(permissions.BasePermission):
def has_permission(self, r, view) -> bool:
if r.method == "GET":
return _has_perm(r, "can_view_global_keystore")

return _has_perm(r, "can_edit_global_keystore")


class URLActionPerms(permissions.BasePermission):
def has_permission(self, r, view) -> bool:
if r.method in {"GET", "PATCH"}:
Expand Down
5 changes: 3 additions & 2 deletions api/tacticalrmm/core/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
CodeSignPerms,
CoreSettingsPerms,
CustomFieldPerms,
GlobalKeyStorePerms,
RunServerScriptPerms,
ServerMaintPerms,
URLActionPerms,
Expand Down Expand Up @@ -310,7 +311,7 @@ def delete(self, request):


class GetAddKeyStore(APIView):
permission_classes = [IsAuthenticated, CoreSettingsPerms]
permission_classes = [IsAuthenticated, GlobalKeyStorePerms]

def get(self, request):
keys = GlobalKVStore.objects.all()
Expand All @@ -325,7 +326,7 @@ def post(self, request):


class UpdateDeleteKeyStore(APIView):
permission_classes = [IsAuthenticated, CoreSettingsPerms]
permission_classes = [IsAuthenticated, GlobalKeyStorePerms]

def put(self, request, pk):
key = get_object_or_404(GlobalKVStore, pk=pk)
Expand Down
Loading

0 comments on commit 59c880d

Please sign in to comment.