Skip to content

Commit 7284d9f

Browse files
committed
Release 0.18.0
2 parents 0b92fee + 5153940 commit 7284d9f

File tree

76 files changed

+1294
-195
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

76 files changed

+1294
-195
lines changed

.devcontainer/api.dockerfile

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
# pulls community scripts from git repo
2-
FROM python:3.11.6-slim AS GET_SCRIPTS_STAGE
2+
FROM python:3.11.8-slim AS GET_SCRIPTS_STAGE
33

44
RUN apt-get update && \
55
apt-get install -y --no-install-recommends git && \
66
git clone https://github.com/amidaware/community-scripts.git /community-scripts
77

8-
FROM python:3.11.6-slim
8+
FROM python:3.11.8-slim
99

1010
ENV TACTICAL_DIR /opt/tactical
1111
ENV TACTICAL_READY_FILE ${TACTICAL_DIR}/tmp/tactical.ready

.github/workflows/ci-tests.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
name: Tests
1515
strategy:
1616
matrix:
17-
python-version: ["3.11.6"]
17+
python-version: ["3.11.8"]
1818

1919
steps:
2020
- uses: actions/checkout@v4

.vscode/settings.json

-20
Original file line numberDiff line numberDiff line change
@@ -8,24 +8,6 @@
88
"reportGeneralTypeIssues": "none"
99
},
1010
"python.analysis.typeCheckingMode": "basic",
11-
"python.linting.enabled": true,
12-
"python.linting.mypyEnabled": true,
13-
"python.linting.mypyArgs": [
14-
"--ignore-missing-imports",
15-
"--follow-imports=silent",
16-
"--show-column-numbers",
17-
"--strict"
18-
],
19-
"python.linting.ignorePatterns": [
20-
"**/site-packages/**/*.py",
21-
".vscode/*.py",
22-
"**env/**"
23-
],
24-
"python.formatting.provider": "none",
25-
//"mypy.targets": [
26-
//"api/tacticalrmm"
27-
//],
28-
//"mypy.runUsingActiveInterpreter": true,
2911
"editor.bracketPairColorization.enabled": true,
3012
"editor.guides.bracketPairs": true,
3113
"editor.formatOnSave": true,
@@ -34,7 +16,6 @@
3416
"**/docker/**/docker-compose*.yml": "dockercompose"
3517
},
3618
"files.watcherExclude": {
37-
"files.watcherExclude": {
3819
"**/.git/objects/**": true,
3920
"**/.git/subtree-cache/**": true,
4021
"**/node_modules/": true,
@@ -53,7 +34,6 @@
5334
"**/*.parquet*": true,
5435
"**/*.pyc": true,
5536
"**/*.zip": true
56-
}
5737
},
5838
"go.useLanguageServer": true,
5939
"[go]": {

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ Demo database resets every hour. A lot of features are disabled for obvious reas
1919
- Teamviewer-like remote desktop control
2020
- Real-time remote shell
2121
- Remote file browser (download and upload files)
22-
- Remote command and script execution (batch, powershell and python scripts)
22+
- Remote command and script execution (batch, powershell, python, nushell and deno scripts)
2323
- Event log viewer
2424
- Services management
2525
- Windows patch management

ansible/roles/trmm_dev/defaults/main.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
user: "tactical"
3-
python_ver: "3.11.6"
3+
python_ver: "3.11.8"
44
go_ver: "1.20.7"
55
backend_repo: "https://github.com/amidaware/tacticalrmm.git"
66
frontend_repo: "https://github.com/amidaware/tacticalrmm-web.git"

ansible/roles/trmm_dev/templates/local_settings.j2

-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ DATABASES = {
1313
'PORT': '5432',
1414
}
1515
}
16-
REDIS_HOST = "localhost"
1716
ADMIN_ENABLED = True
1817
CERT_FILE = "{{ fullchain_dest }}"
1918
KEY_FILE = "{{ privkey_dest }}"

api/tacticalrmm/accounts/models.py

+10-1
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,15 @@ class User(AbstractUser, BaseAuditModel):
6464
on_delete=models.SET_NULL,
6565
)
6666

67+
@property
68+
def mesh_user_id(self):
69+
return f"user//{self.mesh_username}"
70+
71+
@property
72+
def mesh_username(self):
73+
# lower() needed for mesh api
74+
return f"{self.username.lower()}___{self.pk}"
75+
6776
@staticmethod
6877
def serialize(user):
6978
# serializes the task and returns json
@@ -195,7 +204,7 @@ def __str__(self):
195204
def save(self, *args, **kwargs) -> None:
196205
# delete cache on save
197206
cache.delete(f"{ROLE_CACHE_PREFIX}{self.name}")
198-
super(BaseAuditModel, self).save(*args, **kwargs)
207+
super().save(*args, **kwargs)
199208

200209
@staticmethod
201210
def serialize(role):

api/tacticalrmm/accounts/utils.py

+6
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
from typing import TYPE_CHECKING
2+
23
from django.conf import settings
34

45
if TYPE_CHECKING:
56
from django.http import HttpRequest
7+
68
from accounts.models import User
79

810

@@ -16,3 +18,7 @@ def is_root_user(*, request: "HttpRequest", user: "User") -> bool:
1618
getattr(settings, "DEMO", False) and request.user.username == settings.ROOT_USER
1719
)
1820
return root or demo
21+
22+
23+
def is_superuser(user: "User") -> bool:
24+
return user.role and getattr(user.role, "is_superuser")

api/tacticalrmm/accounts/views.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from rest_framework.views import APIView
1212

1313
from accounts.utils import is_root_user
14+
from core.tasks import sync_mesh_perms_task
1415
from logs.models import AuditLog
1516
from tacticalrmm.helpers import notify_error
1617

@@ -133,6 +134,7 @@ def post(self, request):
133134
user.role = role
134135

135136
user.save()
137+
sync_mesh_perms_task.delay()
136138
return Response(user.username)
137139

138140

@@ -153,6 +155,7 @@ def put(self, request, pk):
153155
serializer = UserSerializer(instance=user, data=request.data, partial=True)
154156
serializer.is_valid(raise_exception=True)
155157
serializer.save()
158+
sync_mesh_perms_task.delay()
156159

157160
return Response("ok")
158161

@@ -162,7 +165,7 @@ def delete(self, request, pk):
162165
return notify_error("The root user cannot be deleted from the UI")
163166

164167
user.delete()
165-
168+
sync_mesh_perms_task.delay()
166169
return Response("ok")
167170

168171

@@ -243,11 +246,13 @@ def put(self, request, pk):
243246
serializer = RoleSerializer(instance=role, data=request.data)
244247
serializer.is_valid(raise_exception=True)
245248
serializer.save()
249+
sync_mesh_perms_task.delay()
246250
return Response("Role was edited")
247251

248252
def delete(self, request, pk):
249253
role = get_object_or_404(Role, pk=pk)
250254
role.delete()
255+
sync_mesh_perms_task.delay()
251256
return Response("Role was removed")
252257

253258

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 4.2.10 on 2024-02-19 05:57
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("agents", "0058_alter_agent_time_zone"),
10+
]
11+
12+
operations = [
13+
migrations.AlterField(
14+
model_name="agenthistory",
15+
name="id",
16+
field=models.BigAutoField(primary_key=True, serialize=False),
17+
),
18+
]

api/tacticalrmm/agents/models.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from agents.utils import get_agent_url
2121
from checks.models import CheckResult
2222
from core.models import TZ_CHOICES
23-
from core.utils import get_core_settings, send_command_with_mesh
23+
from core.utils import _b64_to_hex, get_core_settings, send_command_with_mesh
2424
from logs.models import BaseAuditModel, DebugLog, PendingAction
2525
from tacticalrmm.constants import (
2626
AGENT_STATUS_OFFLINE,
@@ -452,6 +452,10 @@ def serial_number(self) -> str:
452452
except:
453453
return ""
454454

455+
@property
456+
def hex_mesh_node_id(self) -> str:
457+
return _b64_to_hex(self.mesh_node_id)
458+
455459
@classmethod
456460
def online_agents(cls, min_version: str = "") -> "List[Agent]":
457461
if min_version:
@@ -610,6 +614,8 @@ def run_script(
610614
},
611615
"run_as_user": run_as_user,
612616
"env_vars": parsed_env_vars,
617+
"nushell_enable_config": settings.NUSHELL_ENABLE_CONFIG,
618+
"deno_default_permissions": settings.DENO_DEFAULT_PERMISSIONS,
613619
}
614620

615621
if history_pk != 0:
@@ -1084,6 +1090,7 @@ def save_to_field(self, value: Union[List[Any], bool, str]) -> None:
10841090
class AgentHistory(models.Model):
10851091
objects = PermissionQuerySet.as_manager()
10861092

1093+
id = models.BigAutoField(primary_key=True)
10871094
agent = models.ForeignKey(
10881095
Agent,
10891096
related_name="history",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
from unittest.mock import patch
2+
3+
from model_bakery import baker
4+
5+
from agents.models import Agent
6+
from tacticalrmm.constants import AgentMonType
7+
from tacticalrmm.test import TacticalTestCase
8+
9+
10+
class AgentSaveTestCase(TacticalTestCase):
11+
def setUp(self):
12+
self.client1 = baker.make("clients.Client")
13+
self.client2 = baker.make("clients.Client")
14+
self.site1 = baker.make("clients.Site", client=self.client1)
15+
self.site2 = baker.make("clients.Site", client=self.client2)
16+
self.site3 = baker.make("clients.Site", client=self.client2)
17+
self.agent = baker.make(
18+
"agents.Agent",
19+
site=self.site1,
20+
monitoring_type=AgentMonType.SERVER,
21+
)
22+
23+
@patch.object(Agent, "set_alert_template")
24+
def test_set_alert_template_called_on_mon_type_change(
25+
self, mock_set_alert_template
26+
):
27+
self.agent.monitoring_type = AgentMonType.WORKSTATION
28+
self.agent.save()
29+
mock_set_alert_template.assert_called_once()
30+
31+
@patch.object(Agent, "set_alert_template")
32+
def test_set_alert_template_called_on_site_change(self, mock_set_alert_template):
33+
self.agent.site = self.site2
34+
self.agent.save()
35+
mock_set_alert_template.assert_called_once()
36+
37+
@patch.object(Agent, "set_alert_template")
38+
def test_set_alert_template_called_on_site_and_montype_change(
39+
self, mock_set_alert_template
40+
):
41+
print(f"before: {self.agent.monitoring_type} site: {self.agent.site_id}")
42+
self.agent.site = self.site3
43+
self.agent.monitoring_type = AgentMonType.WORKSTATION
44+
self.agent.save()
45+
mock_set_alert_template.assert_called_once()
46+
print(f"after: {self.agent.monitoring_type} site: {self.agent.site_id}")
47+
48+
@patch.object(Agent, "set_alert_template")
49+
def test_set_alert_template_not_called_without_changes(
50+
self, mock_set_alert_template
51+
):
52+
self.agent.save()
53+
mock_set_alert_template.assert_not_called()
54+
55+
@patch.object(Agent, "set_alert_template")
56+
def test_set_alert_template_not_called_on_non_relevant_field_change(
57+
self, mock_set_alert_template
58+
):
59+
self.agent.hostname = "abc123"
60+
self.agent.save()
61+
mock_set_alert_template.assert_not_called()

api/tacticalrmm/agents/urls.py

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
path("<agent:agent_id>/wmi/", views.WMI.as_view()),
1616
path("<agent:agent_id>/recover/", views.recover),
1717
path("<agent:agent_id>/reboot/", views.Reboot.as_view()),
18+
path("<agent:agent_id>/shutdown/", views.Shutdown.as_view()),
1819
path("<agent:agent_id>/ping/", views.ping),
1920
# alias for checks get view
2021
path("<agent:agent_id>/checks/", GetAddChecks.as_view()),

api/tacticalrmm/agents/views.py

+23-7
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from rest_framework.response import Response
2222
from rest_framework.views import APIView
2323

24+
from core.tasks import sync_mesh_perms_task
2425
from core.utils import (
2526
get_core_settings,
2627
get_mesh_ws_url,
@@ -258,6 +259,7 @@ def put(self, request, agent_id):
258259
serializer.is_valid(raise_exception=True)
259260
serializer.save()
260261

262+
sync_mesh_perms_task.delay()
261263
return Response("The agent was updated successfully")
262264

263265
# uninstall agent
@@ -283,6 +285,7 @@ def delete(self, request, agent_id):
283285
message=f"Unable to remove agent {name} from meshcentral database: {e}",
284286
log_type=DebugLogType.AGENT_ISSUES,
285287
)
288+
sync_mesh_perms_task.delay()
286289
return Response(f"{name} will now be uninstalled.")
287290

288291

@@ -325,13 +328,13 @@ def get(self, request, agent_id):
325328
agent = get_object_or_404(Agent, agent_id=agent_id)
326329
core = get_core_settings()
327330

328-
if not core.mesh_disable_auto_login:
329-
token = get_login_token(
330-
key=core.mesh_token, user=f"user//{core.mesh_username}"
331-
)
332-
token_param = f"login={token}&"
333-
else:
334-
token_param = ""
331+
user = (
332+
request.user.mesh_user_id
333+
if core.sync_mesh_with_trmm
334+
else f"user//{core.mesh_api_superuser}"
335+
)
336+
token = get_login_token(key=core.mesh_token, user=user)
337+
token_param = f"login={token}&"
335338

336339
control = f"{core.mesh_site}/?{token_param}gotonode={agent.mesh_node_id}&viewmode=11&hide=31"
337340
terminal = f"{core.mesh_site}/?{token_param}gotonode={agent.mesh_node_id}&viewmode=12&hide=31"
@@ -491,6 +494,19 @@ def send_raw_cmd(request, agent_id):
491494
return Response(r)
492495

493496

497+
class Shutdown(APIView):
498+
permission_classes = [IsAuthenticated, RebootAgentPerms]
499+
500+
# shutdown
501+
def post(self, request, agent_id):
502+
agent = get_object_or_404(Agent, agent_id=agent_id)
503+
r = asyncio.run(agent.nats_cmd({"func": "shutdown"}, timeout=10))
504+
if r != "ok":
505+
return notify_error("Unable to contact the agent")
506+
507+
return Response("ok")
508+
509+
494510
class Reboot(APIView):
495511
permission_classes = [IsAuthenticated, RebootAgentPerms]
496512

api/tacticalrmm/alerts/tests.py

+4
Original file line numberDiff line numberDiff line change
@@ -1429,6 +1429,8 @@ def test_alert_actions(
14291429
"run_as_user": False,
14301430
"env_vars": ["hello=world", "foo=bar"],
14311431
"id": AgentHistory.objects.last().pk, # type: ignore
1432+
"nushell_enable_config": settings.NUSHELL_ENABLE_CONFIG,
1433+
"deno_default_permissions": settings.DENO_DEFAULT_PERMISSIONS,
14321434
}
14331435

14341436
nats_cmd.assert_called_with(data, timeout=30, wait=True)
@@ -1460,6 +1462,8 @@ def test_alert_actions(
14601462
"run_as_user": False,
14611463
"env_vars": ["resolved=action", "env=vars"],
14621464
"id": AgentHistory.objects.last().pk, # type: ignore
1465+
"nushell_enable_config": settings.NUSHELL_ENABLE_CONFIG,
1466+
"deno_default_permissions": settings.DENO_DEFAULT_PERMISSIONS,
14631467
}
14641468

14651469
nats_cmd.assert_called_with(data, timeout=35, wait=True)

0 commit comments

Comments
 (0)