Skip to content

Commit

Permalink
Release 0.18.0
Browse files Browse the repository at this point in the history
  • Loading branch information
wh1te909 committed Mar 27, 2024
2 parents 0b92fee + 5153940 commit 7284d9f
Show file tree
Hide file tree
Showing 76 changed files with 1,294 additions and 195 deletions.
4 changes: 2 additions & 2 deletions .devcontainer/api.dockerfile
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# pulls community scripts from git repo
FROM python:3.11.6-slim AS GET_SCRIPTS_STAGE
FROM python:3.11.8-slim AS GET_SCRIPTS_STAGE

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

FROM python:3.11.6-slim
FROM python:3.11.8-slim

ENV TACTICAL_DIR /opt/tactical
ENV TACTICAL_READY_FILE ${TACTICAL_DIR}/tmp/tactical.ready
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
name: Tests
strategy:
matrix:
python-version: ["3.11.6"]
python-version: ["3.11.8"]

steps:
- uses: actions/checkout@v4
Expand Down
20 changes: 0 additions & 20 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,6 @@
"reportGeneralTypeIssues": "none"
},
"python.analysis.typeCheckingMode": "basic",
"python.linting.enabled": true,
"python.linting.mypyEnabled": true,
"python.linting.mypyArgs": [
"--ignore-missing-imports",
"--follow-imports=silent",
"--show-column-numbers",
"--strict"
],
"python.linting.ignorePatterns": [
"**/site-packages/**/*.py",
".vscode/*.py",
"**env/**"
],
"python.formatting.provider": "none",
//"mypy.targets": [
//"api/tacticalrmm"
//],
//"mypy.runUsingActiveInterpreter": true,
"editor.bracketPairColorization.enabled": true,
"editor.guides.bracketPairs": true,
"editor.formatOnSave": true,
Expand All @@ -34,7 +16,6 @@
"**/docker/**/docker-compose*.yml": "dockercompose"
},
"files.watcherExclude": {
"files.watcherExclude": {
"**/.git/objects/**": true,
"**/.git/subtree-cache/**": true,
"**/node_modules/": true,
Expand All @@ -53,7 +34,6 @@
"**/*.parquet*": true,
"**/*.pyc": true,
"**/*.zip": true
}
},
"go.useLanguageServer": true,
"[go]": {
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Demo database resets every hour. A lot of features are disabled for obvious reas
- Teamviewer-like remote desktop control
- Real-time remote shell
- Remote file browser (download and upload files)
- Remote command and script execution (batch, powershell and python scripts)
- Remote command and script execution (batch, powershell, python, nushell and deno scripts)
- Event log viewer
- Services management
- Windows patch management
Expand Down
2 changes: 1 addition & 1 deletion ansible/roles/trmm_dev/defaults/main.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
user: "tactical"
python_ver: "3.11.6"
python_ver: "3.11.8"
go_ver: "1.20.7"
backend_repo: "https://github.com/amidaware/tacticalrmm.git"
frontend_repo: "https://github.com/amidaware/tacticalrmm-web.git"
Expand Down
1 change: 0 additions & 1 deletion ansible/roles/trmm_dev/templates/local_settings.j2
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ DATABASES = {
'PORT': '5432',
}
}
REDIS_HOST = "localhost"
ADMIN_ENABLED = True
CERT_FILE = "{{ fullchain_dest }}"
KEY_FILE = "{{ privkey_dest }}"
Expand Down
11 changes: 10 additions & 1 deletion api/tacticalrmm/accounts/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,15 @@ class User(AbstractUser, BaseAuditModel):
on_delete=models.SET_NULL,
)

@property
def mesh_user_id(self):
return f"user//{self.mesh_username}"

@property
def mesh_username(self):
# lower() needed for mesh api
return f"{self.username.lower()}___{self.pk}"

@staticmethod
def serialize(user):
# serializes the task and returns json
Expand Down Expand Up @@ -195,7 +204,7 @@ def __str__(self):
def save(self, *args, **kwargs) -> None:
# delete cache on save
cache.delete(f"{ROLE_CACHE_PREFIX}{self.name}")
super(BaseAuditModel, self).save(*args, **kwargs)
super().save(*args, **kwargs)

@staticmethod
def serialize(role):
Expand Down
6 changes: 6 additions & 0 deletions api/tacticalrmm/accounts/utils.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from typing import TYPE_CHECKING

from django.conf import settings

if TYPE_CHECKING:
from django.http import HttpRequest

from accounts.models import User


Expand All @@ -16,3 +18,7 @@ def is_root_user(*, request: "HttpRequest", user: "User") -> bool:
getattr(settings, "DEMO", False) and request.user.username == settings.ROOT_USER
)
return root or demo


def is_superuser(user: "User") -> bool:
return user.role and getattr(user.role, "is_superuser")
7 changes: 6 additions & 1 deletion api/tacticalrmm/accounts/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from rest_framework.views import APIView

from accounts.utils import is_root_user
from core.tasks import sync_mesh_perms_task
from logs.models import AuditLog
from tacticalrmm.helpers import notify_error

Expand Down Expand Up @@ -133,6 +134,7 @@ def post(self, request):
user.role = role

user.save()
sync_mesh_perms_task.delay()
return Response(user.username)


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

return Response("ok")

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

user.delete()

sync_mesh_perms_task.delay()
return Response("ok")


Expand Down Expand Up @@ -243,11 +246,13 @@ def put(self, request, pk):
serializer = RoleSerializer(instance=role, data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
sync_mesh_perms_task.delay()
return Response("Role was edited")

def delete(self, request, pk):
role = get_object_or_404(Role, pk=pk)
role.delete()
sync_mesh_perms_task.delay()
return Response("Role was removed")


Expand Down
18 changes: 18 additions & 0 deletions api/tacticalrmm/agents/migrations/0059_alter_agenthistory_id.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 4.2.10 on 2024-02-19 05:57

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("agents", "0058_alter_agent_time_zone"),
]

operations = [
migrations.AlterField(
model_name="agenthistory",
name="id",
field=models.BigAutoField(primary_key=True, serialize=False),
),
]
9 changes: 8 additions & 1 deletion api/tacticalrmm/agents/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from agents.utils import get_agent_url
from checks.models import CheckResult
from core.models import TZ_CHOICES
from core.utils import get_core_settings, send_command_with_mesh
from core.utils import _b64_to_hex, get_core_settings, send_command_with_mesh
from logs.models import BaseAuditModel, DebugLog, PendingAction
from tacticalrmm.constants import (
AGENT_STATUS_OFFLINE,
Expand Down Expand Up @@ -452,6 +452,10 @@ def serial_number(self) -> str:
except:
return ""

@property
def hex_mesh_node_id(self) -> str:
return _b64_to_hex(self.mesh_node_id)

@classmethod
def online_agents(cls, min_version: str = "") -> "List[Agent]":
if min_version:
Expand Down Expand Up @@ -610,6 +614,8 @@ def run_script(
},
"run_as_user": run_as_user,
"env_vars": parsed_env_vars,
"nushell_enable_config": settings.NUSHELL_ENABLE_CONFIG,
"deno_default_permissions": settings.DENO_DEFAULT_PERMISSIONS,
}

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

id = models.BigAutoField(primary_key=True)
agent = models.ForeignKey(
Agent,
related_name="history",
Expand Down
61 changes: 61 additions & 0 deletions api/tacticalrmm/agents/tests/test_agent_save.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
from unittest.mock import patch

from model_bakery import baker

from agents.models import Agent
from tacticalrmm.constants import AgentMonType
from tacticalrmm.test import TacticalTestCase


class AgentSaveTestCase(TacticalTestCase):
def setUp(self):
self.client1 = baker.make("clients.Client")
self.client2 = baker.make("clients.Client")
self.site1 = baker.make("clients.Site", client=self.client1)
self.site2 = baker.make("clients.Site", client=self.client2)
self.site3 = baker.make("clients.Site", client=self.client2)
self.agent = baker.make(
"agents.Agent",
site=self.site1,
monitoring_type=AgentMonType.SERVER,
)

@patch.object(Agent, "set_alert_template")
def test_set_alert_template_called_on_mon_type_change(
self, mock_set_alert_template
):
self.agent.monitoring_type = AgentMonType.WORKSTATION
self.agent.save()
mock_set_alert_template.assert_called_once()

@patch.object(Agent, "set_alert_template")
def test_set_alert_template_called_on_site_change(self, mock_set_alert_template):
self.agent.site = self.site2
self.agent.save()
mock_set_alert_template.assert_called_once()

@patch.object(Agent, "set_alert_template")
def test_set_alert_template_called_on_site_and_montype_change(
self, mock_set_alert_template
):
print(f"before: {self.agent.monitoring_type} site: {self.agent.site_id}")
self.agent.site = self.site3
self.agent.monitoring_type = AgentMonType.WORKSTATION
self.agent.save()
mock_set_alert_template.assert_called_once()
print(f"after: {self.agent.monitoring_type} site: {self.agent.site_id}")

@patch.object(Agent, "set_alert_template")
def test_set_alert_template_not_called_without_changes(
self, mock_set_alert_template
):
self.agent.save()
mock_set_alert_template.assert_not_called()

@patch.object(Agent, "set_alert_template")
def test_set_alert_template_not_called_on_non_relevant_field_change(
self, mock_set_alert_template
):
self.agent.hostname = "abc123"
self.agent.save()
mock_set_alert_template.assert_not_called()
1 change: 1 addition & 0 deletions api/tacticalrmm/agents/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
path("<agent:agent_id>/wmi/", views.WMI.as_view()),
path("<agent:agent_id>/recover/", views.recover),
path("<agent:agent_id>/reboot/", views.Reboot.as_view()),
path("<agent:agent_id>/shutdown/", views.Shutdown.as_view()),
path("<agent:agent_id>/ping/", views.ping),
# alias for checks get view
path("<agent:agent_id>/checks/", GetAddChecks.as_view()),
Expand Down
30 changes: 23 additions & 7 deletions api/tacticalrmm/agents/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from rest_framework.response import Response
from rest_framework.views import APIView

from core.tasks import sync_mesh_perms_task
from core.utils import (
get_core_settings,
get_mesh_ws_url,
Expand Down Expand Up @@ -258,6 +259,7 @@ def put(self, request, agent_id):
serializer.is_valid(raise_exception=True)
serializer.save()

sync_mesh_perms_task.delay()
return Response("The agent was updated successfully")

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


Expand Down Expand Up @@ -325,13 +328,13 @@ def get(self, request, agent_id):
agent = get_object_or_404(Agent, agent_id=agent_id)
core = get_core_settings()

if not core.mesh_disable_auto_login:
token = get_login_token(
key=core.mesh_token, user=f"user//{core.mesh_username}"
)
token_param = f"login={token}&"
else:
token_param = ""
user = (
request.user.mesh_user_id
if core.sync_mesh_with_trmm
else f"user//{core.mesh_api_superuser}"
)
token = get_login_token(key=core.mesh_token, user=user)
token_param = f"login={token}&"

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


class Shutdown(APIView):
permission_classes = [IsAuthenticated, RebootAgentPerms]

# shutdown
def post(self, request, agent_id):
agent = get_object_or_404(Agent, agent_id=agent_id)
r = asyncio.run(agent.nats_cmd({"func": "shutdown"}, timeout=10))
if r != "ok":
return notify_error("Unable to contact the agent")

return Response("ok")


class Reboot(APIView):
permission_classes = [IsAuthenticated, RebootAgentPerms]

Expand Down
4 changes: 4 additions & 0 deletions api/tacticalrmm/alerts/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -1429,6 +1429,8 @@ def test_alert_actions(
"run_as_user": False,
"env_vars": ["hello=world", "foo=bar"],
"id": AgentHistory.objects.last().pk, # type: ignore
"nushell_enable_config": settings.NUSHELL_ENABLE_CONFIG,
"deno_default_permissions": settings.DENO_DEFAULT_PERMISSIONS,
}

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

nats_cmd.assert_called_with(data, timeout=35, wait=True)
Expand Down
Loading

0 comments on commit 7284d9f

Please sign in to comment.