Compare commits

..

150 Commits

Author SHA1 Message Date
wh1te909
e76fa878d2 Release 0.18.2 2024-04-09 01:02:48 +00:00
wh1te909
376b421eb9 bump versions 2024-04-09 00:37:07 +00:00
wh1te909
e1643aca80 revert DRF for now until we do more testing 2024-04-08 23:35:48 +00:00
wh1te909
4e97c0c5c9 add note about where to find bulk output results 2024-04-08 23:27:30 +00:00
wh1te909
05b88a3c73 fix for usernames with spaces in them fixes #1820 2024-03-30 22:08:15 +00:00
wh1te909
3c087d49e9 update reqs 2024-03-30 06:32:59 +00:00
wh1te909
d81fcccf10 add guest sharing perm 2024-03-30 05:56:24 +00:00
wh1te909
ee3a7bbbfc fix run urlactions perms fixes #1819 2024-03-30 05:52:09 +00:00
wh1te909
82d9e2fb16 back to dev 2024-03-30 05:49:56 +00:00
wh1te909
6ab39d6f70 Release 0.18.1 2024-03-29 21:07:45 +00:00
wh1te909
4aa413e697 bump version 2024-03-29 21:07:33 +00:00
wh1te909
04b3fc54b0 add nonalpha chars to mesh password #1814 2024-03-29 20:10:35 +00:00
wh1te909
e4c5a4e886 fix rights 2024-03-29 08:39:18 +00:00
wh1te909
a0ee7a59eb remove old funcs 2024-03-29 08:36:03 +00:00
wh1te909
b4a05160df skip if no mesh node id #1814 2024-03-28 23:43:18 +00:00
Dan
1a437b3961 Merge pull request #1815 from silversword411/develop
Tweaking bug report template
2024-03-28 14:10:14 -07:00
wh1te909
bda8555190 remove lambda 2024-03-28 07:32:54 +00:00
silversword411
10ca38f91d Tweaking bug report template 2024-03-28 02:24:18 -04:00
wh1te909
a468faad20 fix lint 2024-03-28 04:30:31 +00:00
wh1te909
7a20be4aff fix for mesh sync if trmm username is an email 2024-03-28 04:18:25 +00:00
wh1te909
06b974c8a4 back to dev 2024-03-28 04:18:01 +00:00
wh1te909
7284d9fcd8 Release 0.18.0 2024-03-27 18:16:28 +00:00
wh1te909
515394049a bump version 2024-03-27 18:09:17 +00:00
wh1te909
35c8b4f535 add mgmt command to get mesh login url 2024-03-27 17:28:32 +00:00
wh1te909
1a325a66b4 bump versions 2024-03-25 17:35:44 +00:00
wh1te909
7d82116fb9 add home endpoint 2024-03-25 17:29:43 +00:00
wh1te909
8a7bd4f21b update bins 2024-03-24 19:31:12 +00:00
wh1te909
2e5a2ef12d update nats 2024-03-24 19:29:18 +00:00
wh1te909
89aceda65a update reqs 2024-03-21 18:28:59 +00:00
Dan
39fd83aa16 Merge pull request #1810 from dinger1986/dinger1986-add-mesh-coname-to-initial
Update views.py
2024-03-20 17:03:07 -07:00
dinger1986
a23d811fe8 Update tests.py 2024-03-20 23:29:33 +00:00
dinger1986
a238779724 Update tests.py 2024-03-20 23:24:51 +00:00
dinger1986
3a848bc037 Update views.py 2024-03-20 20:52:31 +00:00
wh1te909
0528ecb454 fix iter logic 2024-03-18 09:12:18 +00:00
wh1te909
141835593c ensure email always verified 2024-03-16 09:03:20 +00:00
wh1te909
3d06200368 update deno 2024-03-16 09:02:54 +00:00
wh1te909
729bef9a77 update reqs 2024-03-15 07:53:28 +00:00
wh1te909
94f33bd642 force sync in hosted 2024-03-13 02:00:54 +00:00
wh1te909
7e010cdbca nodesource added their installation scripts back 2024-03-13 01:06:07 +00:00
wh1te909
8887bcd941 disable auto login no longer needed with mesh sync 2024-03-12 05:26:40 +00:00
wh1te909
56aeeee04c add stdout 2024-03-12 05:22:32 +00:00
wh1te909
98eb3c7287 fix mgmt commands 2024-03-11 20:27:12 +00:00
wh1te909
6819c1989b move to mgmt commands 2024-03-11 19:05:20 +00:00
wh1te909
7e01dd3e97 change to run ever 2 hours 2024-03-11 16:49:02 +00:00
wh1te909
ea4f2c3de8 break sync into chunks 2024-03-10 22:29:00 +00:00
wh1te909
b2f63b8761 should have been 10mb default 2024-03-10 21:26:12 +00:00
wh1te909
65865101ce handle large requests 2024-03-10 02:05:38 +00:00
wh1te909
c3637afe69 max websocket max size customizable 2024-03-10 00:14:04 +00:00
wh1te909
ab543ddf0c add option to use own cert during install 2024-03-09 19:21:21 +00:00
wh1te909
80595e76e7 cleanup orphaned checkhistory results fixes #1789 2024-03-09 08:31:25 +00:00
wh1te909
d49e68737a update reqs 2024-03-09 08:30:53 +00:00
wh1te909
712e15ba80 just try returning str for all 2024-03-05 20:45:34 +00:00
wh1te909
986160e667 also allow accessing floats 2024-03-05 20:27:32 +00:00
wh1te909
1ae4e23db1 more sync mesh fixes 2024-03-04 10:05:45 +00:00
wh1te909
bad646141c rework mesh sync #182 2024-03-03 11:37:24 +00:00
wh1te909
7911235b68 fix serializer/tests 2024-02-29 07:53:05 +00:00
wh1te909
12dee4d14d py 3.11.8 and update reqs 2024-02-29 02:09:33 +00:00
wh1te909
cba841beb8 don't show in hosted 2024-02-29 02:07:53 +00:00
wh1te909
4e3ebf7078 remove from local settings 2024-02-29 01:56:19 +00:00
wh1te909
1c34969f64 fix redis 2024-02-25 23:42:08 +00:00
wh1te909
dc26cabacd make sure to cleanup if sync is toggled off 2024-02-25 07:17:54 +00:00
wh1te909
a7bffcd471 install by default 2024-02-25 06:41:40 +00:00
wh1te909
6ae56ac2cc increase max ws response size for instances with large agent counts 2024-02-25 02:18:40 +00:00
wh1te909
03c087020c exclude inactive users from the sync 2024-02-25 02:17:07 +00:00
wh1te909
857a1ab9c4 handle old node and add mgmt command for sync mesh 2024-02-24 23:19:03 +00:00
wh1te909
64d9530e13 fixes to sync mesh #182 2024-02-24 07:53:05 +00:00
wh1te909
5dac1efc30 sync mesh users/perms with trmm #182 2024-02-23 21:17:24 +00:00
wh1te909
18bc74bc96 match more flags 2024-02-23 18:56:23 +00:00
wh1te909
f64efc63f8 allow access to jsonfields in script vars 2024-02-23 02:48:32 +00:00
Dan
e84b897991 Merge pull request #1766 from conlan0/develop
Add agent shutdown endpoint and nats
2024-02-22 13:48:03 -08:00
wh1te909
519647ef93 exit on install if existing 2024-02-22 21:25:45 +00:00
wh1te909
f694fe00e4 allow getting pk/id 2024-02-22 21:18:50 +00:00
wh1te909
0b951f27b6 add defaults 2024-02-22 21:18:06 +00:00
wh1te909
8aa082c9df exit restore if existing install 2024-02-22 21:17:05 +00:00
wh1te909
f2c5d47bd8 add migration 2024-02-22 04:52:05 +00:00
Dan
ac7642cc15 Merge pull request #1676 from NiceGuyIT/feature/cross-platform-scripting
[Feature] Add cross site scripting
2024-02-21 20:48:24 -08:00
conlan0
8f34865dab Add shutdown url 2024-02-21 21:29:53 -05:00
conlan0
c762d12a40 Add shutdown class 2024-02-21 21:29:29 -05:00
wh1te909
fe1e71dc07 update vscode settings 2024-02-21 17:34:44 +00:00
wh1te909
85b0350ed4 update reqs 2024-02-21 17:34:32 +00:00
wh1te909
a980491455 update reqs 2024-02-20 22:28:10 +00:00
wh1te909
5798c0ccaa wrong branch 2024-02-20 22:22:16 +00:00
wh1te909
742f49ca1f update reqs 2024-02-19 06:06:57 +00:00
wh1te909
5560fc805b switch to bigint for pk 2024-02-19 06:01:01 +00:00
wh1te909
9d4f8a4e8c update reqs 2024-02-09 17:39:55 +00:00
wh1te909
b4d25d6285 revert, prevent recursion 2024-02-09 17:31:59 +00:00
wh1te909
a504a376bd avoid db call and add test 2024-02-09 16:59:50 +00:00
wh1te909
f61ea6e90a fix super calls 2024-02-09 16:58:59 +00:00
wh1te909
b2651df36f wrong model, and don't need to pass class 2024-02-09 16:49:52 +00:00
wh1te909
b56c086841 back to dev [skip ci] 2024-02-06 06:46:32 +00:00
wh1te909
0b92fee42e Release 0.17.5 2024-02-06 06:42:28 +00:00
wh1te909
4343478c7b bump version 2024-02-06 06:41:51 +00:00
wh1te909
94649cbfc7 handle localhost bind issues on some instances 2024-02-06 06:19:57 +00:00
wh1te909
fb83f84d84 back to dev [skip ci] 2024-02-06 04:20:28 +00:00
wh1te909
e099a5a32e Release 0.17.4 2024-02-05 17:32:42 +00:00
wh1te909
84c2632d40 bump versions 2024-02-05 09:06:15 +00:00
wh1te909
3417ee25eb update reqs 2024-02-03 06:15:44 +00:00
wh1te909
6ada30102c bump web ver [skip ci] 2024-02-02 01:15:27 +00:00
wh1te909
ac86ca7266 forgot to add year 2024-02-01 17:15:47 +00:00
wh1te909
bb1d3edf71 make workers consistent with standard install [skip ci] 2024-01-30 19:05:17 +00:00
wh1te909
97b9253017 handle alert template when montype/site changes fixes #1733 2024-01-30 08:56:44 +00:00
wh1te909
971c2180c9 update mesh [skip ci] 2024-01-28 03:54:45 +00:00
wh1te909
f96dc6991e feat: hide custom fields in summary tab only closes #1745 2024-01-28 03:24:47 +00:00
wh1te909
6855493b2f feat: add serial number to linux/mac #1683 2024-01-27 02:54:26 +00:00
wh1te909
ff0d1f7c42 feat: show cpu cores/threads in summary tab closes #1715 2024-01-27 01:32:09 +00:00
wh1te909
3ae5824761 internal only now 2024-01-26 20:55:32 +00:00
wh1te909
702e865715 format 2024-01-26 20:55:08 +00:00
wh1te909
6bcf64c83f fix func 2024-01-26 19:35:33 +00:00
wh1te909
18b270c9d0 fixes to nats rework and add tests 2024-01-26 19:19:38 +00:00
wh1te909
783376acb0 node 20 2024-01-26 18:35:39 +00:00
wh1te909
81dab470d2 blacked 2024-01-26 07:38:52 +00:00
wh1te909
a12f0feb66 rework nats 2024-01-26 07:26:50 +00:00
wh1te909
d3c99d9c1c update bins 2024-01-26 07:09:00 +00:00
wh1te909
3eb3586c0f ioutil is deprecated 2024-01-26 07:08:18 +00:00
wh1te909
fdde16cf56 feat: add from name to email closes #1726 2024-01-26 00:39:45 +00:00
wh1te909
b8bc5596fd feat: add time and ret code to script test #1713 2024-01-26 00:03:11 +00:00
wh1te909
47842a79c7 update reqs 2024-01-26 00:02:08 +00:00
wh1te909
391d5bc386 update nats-api 2024-01-21 03:42:02 +00:00
wh1te909
ba8561e357 update reqs 2024-01-21 03:17:13 +00:00
wh1te909
6aa1170cef fix for redis 5 2024-01-16 03:18:10 +00:00
wh1te909
6d4363e685 prep for celery 6 2024-01-16 02:53:45 +00:00
wh1te909
6b02b1e1e8 update reqs 2024-01-15 03:16:58 +00:00
wh1te909
df3e68fbaf debian repo issue #1721
(cherry picked from commit 58a5550989)
2023-12-30 01:21:11 +00:00
wh1te909
58a5550989 debian repo issue #1721 2023-12-30 01:20:40 +00:00
wh1te909
ccc9e44ace nodesource no longer installs npm on node 18
(cherry picked from commit f225c5cf9a)
2023-12-29 05:27:59 +00:00
wh1te909
f225c5cf9a nodesource no longer installs npm on node 18 2023-12-29 05:24:43 +00:00
Dan
5c62c7992c Merge pull request #1717 from alexcmatm/patch-1
Add gmail relay handling for emails
2023-12-27 17:03:30 -08:00
wh1te909
70b8f09ccb fix logic 2023-12-28 00:50:24 +00:00
Alexandra Stone
abfeafa026 Add gmail relay handling for emails
This change adds ehlo and starttls when the server hostname is smtp-relay.gmail.com and authentication is disabled.
Just sending the message and quitting isn't enough for gmail specifically.
2023-12-27 14:07:57 -07:00
wh1te909
aa029b005f back to dev [skip ci] 2023-12-24 01:36:38 +00:00
wh1te909
6cc55e8f36 Release 0.17.3 2023-12-24 01:22:06 +00:00
wh1te909
b753d2ca1e bump agent version 2023-12-24 01:10:59 +00:00
wh1te909
1e50329c9e bump version 2023-12-22 17:40:36 +00:00
wh1te909
4942811694 update reqs 2023-12-22 17:38:16 +00:00
wh1te909
59e37e0ccb also make sudo changes to restore 2023-12-22 17:38:04 +00:00
Dan
20aa86d8a9 Merge pull request #1712 from Tenebor/ubuntu-psql-fix
Ubuntu psql fix and cert folder chown
2023-12-21 12:44:51 -08:00
Tenebor
64c5ab7042 fix: chown on ssl cert
Exec chown on /etc/letsencrypt only in case of secure installation.
2023-12-21 20:51:16 +01:00
Tenebor
d210f5171a fix: use interactive shell to run psql
Using ubuntu "sudo -u postgres psql" returns a permission error
2023-12-21 16:47:39 +01:00
wh1te909
c7eee0f14d update reqs 2023-12-11 19:27:28 +00:00
wh1te909
221753b62e update hash_bucket_size 2023-12-11 18:36:08 +00:00
wh1te909
d213e4d37f vscode 2023-12-11 18:35:21 +00:00
wh1te909
f8695f21d3 back to dev 2023-12-11 18:34:15 +00:00
David Randall
4ac1030289 Fix: Unused import
Signed-off-by: David Randall <David@NiceGuyIT.biz>
2023-12-10 18:41:30 -05:00
David Randall
93c7117319 Fix: Whitespace formatting 2023-12-10 18:06:09 -05:00
David Randall
974afd92ce Merge remote-tracking branch 'upstream/develop' into feature/cross-platform-scripting 2023-12-05 19:32:55 -05:00
David Randall
eafd38d3f2 Merge branch 'feature/cross-platform-scripting' of github.com:NiceGuyIT/tacticalrmm into feature/cross-platform-scripting 2023-12-03 23:20:00 -05:00
David Randall
c4e590e7a0 Add: Server variables are opt-out by default
- Pull the Nushell and Deno versions from the server.
- Support downloading Nushell and Deno from a url (not GitHUb).
- Add support for nu config.nu and env.nu files.
- Add support for default Deno permissions.
2023-12-03 23:19:43 -05:00
David Randall
d754f3dd4c Merge branch 'develop' into feature/cross-platform-scripting 2023-11-18 20:08:49 -05:00
David Randall
f54fc9e990 Fix: Linux uninstall script
Signed-off-by: David Randall <David@NiceGuyIT.biz>
2023-11-18 19:51:59 -05:00
David Randall
1bf8ff73f8 [Feature] Add cross site scripting 2023-11-12 15:10:18 -05:00
95 changed files with 1640 additions and 342 deletions

View File

@@ -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

View File

@@ -14,11 +14,12 @@ assignees: ''
**Installation Method:**
- [ ] Standard
- [ ] Standard with `--insecure` flag at install
- [ ] Docker
**Agent Info (please complete the following information):**
- Agent version (as shown in the 'Summary' tab of the agent from web UI):
- Agent OS: [e.g. Win 10 v2004, Server 2012 R2]
- Agent OS: [e.g. Win 10 v2004, Server 2016]
**Describe the bug**
A clear and concise description of what the bug is.

View File

@@ -14,7 +14,7 @@ jobs:
name: Tests
strategy:
matrix:
python-version: ["3.11.6"]
python-version: ["3.11.8"]
steps:
- uses: actions/checkout@v4

24
.vscode/settings.json vendored
View File

@@ -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,
@@ -34,7 +16,6 @@
"**/docker/**/docker-compose*.yml": "dockercompose"
},
"files.watcherExclude": {
"files.watcherExclude": {
"**/.git/objects/**": true,
"**/.git/subtree-cache/**": true,
"**/node_modules/": true,
@@ -53,18 +34,17 @@
"**/*.parquet*": true,
"**/*.pyc": true,
"**/*.zip": true
}
},
"go.useLanguageServer": true,
"[go]": {
"editor.codeActionsOnSave": {
"source.organizeImports": false
"source.organizeImports": "never"
},
"editor.snippetSuggestions": "none"
},
"[go.mod]": {
"editor.codeActionsOnSave": {
"source.organizeImports": true
"source.organizeImports": "explicit"
}
},
"gopls": {

View File

@@ -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

View File

@@ -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"

View File

@@ -13,7 +13,7 @@ http {
server_tokens off;
tcp_nopush on;
types_hash_max_size 2048;
server_names_hash_bucket_size 64;
server_names_hash_bucket_size 256;
include /etc/nginx/mime.types;
default_type application/octet-stream;
ssl_protocols TLSv1.2 TLSv1.3;

View File

@@ -13,7 +13,6 @@ DATABASES = {
'PORT': '5432',
}
}
REDIS_HOST = "localhost"
ADMIN_ENABLED = True
CERT_FILE = "{{ fullchain_dest }}"
KEY_FILE = "{{ privkey_dest }}"

View File

@@ -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.replace(' ', '').lower()}___{self.pk}"
@staticmethod
def serialize(user):
# serializes the task and returns json
@@ -195,7 +204,7 @@ class Role(BaseAuditModel):
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):

View File

@@ -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
@@ -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")

View File

@@ -11,6 +11,7 @@ from rest_framework.response import Response
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
@@ -133,6 +134,7 @@ class GetAddUsers(APIView):
user.role = role
user.save()
sync_mesh_perms_task.delay()
return Response(user.username)
@@ -153,6 +155,7 @@ class GetUpdateDeleteUser(APIView):
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")
@@ -162,7 +165,7 @@ class GetUpdateDeleteUser(APIView):
return notify_error("The root user cannot be deleted from the UI")
user.delete()
sync_mesh_perms_task.delay()
return Response("ok")
@@ -243,11 +246,13 @@ class GetUpdateDeleteRole(APIView):
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")

View File

@@ -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),
),
]

View File

@@ -20,7 +20,7 @@ from packaging.version import Version as LooseVersion
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,
@@ -126,6 +126,22 @@ class Agent(BaseAuditModel):
def __str__(self) -> str:
return self.hostname
def save(self, *args, **kwargs):
# prevent recursion since calling set_alert_template() also calls save()
if not hasattr(self, "_processing_set_alert_template"):
self._processing_set_alert_template = False
if self.pk and not self._processing_set_alert_template:
orig = Agent.objects.get(pk=self.pk)
mon_type_changed = self.monitoring_type != orig.monitoring_type
site_changed = self.site_id != orig.site_id
if mon_type_changed or site_changed:
self._processing_set_alert_template = True
self.set_alert_template()
self._processing_set_alert_template = False
super().save(*args, **kwargs)
@property
def client(self) -> "Client":
return self.site.client
@@ -282,7 +298,20 @@ class Agent(BaseAuditModel):
try:
cpus = self.wmi_detail["cpu"]
for cpu in cpus:
ret.append([x["Name"] for x in cpu if "Name" in x][0])
name = [x["Name"] for x in cpu if "Name" in x][0]
lp, nc = "", ""
with suppress(Exception):
lp = [
x["NumberOfLogicalProcessors"]
for x in cpu
if "NumberOfCores" in x
][0]
nc = [x["NumberOfCores"] for x in cpu if "NumberOfCores" in x][0]
if lp and nc:
cpu_string = f"{name}, {nc}C/{lp}T"
else:
cpu_string = name
ret.append(cpu_string)
return ret
except:
return ["unknown cpu model"]
@@ -413,13 +442,20 @@ class Agent(BaseAuditModel):
@property
def serial_number(self) -> str:
if self.is_posix:
return ""
try:
return self.wmi_detail["serialnumber"]
except:
return ""
try:
return self.wmi_detail["bios"][0][0]["SerialNumber"]
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:
@@ -507,24 +543,32 @@ class Agent(BaseAuditModel):
)
return {
"agent_policy": self.policy
if self.policy and not self.policy.is_agent_excluded(self)
else None,
"site_policy": site_policy
if (site_policy and not site_policy.is_agent_excluded(self))
and not self.block_policy_inheritance
else None,
"client_policy": client_policy
if (client_policy and not client_policy.is_agent_excluded(self))
and not self.block_policy_inheritance
and not self.site.block_policy_inheritance
else None,
"default_policy": default_policy
if (default_policy and not default_policy.is_agent_excluded(self))
and not self.block_policy_inheritance
and not self.site.block_policy_inheritance
and not self.client.block_policy_inheritance
else None,
"agent_policy": (
self.policy
if self.policy and not self.policy.is_agent_excluded(self)
else None
),
"site_policy": (
site_policy
if (site_policy and not site_policy.is_agent_excluded(self))
and not self.block_policy_inheritance
else None
),
"client_policy": (
client_policy
if (client_policy and not client_policy.is_agent_excluded(self))
and not self.block_policy_inheritance
and not self.site.block_policy_inheritance
else None
),
"default_policy": (
default_policy
if (default_policy and not default_policy.is_agent_excluded(self))
and not self.block_policy_inheritance
and not self.site.block_policy_inheritance
and not self.client.block_policy_inheritance
else None
),
}
def check_run_interval(self) -> int:
@@ -570,6 +614,8 @@ class Agent(BaseAuditModel):
},
"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:
@@ -1044,6 +1090,7 @@ class AgentCustomField(models.Model):
class AgentHistory(models.Model):
objects = PermissionQuerySet.as_manager()
id = models.BigAutoField(primary_key=True)
agent = models.ForeignKey(
Agent,
related_name="history",

View File

@@ -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()

View File

@@ -15,6 +15,7 @@ urlpatterns = [
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()),

View File

@@ -21,6 +21,7 @@ from rest_framework.permissions import IsAuthenticated
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,
@@ -258,6 +259,7 @@ class GetUpdateDeleteAgent(APIView):
serializer.is_valid(raise_exception=True)
serializer.save()
sync_mesh_perms_task.delay()
return Response("The agent was updated successfully")
# uninstall agent
@@ -283,6 +285,7 @@ class GetUpdateDeleteAgent(APIView):
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.")
@@ -325,13 +328,13 @@ class AgentMeshCentral(APIView):
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"
@@ -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]
@@ -971,6 +987,8 @@ def bulk(request):
debug_info={"ip": request._client_ip},
)
ht = "Check the History tab on the agent to view the results."
if request.data["mode"] == "command":
if request.data["shell"] == "custom" and request.data["custom_shell"]:
shell = request.data["custom_shell"]
@@ -985,7 +1003,7 @@ def bulk(request):
username=request.user.username[:50],
run_as_user=request.data["run_as_user"],
)
return Response(f"Command will now be run on {len(agents)} agents")
return Response(f"Command will now be run on {len(agents)} agents. {ht}")
elif request.data["mode"] == "script":
script = get_object_or_404(Script, pk=request.data["script"])
@@ -1000,7 +1018,7 @@ def bulk(request):
env_vars=request.data["env_vars"],
)
return Response(f"{script.name} will now be run on {len(agents)} agents")
return Response(f"{script.name} will now be run on {len(agents)} agents. {ht}")
elif request.data["mode"] == "patch":
if request.data["patchMode"] == "install":

View File

@@ -169,15 +169,17 @@ class Alert(models.Model):
assigned_check=check,
agent=agent,
alert_type=AlertType.CHECK,
severity=check.alert_severity
if check.check_type
not in {
CheckType.MEMORY,
CheckType.CPU_LOAD,
CheckType.DISK_SPACE,
CheckType.SCRIPT,
}
else alert_severity,
severity=(
check.alert_severity
if check.check_type
not in {
CheckType.MEMORY,
CheckType.CPU_LOAD,
CheckType.DISK_SPACE,
CheckType.SCRIPT,
}
else alert_severity
),
message=f"{agent.hostname} has a {check.check_type} check: {check.readable_desc} that failed.",
hidden=True,
),

View File

@@ -1429,6 +1429,8 @@ class TestAlertTasks(TacticalTestCase):
"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)
@@ -1460,6 +1462,8 @@ class TestAlertTasks(TacticalTestCase):
"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)

View File

@@ -22,4 +22,12 @@ def get_agent_config() -> AgentCheckInConfig:
*getattr(settings, "CHECKIN_SYNCMESH", (800, 1200))
),
limit_data=getattr(settings, "LIMIT_DATA", False),
install_nushell=getattr(settings, "INSTALL_NUSHELL", False),
install_nushell_version=getattr(settings, "INSTALL_NUSHELL_VERSION", ""),
install_nushell_url=getattr(settings, "INSTALL_NUSHELL_URL", ""),
nushell_enable_config=getattr(settings, "NUSHELL_ENABLE_CONFIG", False),
install_deno=getattr(settings, "INSTALL_DENO", False),
install_deno_version=getattr(settings, "INSTALL_DENO_VERSION", ""),
install_deno_url=getattr(settings, "INSTALL_DENO_URL", ""),
deno_default_permissions=getattr(settings, "DENO_DEFAULT_PERMISSIONS", ""),
)

View File

@@ -20,6 +20,7 @@ from autotasks.serializers import TaskGOGetSerializer, TaskResultSerializer
from checks.constants import CHECK_DEFER, CHECK_RESULT_DEFER
from checks.models import Check, CheckResult
from checks.serializers import CheckRunnerGetSerializer
from core.tasks import sync_mesh_perms_task
from core.utils import (
download_mesh_agent,
get_core_settings,
@@ -31,6 +32,8 @@ from logs.models import DebugLog, PendingAction
from software.models import InstalledSoftware
from tacticalrmm.constants import (
AGENT_DEFER,
TRMM_MAX_REQUEST_SIZE,
AgentHistoryType,
AgentMonType,
AgentPlat,
AuditActionType,
@@ -338,6 +341,12 @@ class TaskRunner(APIView):
AutomatedTask.objects.select_related("custom_field"), pk=pk
)
content_length = request.META.get("CONTENT_LENGTH")
if content_length and int(content_length) > TRMM_MAX_REQUEST_SIZE:
request.data["stdout"] = ""
request.data["stderr"] = "Content truncated due to excessive request size."
request.data["retcode"] = 1
# get task result or create if doesn't exist
try:
task_result = (
@@ -356,7 +365,7 @@ class TaskRunner(APIView):
AgentHistory.objects.create(
agent=agent,
type=AuditActionType.TASK_RUN,
type=AgentHistoryType.TASK_RUN,
command=task.name,
script_results=request.data,
)
@@ -481,6 +490,7 @@ class NewAgent(APIView):
)
ret = {"pk": agent.pk, "token": token.key}
sync_mesh_perms_task.delay()
return Response(ret)
@@ -559,6 +569,15 @@ class AgentHistoryResult(APIView):
permission_classes = [IsAuthenticated]
def patch(self, request, agentid, pk):
content_length = request.META.get("CONTENT_LENGTH")
if content_length and int(content_length) > TRMM_MAX_REQUEST_SIZE:
request.data["script_results"]["stdout"] = ""
request.data["script_results"][
"stderr"
] = "Content truncated due to excessive request size."
request.data["script_results"]["retcode"] = 1
hist = get_object_or_404(
AgentHistory.objects.filter(agent__agent_id=agentid), pk=pk
)

View File

@@ -47,7 +47,7 @@ class Policy(BaseAuditModel):
old_policy: Optional[Policy] = (
type(self).objects.get(pk=self.pk) if self.pk else None
)
super(Policy, self).save(old_model=old_policy, *args, **kwargs)
super().save(old_model=old_policy, *args, **kwargs)
# check if alert template was changes and cache on agents
if old_policy:
@@ -68,10 +68,7 @@ class Policy(BaseAuditModel):
cache.delete_many_pattern("site_server_*")
cache.delete_many_pattern("agent_*")
super(Policy, self).delete(
*args,
**kwargs,
)
super().delete(*args, **kwargs)
def __str__(self) -> str:
return self.name

View File

@@ -126,7 +126,7 @@ class TestPolicyViews(TacticalTestCase):
resp = self.client.put(url, data, format="json")
self.assertEqual(resp.status_code, 200)
cache_alert_template.called_once()
cache_alert_template.assert_called_once()
self.check_not_authenticated("put", url)

View File

@@ -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 = [
("autotasks", "0039_alter_automatedtask_task_type"),
]
operations = [
migrations.AlterField(
model_name="taskresult",
name="id",
field=models.BigAutoField(primary_key=True, serialize=False),
),
]

View File

@@ -151,7 +151,7 @@ class AutomatedTask(BaseAuditModel):
# get old task if exists
old_task = AutomatedTask.objects.get(pk=self.pk) if self.pk else None
super(AutomatedTask, self).save(old_model=old_task, *args, **kwargs)
super().save(old_model=old_task, *args, **kwargs)
# check if fields were updated that require a sync to the agent and set status to notsynced
if old_task:
@@ -174,10 +174,7 @@ class AutomatedTask(BaseAuditModel):
cache.delete_many_pattern("site_*_tasks")
cache.delete_many_pattern("agent_*_tasks")
super(AutomatedTask, self).delete(
*args,
**kwargs,
)
super().delete(*args, **kwargs)
@property
def schedule(self) -> Optional[str]:
@@ -248,16 +245,20 @@ class AutomatedTask(BaseAuditModel):
"name": self.win_task_name,
"overwrite_task": True,
"enabled": self.enabled,
"trigger": self.task_type
if self.task_type != TaskType.CHECK_FAILURE
else TaskType.MANUAL,
"trigger": (
self.task_type
if self.task_type != TaskType.CHECK_FAILURE
else TaskType.MANUAL
),
"multiple_instances": self.task_instance_policy or 0,
"delete_expired_task_after": self.remove_if_not_scheduled
if self.expire_date
else False,
"start_when_available": self.run_asap_after_missed
if self.task_type != TaskType.RUN_ONCE
else True,
"delete_expired_task_after": (
self.remove_if_not_scheduled if self.expire_date else False
),
"start_when_available": (
self.run_asap_after_missed
if self.task_type != TaskType.RUN_ONCE
else True
),
}
if self.task_type in (
@@ -466,6 +467,7 @@ class TaskResult(models.Model):
objects = PermissionQuerySet.as_manager()
id = models.BigAutoField(primary_key=True)
agent = models.ForeignKey(
"agents.Agent",
related_name="taskresults",

View File

@@ -2,6 +2,7 @@ from datetime import datetime
from django.utils import timezone as djangotime
from rest_framework import serializers
from django.conf import settings
from scripts.models import Script
from tacticalrmm.constants import TaskType
@@ -257,6 +258,8 @@ class TaskGOGetSerializer(serializers.ModelSerializer):
shell=script.shell,
env_vars=env_vars,
),
"nushell_enable_config": settings.NUSHELL_ENABLE_CONFIG,
"deno_default_permissions": settings.DENO_DEFAULT_PERMISSIONS,
}
)
if actions_to_remove:

View File

@@ -0,0 +1,23 @@
# Generated by Django 4.2.10 on 2024-02-19 05:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("checks", "0031_check_env_vars"),
]
operations = [
migrations.AlterField(
model_name="checkhistory",
name="id",
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name="checkresult",
name="id",
field=models.BigAutoField(primary_key=True, serialize=False),
),
]

View File

@@ -168,10 +168,7 @@ class Check(BaseAuditModel):
elif self.agent:
cache.delete(f"agent_{self.agent.agent_id}_checks")
super(Check, self).save(
*args,
**kwargs,
)
super().save(*args, **kwargs)
def delete(self, *args, **kwargs):
# if check is a policy check clear cache on everything
@@ -183,10 +180,7 @@ class Check(BaseAuditModel):
elif self.agent:
cache.delete(f"agent_{self.agent.agent_id}_checks")
super(Check, self).delete(
*args,
**kwargs,
)
super().delete(*args, **kwargs)
@property
def readable_desc(self):
@@ -290,6 +284,7 @@ class CheckResult(models.Model):
class Meta:
unique_together = (("agent", "assigned_check"),)
id = models.BigAutoField(primary_key=True)
agent = models.ForeignKey(
"agents.Agent",
related_name="checkresults",
@@ -338,10 +333,7 @@ class CheckResult(models.Model):
):
self.alert_severity = AlertSeverity.WARNING
super(CheckResult, self).save(
*args,
**kwargs,
)
super().save(*args, **kwargs)
@property
def history_info(self):
@@ -673,6 +665,7 @@ class CheckResult(models.Model):
class CheckHistory(models.Model):
objects = PermissionQuerySet.as_manager()
id = models.BigAutoField(primary_key=True)
check_id = models.PositiveIntegerField(default=0)
agent_id = models.CharField(max_length=200, null=True, blank=True)
x = models.DateTimeField(auto_now_add=True)

View File

@@ -49,11 +49,7 @@ class Client(BaseAuditModel):
# get old client if exists
old_client = Client.objects.get(pk=self.pk) if self.pk else None
super(Client, self).save(
old_model=old_client,
*args,
**kwargs,
)
super().save(old_model=old_client, *args, **kwargs)
# check if polcies have changed and initiate task to reapply policies if so
if old_client and (
@@ -129,11 +125,7 @@ class Site(BaseAuditModel):
# get old client if exists
old_site = Site.objects.get(pk=self.pk) if self.pk else None
super(Site, self).save(
old_model=old_site,
*args,
**kwargs,
)
super().save(old_model=old_site, *args, **kwargs)
# check if polcies have changed and initiate task to reapply policies if so
if old_site:

View File

@@ -88,6 +88,7 @@ class TestClientViews(TacticalTestCase):
"client": {"name": "Setup Client"},
"site": {"name": "Setup Site"},
"timezone": "America/Los_Angeles",
"companyname": "TestCo Inc.",
"initialsetup": True,
}
r = self.client.post(url, payload, format="json")

View File

@@ -92,7 +92,8 @@ class GetAddClients(APIView):
if "initialsetup" in request.data.keys():
core = get_core_settings()
core.default_time_zone = request.data["timezone"]
core.save(update_fields=["default_time_zone"])
core.mesh_company_name = request.data["companyname"]
core.save(update_fields=["default_time_zone", "mesh_company_name"])
# save custom fields
if "custom_fields" in request.data.keys():

View File

@@ -41,6 +41,7 @@ agentBin="${agentBinPath}/${binName}"
agentConf='/etc/tacticalagent'
agentSvcName='tacticalagent.service'
agentSysD="/etc/systemd/system/${agentSvcName}"
agentDir='/opt/tacticalagent'
meshDir='/opt/tacticalmesh'
meshSystemBin="${meshDir}/meshagent"
meshSvcName='meshagent.service'
@@ -65,16 +66,20 @@ RemoveOldAgent() {
if [ -f "${agentSysD}" ]; then
systemctl disable ${agentSvcName}
systemctl stop ${agentSvcName}
rm -f ${agentSysD}
rm -f "${agentSysD}"
systemctl daemon-reload
fi
if [ -f "${agentConf}" ]; then
rm -f ${agentConf}
rm -f "${agentConf}"
fi
if [ -f "${agentBin}" ]; then
rm -f ${agentBin}
rm -f "${agentBin}"
fi
if [ -d "${agentDir}" ]; then
rm -rf "${agentDir}"
fi
}
@@ -132,16 +137,18 @@ Uninstall() {
RemoveOldAgent
}
if [ $# -ne 0 ] && [ $1 == 'uninstall' ]; then
if [ $# -ne 0 ] && [[ $1 =~ ^(uninstall|-uninstall|--uninstall)$ ]]; then
Uninstall
# Remove the current script
rm "$0"
exit 0
fi
while [[ "$#" -gt 0 ]]; do
case $1 in
--debug) DEBUG=1 ;;
--insecure) INSECURE=1 ;;
--nomesh) NOMESH=1 ;;
-debug | --debug | debug) DEBUG=1 ;;
-insecure | --insecure | insecure) INSECURE=1 ;;
-nomesh | --nomesh | nomesh) NOMESH=1 ;;
*)
echo "ERROR: Unknown parameter: $1"
exit 1

View File

@@ -27,7 +27,7 @@ class Command(BaseCommand):
self._warning("Mesh device group:", core.mesh_device_group)
try:
token = get_auth_token(core.mesh_username, core.mesh_token)
token = get_auth_token(core.mesh_api_superuser, core.mesh_token)
except Exception as e:
self._error("Error getting auth token:")
self._error(str(e))

View File

@@ -5,6 +5,7 @@ from tacticalrmm.constants import (
AGENT_OUTAGES_LOCK,
ORPHANED_WIN_TASK_LOCK,
RESOLVE_ALERTS_LOCK,
SYNC_MESH_PERMS_TASK_LOCK,
SYNC_SCHED_TASK_LOCK,
)
@@ -18,5 +19,6 @@ class Command(BaseCommand):
ORPHANED_WIN_TASK_LOCK,
RESOLVE_ALERTS_LOCK,
SYNC_SCHED_TASK_LOCK,
SYNC_MESH_PERMS_TASK_LOCK,
):
cache.delete(key)

View File

@@ -4,7 +4,7 @@ import os
from django.conf import settings
from django.core.management.base import BaseCommand
from tacticalrmm.helpers import get_nats_internal_protocol, get_nats_ports
from tacticalrmm.helpers import get_nats_url
class Command(BaseCommand):
@@ -20,11 +20,9 @@ class Command(BaseCommand):
else:
ssl = "disable"
nats_std_port, _ = get_nats_ports()
proto = get_nats_internal_protocol()
config = {
"key": settings.SECRET_KEY,
"natsurl": f"{proto}://{settings.ALLOWED_HOSTS[0]}:{nats_std_port}",
"natsurl": get_nats_url(),
"user": db["USER"],
"pass": db["PASSWORD"],
"host": db["HOST"],

View File

@@ -5,13 +5,14 @@ import websockets
from django.core.management.base import BaseCommand
from core.utils import get_mesh_ws_url
from tacticalrmm.constants import TRMM_WS_MAX_SIZE
class Command(BaseCommand):
help = "Sets up initial mesh central configuration"
async def websocket_call(self, uri):
async with websockets.connect(uri) as websocket:
async with websockets.connect(uri, max_size=TRMM_WS_MAX_SIZE) as websocket:
# Get Invitation Link
await websocket.send(
json.dumps(

View File

@@ -0,0 +1,19 @@
from django.core.management.base import BaseCommand
from meshctrl.utils import get_login_token
from core.utils import get_core_settings
class Command(BaseCommand):
help = "generate a url to login to mesh as the superuser"
def handle(self, *args, **kwargs):
core = get_core_settings()
token = get_login_token(key=core.mesh_token, user=f"user//{core.mesh_username}")
token_param = f"login={token}&"
control = f"{core.mesh_site}/?{token_param}"
self.stdout.write(self.style.SUCCESS(control))

View File

@@ -6,13 +6,14 @@ from django.conf import settings
from django.core.management.base import BaseCommand
from core.utils import get_core_settings, get_mesh_ws_url
from tacticalrmm.constants import TRMM_WS_MAX_SIZE
class Command(BaseCommand):
help = "Sets up initial mesh central configuration"
async def websocket_call(self, uri):
async with websockets.connect(uri) as websocket:
async with websockets.connect(uri, max_size=TRMM_WS_MAX_SIZE) as websocket:
# Get Device groups to see if it exists
await websocket.send(json.dumps({"action": "meshes"}))

View File

@@ -6,6 +6,8 @@ from accounts.models import User
from agents.models import Agent
from autotasks.models import AutomatedTask
from checks.models import Check, CheckHistory
from core.models import CoreSettings
from core.tasks import remove_orphaned_history_results, sync_mesh_perms_task
from scripts.models import Script
from tacticalrmm.constants import AGENT_DEFER, ScriptType
@@ -54,4 +56,22 @@ class Command(BaseCommand):
agent.save(update_fields=["goarch"])
self.stdout.write(
self.style.SUCCESS("Checking for orphaned history results...")
)
count = remove_orphaned_history_results()
if count:
self.stdout.write(
self.style.SUCCESS(f"Removed {count} orphaned history results.")
)
core = CoreSettings.objects.first()
if core.sync_mesh_with_trmm:
self.stdout.write(
self.style.SUCCESS(
"Syncing trmm users/permissions with meshcentral, this might take a long time...please wait..."
)
)
sync_mesh_perms_task()
self.stdout.write("Post update tasks finished")

View File

@@ -8,6 +8,7 @@ from core.tasks import (
core_maintenance_tasks,
resolve_alerts_task,
resolve_pending_actions,
sync_mesh_perms_task,
sync_scheduled_tasks,
)
from winupdate.tasks import auto_approve_updates_task, check_agent_update_schedule_task
@@ -28,3 +29,4 @@ class Command(BaseCommand):
remove_orphaned_win_tasks.delay()
auto_approve_updates_task.delay()
check_agent_update_schedule_task.delay()
sync_mesh_perms_task.delay()

View File

@@ -0,0 +1,15 @@
from django.core.management.base import BaseCommand
from core.tasks import sync_mesh_perms_task
class Command(BaseCommand):
help = "Sync mesh users/perms with trmm users/perms"
def handle(self, *args, **kwargs):
self.stdout.write(
self.style.SUCCESS(
"Syncing trmm users/permissions with meshcentral, this might take a long time...please wait..."
)
)
sync_mesh_perms_task()

View File

@@ -0,0 +1,183 @@
import asyncio
import json
import re
import secrets
import string
import traceback
from typing import TYPE_CHECKING, Any
import websockets
from accounts.utils import is_superuser
from tacticalrmm.constants import TRMM_WS_MAX_SIZE
from tacticalrmm.logger import logger
if TYPE_CHECKING:
from accounts.models import User
def build_mesh_display_name(
*, first_name: str | None, last_name: str | None, company_name: str | None
) -> str:
ret = ""
if first_name:
ret += first_name
if last_name:
ret += f" {last_name}"
if ret and company_name:
ret += f" - {company_name}"
elif company_name:
ret += company_name
return ret
def has_mesh_perms(*, user: "User") -> bool:
if user.is_superuser or is_superuser(user):
return True
return user.role and getattr(user.role, "can_use_mesh")
def make_mesh_password() -> str:
alpha = string.ascii_letters + string.digits
nonalpha = "!@#$"
passwd = [secrets.choice(alpha) for _ in range(29)] + [secrets.choice(nonalpha)]
secrets.SystemRandom().shuffle(passwd)
return "".join(passwd)
def transform_trmm(obj):
ret = []
try:
for node in obj:
node_id = node["node_id"]
user_ids = [link["_id"] for link in node["links"]]
ret.append({"node_id": node_id, "user_ids": user_ids})
except Exception:
logger.debug(traceback.format_exc)
return ret
def transform_mesh(obj):
pattern = re.compile(r".*___\d+")
ret = []
try:
for _, nodes in obj.items():
for node in nodes:
node_id = node["_id"]
try:
user_ids = [
user_id
for user_id in node["links"].keys()
if pattern.match(user_id)
]
except KeyError:
# will trigger on initial sync cuz no mesh users yet
# also triggers for invalid agents after sync
pass
else:
ret.append({"node_id": node_id, "user_ids": user_ids})
except Exception:
logger.debug(traceback.format_exc)
return ret
class MeshSync:
def __init__(self, uri: str):
self.uri = uri
self.mesh_users = self.get_trmm_mesh_users() # full list
def mesh_action(
self, *, payload: dict[str, Any], wait=True
) -> dict[str, Any] | None:
async def _do(payload):
async with websockets.connect(self.uri, max_size=TRMM_WS_MAX_SIZE) as ws:
await ws.send(json.dumps(payload))
if wait:
while 1:
try:
message = await asyncio.wait_for(ws.recv(), 120)
r = json.loads(message)
if r["action"] == payload["action"]:
return r
except asyncio.TimeoutError:
logger.error("Timeout reached.")
return None
else:
return None
payload["responseid"] = "meshctrl"
logger.debug(payload)
return asyncio.run(_do(payload))
def get_unique_mesh_users(
self, trmm_agents_list: list[dict[str, Any]]
) -> list[str]:
userids = [i["links"] for i in trmm_agents_list]
all_ids = [item["_id"] for sublist in userids for item in sublist]
return list(set(all_ids))
def get_trmm_mesh_users(self):
payload = {"action": "users"}
ret = {
i["_id"]: i
for i in self.mesh_action(payload=payload, wait=True)["users"]
if re.search(r".*___\d+", i["_id"])
}
return ret
def add_users_to_node(self, *, node_id: str, user_ids: list[str]):
payload = {
"action": "adddeviceuser",
"nodeid": node_id,
"usernames": [s.replace("user//", "") for s in user_ids],
"rights": 4088024,
"remove": False,
}
self.mesh_action(payload=payload, wait=False)
def delete_users_from_node(self, *, node_id: str, user_ids: list[str]):
payload = {
"action": "adddeviceuser",
"nodeid": node_id,
"userids": user_ids,
"rights": 0,
"remove": True,
}
self.mesh_action(payload=payload, wait=False)
def update_mesh_displayname(self, *, user_info: dict[str, Any]) -> None:
payload = {
"action": "edituser",
"id": user_info["_id"],
"realname": user_info["full_name"],
}
self.mesh_action(payload=payload, wait=False)
def add_user_to_mesh(self, *, user_info: dict[str, Any]) -> None:
payload = {
"action": "adduser",
"username": user_info["username"],
"email": user_info["email"],
"pass": make_mesh_password(),
"resetNextLogin": False,
"randomPassword": False,
"removeEvents": False,
"emailVerified": True,
}
self.mesh_action(payload=payload, wait=False)
if user_info["full_name"]:
self.update_mesh_displayname(user_info=user_info)
def delete_user_from_mesh(self, *, mesh_user_id: str) -> None:
payload = {
"action": "deleteuser",
"userid": mesh_user_id,
}
self.mesh_action(payload=payload, wait=False)

View File

@@ -0,0 +1,17 @@
# Generated by Django 4.2.9 on 2024-01-26 00:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0038_alter_coresettings_default_time_zone"),
]
operations = [
migrations.AddField(
model_name="coresettings",
name="smtp_from_name",
field=models.CharField(blank=True, max_length=255, null=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.2.9 on 2024-01-28 02:50
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0039_coresettings_smtp_from_name"),
]
operations = [
migrations.AddField(
model_name="customfield",
name="hide_in_summary",
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.2.9 on 2024-01-28 03:01
from django.db import migrations
def update_hide_in_summary(apps, schema_editor):
CustomField = apps.get_model("core", "CustomField")
for field in CustomField.objects.filter(hide_in_ui=True):
field.hide_in_summary = True
field.save(update_fields=["hide_in_summary"])
class Migration(migrations.Migration):
dependencies = [
("core", "0040_customfield_hide_in_summary"),
]
operations = [migrations.RunPython(update_hide_in_summary)]

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.2.10 on 2024-02-20 02:51
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0041_auto_20240128_0301"),
]
operations = [
migrations.AddField(
model_name="coresettings",
name="mesh_company_name",
field=models.CharField(blank=True, max_length=255, null=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.2.10 on 2024-02-23 19:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0042_coresettings_mesh_company_name"),
]
operations = [
migrations.AddField(
model_name="coresettings",
name="sync_mesh_with_trmm",
field=models.BooleanField(default=True),
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 4.2.11 on 2024-03-12 05:23
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("core", "0043_coresettings_sync_mesh_with_trmm"),
]
operations = [
migrations.RemoveField(
model_name="coresettings",
name="mesh_disable_auto_login",
),
]

View File

@@ -1,5 +1,6 @@
import smtplib
from contextlib import suppress
from email.headerregistry import Address
from email.message import EmailMessage
from typing import TYPE_CHECKING, List, Optional, cast
@@ -44,6 +45,7 @@ class CoreSettings(BaseAuditModel):
smtp_from_email = models.CharField(
max_length=255, blank=True, default="from@example.com"
)
smtp_from_name = models.CharField(max_length=255, null=True, blank=True)
smtp_host = models.CharField(max_length=255, blank=True, default="smtp.gmail.com")
smtp_host_user = models.CharField(
max_length=255, blank=True, default="admin@example.com"
@@ -72,7 +74,8 @@ class CoreSettings(BaseAuditModel):
mesh_device_group = models.CharField(
max_length=255, null=True, blank=True, default="TacticalRMM"
)
mesh_disable_auto_login = models.BooleanField(default=False)
mesh_company_name = models.CharField(max_length=255, null=True, blank=True)
sync_mesh_with_trmm = models.BooleanField(default=True)
agent_auto_update = models.BooleanField(default=True)
workstation_policy = models.ForeignKey(
"automation.Policy",
@@ -119,7 +122,7 @@ class CoreSettings(BaseAuditModel):
self.mesh_token = settings.MESH_TOKEN_KEY
old_settings = type(self).objects.get(pk=self.pk) if self.pk else None
super(BaseAuditModel, self).save(*args, **kwargs)
super().save(*args, **kwargs)
if old_settings:
if (
@@ -144,6 +147,11 @@ class CoreSettings(BaseAuditModel):
def __str__(self) -> str:
return "Global Site Settings"
@property
def mesh_api_superuser(self) -> str:
# must be lowercase otherwise mesh api breaks
return self.mesh_username.lower()
@property
def sms_is_configured(self) -> bool:
return all(
@@ -207,7 +215,14 @@ class CoreSettings(BaseAuditModel):
try:
msg = EmailMessage()
msg["Subject"] = subject
msg["From"] = from_address
if self.smtp_from_name:
msg["From"] = Address(
display_name=self.smtp_from_name, addr_spec=from_address
)
else:
msg["From"] = from_address
msg["To"] = email_recipients
msg.set_content(body)
@@ -222,9 +237,16 @@ class CoreSettings(BaseAuditModel):
server.send_message(msg)
server.quit()
else:
# smtp relay. no auth required
server.send_message(msg)
server.quit()
# gmail smtp relay specific handling.
if self.smtp_host == "smtp-relay.gmail.com":
server.ehlo()
server.starttls()
server.send_message(msg)
server.quit()
else:
# smtp relay. no auth required
server.send_message(msg)
server.quit()
except Exception as e:
DebugLog.error(message=f"Sending email failed with error: {e}")
@@ -298,6 +320,7 @@ class CustomField(BaseAuditModel):
default=list,
)
hide_in_ui = models.BooleanField(default=False)
hide_in_summary = models.BooleanField(default=False)
class Meta:
unique_together = (("model", "name"),)
@@ -348,7 +371,7 @@ class CodeSignToken(models.Model):
if not self.pk and CodeSignToken.objects.exists():
raise ValidationError("There can only be one CodeSignToken instance")
super(CodeSignToken, self).save(*args, **kwargs)
super().save(*args, **kwargs)
@property
def is_valid(self) -> bool:

View File

@@ -13,7 +13,11 @@ class CoreSettingsPerms(permissions.BasePermission):
class URLActionPerms(permissions.BasePermission):
def has_permission(self, r, view) -> bool:
return _has_perm(r, "can_run_urlactions")
if r.method in {"GET", "PATCH"}:
return _has_perm(r, "can_run_urlactions")
# TODO make a manage url action perm instead?
return _has_perm(r, "can_edit_core_settings")
class ServerMaintPerms(permissions.BasePermission):

View File

@@ -1,3 +1,4 @@
from django.conf import settings
from rest_framework import serializers
from tacticalrmm.constants import ALL_TIMEZONES
@@ -5,7 +6,19 @@ from tacticalrmm.constants import ALL_TIMEZONES
from .models import CodeSignToken, CoreSettings, CustomField, GlobalKVStore, URLAction
class CoreSettingsSerializer(serializers.ModelSerializer):
class HostedCoreMixin:
def to_representation(self, instance):
ret = super().to_representation(instance) # type: ignore
if getattr(settings, "HOSTED", False):
for field in ("mesh_site", "mesh_token", "mesh_username"):
ret[field] = "n/a"
ret["sync_mesh_with_trmm"] = True
return ret
class CoreSettingsSerializer(HostedCoreMixin, serializers.ModelSerializer):
all_timezones = serializers.SerializerMethodField("all_time_zones")
def all_time_zones(self, obj):
@@ -17,7 +30,7 @@ class CoreSettingsSerializer(serializers.ModelSerializer):
# for audting
class CoreSerializer(serializers.ModelSerializer):
class CoreSerializer(HostedCoreMixin, serializers.ModelSerializer):
class Meta:
model = CoreSettings
fields = "__all__"

View File

@@ -1,24 +1,36 @@
import asyncio
import logging
import traceback
from contextlib import suppress
from time import sleep
from typing import TYPE_CHECKING, Any
import nats
from django.conf import settings
from django.db import transaction
from django.db.models import Prefetch
from django.db.utils import DatabaseError
from django.utils import timezone as djangotime
from packaging import version as pyver
from accounts.models import User
from accounts.utils import is_superuser
from agents.models import Agent
from agents.tasks import clear_faults_task, prune_agent_history
from alerts.models import Alert
from alerts.tasks import prune_resolved_alerts
from autotasks.models import AutomatedTask, TaskResult
from checks.models import Check, CheckResult
from checks.models import Check, CheckHistory, CheckResult
from checks.tasks import prune_check_history
from clients.models import Client, Site
from core.utils import get_core_settings
from core.mesh_utils import (
MeshSync,
build_mesh_display_name,
has_mesh_perms,
transform_mesh,
transform_trmm,
)
from core.models import CoreSettings
from core.utils import get_core_settings, get_mesh_ws_url, make_alpha_numeric
from logs.models import PendingAction
from logs.tasks import prune_audit_log, prune_debug_log
from tacticalrmm.celery import app
@@ -27,6 +39,7 @@ from tacticalrmm.constants import (
AGENT_STATUS_ONLINE,
AGENT_STATUS_OVERDUE,
RESOLVE_ALERTS_LOCK,
SYNC_MESH_PERMS_TASK_LOCK,
SYNC_SCHED_TASK_LOCK,
AlertSeverity,
AlertType,
@@ -36,15 +49,34 @@ from tacticalrmm.constants import (
TaskSyncStatus,
TaskType,
)
from tacticalrmm.helpers import setup_nats_options
from tacticalrmm.helpers import make_random_password, setup_nats_options
from tacticalrmm.logger import logger
from tacticalrmm.nats_utils import a_nats_cmd
from tacticalrmm.permissions import _has_perm_on_agent
from tacticalrmm.utils import redis_lock
if TYPE_CHECKING:
from django.db.models import QuerySet
from nats.aio.client import Client as NATSClient
logger = logging.getLogger("trmm")
def remove_orphaned_history_results() -> int:
try:
with transaction.atomic():
check_hist_agentids = CheckHistory.objects.values_list(
"agent_id", flat=True
).distinct()
current_agentids = set(Agent.objects.values_list("agent_id", flat=True))
orphaned_agentids = [
i for i in check_hist_agentids if i not in current_agentids
]
count, _ = CheckHistory.objects.filter(
agent_id__in=orphaned_agentids
).delete()
return count
except Exception as e:
logger.error(str(e))
return 0
@app.task
@@ -53,6 +85,8 @@ def core_maintenance_tasks() -> None:
remove_if_not_scheduled=True, expire_date__lt=djangotime.now()
).delete()
remove_orphaned_history_results()
core = get_core_settings()
# remove old CheckHistory data
@@ -361,3 +395,172 @@ def cache_db_fields_task() -> None:
agents = qs.filter(site__client=client)
client.failing_checks = _get_failing_data(agents)
client.save(update_fields=["failing_checks"])
@app.task(bind=True)
def sync_mesh_perms_task(self):
with redis_lock(SYNC_MESH_PERMS_TASK_LOCK, self.app.oid) as acquired:
if not acquired:
return f"{self.app.oid} still running"
try:
core = CoreSettings.objects.first()
do_not_sync = not core.sync_mesh_with_trmm
uri = get_mesh_ws_url()
ms = MeshSync(uri)
if do_not_sync:
for user in ms.mesh_users:
ms.delete_user_from_mesh(mesh_user_id=user)
return
company_name = core.mesh_company_name
mnp = {"action": "nodes"}
mesh_nodes_raw = ms.mesh_action(payload=mnp, wait=True)["nodes"]
users = User.objects.select_related("role").filter(
agent=None,
is_installer_user=False,
is_active=True,
block_dashboard_login=False,
)
trmm_agents_meshnodeids = [
f"node//{i.hex_mesh_node_id}"
for i in Agent.objects.only("mesh_node_id")
if i.mesh_node_id
]
mesh_users_dict = {}
for user in users:
full_name = build_mesh_display_name(
first_name=user.first_name,
last_name=user.last_name,
company_name=company_name,
)
# mesh user creation will fail if same email exists for another user
# make sure that doesn't happen by making a random email
rand_str1 = make_random_password(len=6)
rand_str2 = make_random_password(len=5)
# for trmm users whos usernames are emails
email_prefix = make_alpha_numeric(user.username)
email = f"{email_prefix}.{rand_str1}@tacticalrmm-do-not-change-{rand_str2}.local"
mesh_users_dict[user.mesh_user_id] = {
"_id": user.mesh_user_id,
"username": user.mesh_username,
"full_name": full_name,
"email": email,
}
new_trmm_agents = []
for agent in Agent.objects.defer(*AGENT_DEFER):
if not agent.mesh_node_id:
continue
agent_dict = {
"node_id": f"node//{agent.hex_mesh_node_id}",
"hostname": agent.hostname,
}
tmp: list[dict[str, str]] = []
for user in users:
if not has_mesh_perms(user=user):
logger.debug(f"No mesh perms for {user} on {agent.hostname}")
continue
if (user.is_superuser or is_superuser(user)) or _has_perm_on_agent(
user, agent.agent_id
):
tmp.append({"_id": user.mesh_user_id})
agent_dict["links"] = tmp
new_trmm_agents.append(agent_dict)
final_trmm = transform_trmm(new_trmm_agents)
final_mesh = transform_mesh(mesh_nodes_raw)
# delete users first
source_users_global = set()
for item in final_trmm:
source_users_global.update(item["user_ids"])
target_users_global = set()
for item in final_mesh:
target_users_global.update(item["user_ids"])
# identify and create new users
new_users = list(source_users_global - target_users_global)
for user_id in new_users:
user_info = mesh_users_dict[user_id]
logger.info(f"Adding new user {user_info['username']} to mesh")
ms.add_user_to_mesh(user_info=user_info)
users_to_delete_globally = list(target_users_global - source_users_global)
for user_id in users_to_delete_globally:
logger.info(f"Deleting {user_id} from mesh")
ms.delete_user_from_mesh(mesh_user_id=user_id)
source_map = {item["node_id"]: set(item["user_ids"]) for item in final_trmm}
target_map = {item["node_id"]: set(item["user_ids"]) for item in final_mesh}
def _get_sleep_after_n_inter(n):
# {number of agents: chunk size}
thresholds = {250: 150, 500: 275, 800: 300, 1000: 340}
for threshold, value in sorted(thresholds.items()):
if n <= threshold:
return value
return 375
iter_count = 0
sleep_after = _get_sleep_after_n_inter(len(source_map))
for node_id, source_users in source_map.items():
# skip agents without valid node id
if node_id not in trmm_agents_meshnodeids:
continue
target_users = target_map.get(node_id, set()) - set(
users_to_delete_globally
)
source_users_adjusted = source_users - set(users_to_delete_globally)
# find users that need to be added or deleted
users_to_add = list(source_users_adjusted - target_users)
users_to_delete = list(target_users - source_users_adjusted)
if users_to_add or users_to_delete:
iter_count += 1
if users_to_add:
logger.info(f"Adding {users_to_add} to {node_id}")
ms.add_users_to_node(node_id=node_id, user_ids=users_to_add)
if users_to_delete:
logger.info(f"Deleting {users_to_delete} from {node_id}")
ms.delete_users_from_node(node_id=node_id, user_ids=users_to_delete)
if iter_count % sleep_after == 0 and iter_count != 0:
# mesh is very inefficient with sql, give it time to catch up so we don't crash the system
logger.info(
f"Sleeping for 7 seconds after {iter_count} iterations."
)
sleep(7)
# after all done, see if need to update display name
ms2 = MeshSync(uri)
unique_ids = ms2.get_unique_mesh_users(new_trmm_agents)
for user in unique_ids:
try:
mesh_realname = ms2.mesh_users[user]["realname"]
except KeyError:
mesh_realname = ""
trmm_realname = mesh_users_dict[user]["full_name"]
if mesh_realname != trmm_realname:
logger.info(
f"Display names don't match. Updating {user} name from {mesh_realname} to {trmm_realname}"
)
ms2.update_mesh_displayname(user_info=mesh_users_dict[user])
except Exception:
logger.debug(traceback.format_exc())

View File

@@ -1,3 +1,4 @@
import os
from unittest.mock import patch
import requests
@@ -11,16 +12,15 @@ from model_bakery import baker
from rest_framework.authtoken.models import Token
# from agents.models import Agent
from core.utils import get_core_settings, get_meshagent_url
from core.utils import get_core_settings, get_mesh_ws_url, get_meshagent_url
# from logs.models import PendingAction
from tacticalrmm.constants import (
from tacticalrmm.constants import ( # PAAction,; PAStatus,
CONFIG_MGMT_CMDS,
CustomFieldModel,
MeshAgentIdent,
# PAAction,
# PAStatus,
)
from tacticalrmm.helpers import get_nats_hosts, get_nats_url
from tacticalrmm.test import TacticalTestCase
from .consumers import DashInfo
@@ -110,18 +110,63 @@ class TestCoreTasks(TacticalTestCase):
def test_edit_coresettings(self):
url = "/core/settings/"
# setup
baker.make("automation.Policy", _quantity=2)
# test normal request
data = {
"smtp_from_email": "newexample@example.com",
"mesh_token": "New_Mesh_Token",
"mesh_site": "https://mesh.example.com",
"mesh_username": "bob",
"sync_mesh_with_trmm": False,
}
r = self.client.put(url, data)
self.assertEqual(r.status_code, 200)
self.assertEqual(get_core_settings().smtp_from_email, data["smtp_from_email"])
self.assertEqual(get_core_settings().mesh_token, data["mesh_token"])
core = get_core_settings()
self.assertEqual(core.smtp_from_email, "newexample@example.com")
self.assertEqual(core.mesh_token, "New_Mesh_Token")
self.assertEqual(core.mesh_site, "https://mesh.example.com")
self.assertEqual(core.mesh_username, "bob")
self.assertFalse(core.sync_mesh_with_trmm)
# test to_representation
r = self.client.get(url)
self.assertEqual(r.data["smtp_from_email"], "newexample@example.com")
self.assertEqual(r.data["mesh_token"], "New_Mesh_Token")
self.assertEqual(r.data["mesh_site"], "https://mesh.example.com")
self.assertEqual(r.data["mesh_username"], "bob")
self.assertFalse(r.data["sync_mesh_with_trmm"])
self.check_not_authenticated("put", url)
@override_settings(HOSTED=True)
def test_hosted_edit_coresettings(self):
url = "/core/settings/"
baker.make("automation.Policy", _quantity=2)
data = {
"smtp_from_email": "newexample1@example.com",
"mesh_token": "abc123",
"mesh_site": "https://mesh15534.example.com",
"mesh_username": "jane",
"sync_mesh_with_trmm": False,
}
r = self.client.put(url, data)
self.assertEqual(r.status_code, 200)
core = get_core_settings()
self.assertEqual(core.smtp_from_email, "newexample1@example.com")
self.assertIn("41410834b8bb4481446027f8", core.mesh_token) # type: ignore
self.assertTrue(core.sync_mesh_with_trmm)
if "GHACTIONS" in os.environ:
self.assertEqual(core.mesh_site, "https://example.com")
self.assertEqual(core.mesh_username, "pipeline")
# test to_representation
r = self.client.get(url)
self.assertEqual(r.data["smtp_from_email"], "newexample1@example.com")
self.assertEqual(r.data["mesh_token"], "n/a")
self.assertEqual(r.data["mesh_site"], "n/a")
self.assertEqual(r.data["mesh_username"], "n/a")
self.assertTrue(r.data["sync_mesh_with_trmm"])
self.check_not_authenticated("put", url)
@@ -445,6 +490,80 @@ class TestCoreMgmtCommands(TacticalTestCase):
call_command("get_config", cmd)
class TestNatsUrls(TacticalTestCase):
def setUp(self):
self.setup_coresettings()
def test_standard_install(self):
self.assertEqual(get_nats_url(), "nats://127.0.0.1:4222")
@override_settings(
NATS_STANDARD_PORT=5000,
USE_NATS_STANDARD=True,
ALLOWED_HOSTS=["api.example.com"],
)
def test_custom_port_nats_standard(self):
self.assertEqual(get_nats_url(), "tls://api.example.com:5000")
@override_settings(DOCKER_BUILD=True, ALLOWED_HOSTS=["api.example.com"])
def test_docker_nats(self):
self.assertEqual(get_nats_url(), "nats://api.example.com:4222")
@patch.dict("os.environ", {"NATS_CONNECT_HOST": "172.20.4.3"})
@override_settings(ALLOWED_HOSTS=["api.example.com"])
def test_custom_connect_host_env(self):
self.assertEqual(get_nats_url(), "nats://172.20.4.3:4222")
def test_standard_nats_hosts(self):
self.assertEqual(get_nats_hosts(), ("127.0.0.1", "127.0.0.1", "127.0.0.1"))
@override_settings(DOCKER_BUILD=True, ALLOWED_HOSTS=["api.example.com"])
def test_docker_nats_hosts(self):
self.assertEqual(get_nats_hosts(), ("0.0.0.0", "0.0.0.0", "api.example.com"))
class TestMeshWSUrl(TacticalTestCase):
def setUp(self):
self.setup_coresettings()
@patch("core.utils.get_auth_token")
def test_standard_install(self, mock_token):
mock_token.return_value = "abc123"
self.assertEqual(
get_mesh_ws_url(), "ws://127.0.0.1:4430/control.ashx?auth=abc123"
)
@patch("core.utils.get_auth_token")
@override_settings(MESH_PORT=8876)
def test_standard_install_custom_port(self, mock_token):
mock_token.return_value = "abc123"
self.assertEqual(
get_mesh_ws_url(), "ws://127.0.0.1:8876/control.ashx?auth=abc123"
)
@patch("core.utils.get_auth_token")
@override_settings(DOCKER_BUILD=True, MESH_WS_URL="ws://tactical-meshcentral:4443")
def test_docker_install(self, mock_token):
mock_token.return_value = "abc123"
self.assertEqual(
get_mesh_ws_url(), "ws://tactical-meshcentral:4443/control.ashx?auth=abc123"
)
@patch("core.utils.get_auth_token")
@override_settings(USE_EXTERNAL_MESH=True)
def test_external_mesh(self, mock_token):
mock_token.return_value = "abc123"
from core.models import CoreSettings
core = CoreSettings.objects.first()
core.mesh_site = "https://mesh.external.com" # type: ignore
core.save(update_fields=["mesh_site"]) # type: ignore
self.assertEqual(
get_mesh_ws_url(), "wss://mesh.external.com/control.ashx?auth=abc123"
)
class TestCorePermissions(TacticalTestCase):
def setUp(self):
self.setup_client()

View File

@@ -16,6 +16,7 @@ from tacticalrmm.constants import (
AGENT_TBL_PEND_ACTION_CNT_CACHE_PREFIX,
CORESETTINGS_CACHE_KEY,
ROLE_CACHE_PREFIX,
TRMM_WS_MAX_SIZE,
AgentPlat,
MeshAgentIdent,
)
@@ -83,23 +84,23 @@ def get_core_settings() -> "CoreSettings":
def get_mesh_ws_url() -> str:
core = get_core_settings()
token = get_auth_token(core.mesh_username, core.mesh_token)
token = get_auth_token(core.mesh_api_superuser, core.mesh_token)
if settings.DOCKER_BUILD:
uri = f"{settings.MESH_WS_URL}/control.ashx?auth={token}"
else:
if getattr(settings, "TRMM_INSECURE", False):
site = core.mesh_site.replace("https", "ws")
uri = f"{site}:4430/control.ashx?auth={token}"
else:
if getattr(settings, "USE_EXTERNAL_MESH", False):
site = core.mesh_site.replace("https", "wss")
uri = f"{site}/control.ashx?auth={token}"
else:
mesh_port = getattr(settings, "MESH_PORT", 4430)
uri = f"ws://127.0.0.1:{mesh_port}/control.ashx?auth={token}"
return uri
async def get_mesh_device_id(uri: str, device_group: str) -> None:
async with websockets.connect(uri) as ws:
async with websockets.connect(uri, max_size=TRMM_WS_MAX_SIZE) as ws:
payload = {"action": "meshes", "responseid": "meshctrl"}
await ws.send(json.dumps(payload))
@@ -204,3 +205,7 @@ def get_meshagent_url(
}
return base + "/meshagents?" + urllib.parse.urlencode(params)
def make_alpha_numeric(s: str):
return "".join(filter(str.isalnum, s))

View File

@@ -15,12 +15,13 @@ from django.views.decorators.csrf import csrf_exempt
from redis import from_url
from rest_framework.decorators import api_view, permission_classes
from rest_framework.exceptions import PermissionDenied
from rest_framework.permissions import IsAuthenticated
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.views import APIView
from core.decorators import monitoring_view
from core.tasks import sync_mesh_perms_task
from core.utils import get_core_settings, sysd_svc_is_running, token_is_valid
from logs.models import AuditLog
from tacticalrmm.constants import AuditActionType, PAStatus
@@ -56,14 +57,29 @@ class GetEditCoreSettings(APIView):
return Response(CoreSettingsSerializer(settings).data)
def put(self, request):
data = request.data.copy()
if getattr(settings, "HOSTED", False):
data.pop("mesh_site")
data.pop("mesh_token")
data.pop("mesh_username")
data["sync_mesh_with_trmm"] = True
coresettings = CoreSettings.objects.first()
serializer = CoreSettingsSerializer(instance=coresettings, data=request.data)
serializer = CoreSettingsSerializer(instance=coresettings, data=data)
serializer.is_valid(raise_exception=True)
serializer.save()
sync_mesh_perms_task.delay()
return Response("ok")
@api_view()
@permission_classes([AllowAny])
def home(request):
return Response({"status": "ok"})
@api_view()
def version(request):
return Response(settings.APP_VER)
@@ -91,9 +107,9 @@ def dashboard_info(request):
"show_community_scripts": request.user.show_community_scripts,
"dbl_click_action": request.user.agent_dblclick_action,
"default_agent_tbl_tab": request.user.default_agent_tbl_tab,
"url_action": request.user.url_action.id
if request.user.url_action
else None,
"url_action": (
request.user.url_action.id if request.user.url_action else None
),
"client_tree_sort": request.user.client_tree_sort,
"client_tree_splitter": request.user.client_tree_splitter,
"loading_bar_color": request.user.loading_bar_color,
@@ -315,7 +331,7 @@ class UpdateDeleteKeyStore(APIView):
class GetAddURLAction(APIView):
permission_classes = [IsAuthenticated, CoreSettingsPerms]
permission_classes = [IsAuthenticated, URLActionPerms]
def get(self, request):
actions = URLAction.objects.all()

View File

@@ -4,7 +4,7 @@ from zoneinfo import ZoneInfo
import validators
def as_tz(date_obj, tz, format="%b %d, %I:%M %p"):
def as_tz(date_obj, tz, format="%b %d %Y, %I:%M %p"):
return date_obj.astimezone(ZoneInfo(tz)).strftime(format)

View File

@@ -3,6 +3,7 @@ Copyright (c) 2023-present Amidaware Inc.
This file is subject to the EE License Agreement.
For details, see: https://license.tacticalrmm.com/ee
"""
import json
from typing import TYPE_CHECKING, Any, Dict, List, Tuple

View File

@@ -3,6 +3,7 @@ Copyright (c) 2023-present Amidaware Inc.
This file is subject to the EE License Agreement.
For details, see: https://license.tacticalrmm.com/ee
"""
import urllib.parse
from time import sleep
from typing import Any, Optional

View File

@@ -187,9 +187,11 @@ class TestReportTemplateGenerateView:
template=report_template.template_md,
template_type=report_template.type,
css=report_template.template_css if report_template.template_css else "",
html_template=report_template.template_html.id
if report_template.template_html
else None,
html_template=(
report_template.template_html.id
if report_template.template_html
else None
),
variables=report_template.template_variables,
dependencies={"client": 1},
)

View File

@@ -130,9 +130,9 @@ class GenerateReport(APIView):
template=template.template_md,
template_type=template.type,
css=template.template_css or "",
html_template=template.template_html.id
if template.template_html
else None,
html_template=(
template.template_html.id if template.template_html else None
),
variables=template.template_variables,
dependencies=request.data["dependencies"],
)

View File

@@ -0,0 +1,28 @@
# Generated by Django 4.2.10 on 2024-02-19 05:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("logs", "0024_remove_pendingaction_cancelable_and_more"),
]
operations = [
migrations.AlterField(
model_name="auditlog",
name="id",
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name="debuglog",
name="id",
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name="pendingaction",
name="id",
field=models.BigAutoField(primary_key=True, serialize=False),
),
]

View File

@@ -26,6 +26,7 @@ def get_debug_level() -> str:
class AuditLog(models.Model):
id = models.BigAutoField(primary_key=True)
username = models.CharField(max_length=255)
agent = models.CharField(max_length=255, null=True, blank=True)
agent_id = models.CharField(max_length=255, blank=True, null=True)
@@ -47,7 +48,7 @@ class AuditLog(models.Model):
(self.message[:253] + "..") if len(self.message) > 255 else self.message
)
return super(AuditLog, self).save(*args, **kwargs)
return super().save(*args, **kwargs)
@staticmethod
def audit_mesh_session(
@@ -258,6 +259,7 @@ class AuditLog(models.Model):
class DebugLog(models.Model):
objects = PermissionQuerySet.as_manager()
id = models.BigAutoField(primary_key=True)
entry_time = models.DateTimeField(auto_now_add=True)
agent = models.ForeignKey(
"agents.Agent",
@@ -347,6 +349,7 @@ class DebugLog(models.Model):
class PendingAction(models.Model):
objects = PermissionQuerySet.as_manager()
id = models.BigAutoField(primary_key=True)
agent = models.ForeignKey(
"agents.Agent",
related_name="pendingactions",
@@ -454,10 +457,10 @@ class BaseAuditModel(models.Model):
debug_info=get_debug_info(),
)
super(BaseAuditModel, self).save(*args, **kwargs)
super().save(*args, **kwargs)
def delete(self, *args, **kwargs) -> Tuple[int, Dict[str, int]]:
super(BaseAuditModel, self).delete(*args, **kwargs)
super().delete(*args, **kwargs)
username = get_username()
if username:

View File

@@ -152,9 +152,11 @@ class TestAuditViews(TacticalTestCase):
self.assertEqual(resp.status_code, 200)
self.assertEqual(
len(resp.data["audit_logs"]), # type:ignore
pagination["rowsPerPage"]
if req["count"] > pagination["rowsPerPage"]
else req["count"],
(
pagination["rowsPerPage"]
if req["count"] > pagination["rowsPerPage"]
else req["count"]
),
)
self.assertEqual(resp.data["total"], req["count"]) # type:ignore

View File

@@ -1,5 +1,5 @@
black
daphne==4.0.0
daphne==4.1.0
Werkzeug
django-extensions
isort

View File

@@ -7,4 +7,4 @@ pytest-xdist
pytest-cov
refurb
flake8
daphne==4.0.0
daphne==4.1.0

View File

@@ -1,46 +1,46 @@
adrf==0.1.2
adrf==0.1.5
asgiref==3.7.2
celery==5.3.5
certifi==2023.11.17
celery==5.3.6
certifi==2024.2.2
cffi==1.16.0
channels==4.0.0
channels_redis==4.1.0
cryptography==41.0.7
Django==4.2.7
channels_redis==4.2.0
cryptography==42.0.5
Django==4.2.11
django-cors-headers==4.3.1
django-filter==23.4
django-filter==24.2
django-rest-knox==4.2.0
djangorestframework==3.14.0
drf-spectacular==0.26.5
hiredis==2.2.3
drf-spectacular==0.27.1
hiredis==2.3.2
meshctrl==0.1.15
msgpack==1.0.7
nats-py==2.6.0
packaging==23.2
psutil==5.9.6
psycopg[binary]==3.1.13
msgpack==1.0.8
nats-py==2.7.2
packaging==24.0
psutil==5.9.8
psycopg[binary]==3.1.18
pycparser==2.21
pycryptodome==3.19.0
pycryptodome==3.20.0
pyotp==2.9.0
pyparsing==3.1.1
python-ipware==2.0.0
pyparsing==3.1.2
python-ipware==2.0.2
qrcode==7.4.2
redis==4.5.5
redis==5.0.3
requests==2.31.0
six==1.16.0
sqlparse==0.4.4
twilio==8.10.2
urllib3==2.1.0
uvicorn[standard]==0.23.2
uWSGI==2.0.22
validators==0.20.0
twilio==8.13.0
urllib3==2.2.1
uvicorn[standard]==0.29.0
uWSGI==2.0.24
validators==0.24.0
vine==5.1.0
websockets==12.0
zipp==3.17.0
pandas==2.1.3
zipp==3.18.1
pandas==2.2.1
kaleido==0.2.1
jinja2==3.1.2
markdown==3.5.1
plotly==5.18.0
weasyprint==60.1
jinja2==3.1.3
markdown==3.6
plotly==5.20.0
weasyprint==61.2
ocxsect==0.1.5

View File

@@ -0,0 +1,45 @@
# Generated by Django 4.2.10 on 2024-02-22 04:51
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("scripts", "0019_script_env_vars"),
]
operations = [
migrations.AlterField(
model_name="script",
name="shell",
field=models.CharField(
choices=[
("powershell", "Powershell"),
("cmd", "Batch (CMD)"),
("python", "Python"),
("shell", "Shell"),
("nushell", "Nushell"),
("deno", "Deno"),
],
default="powershell",
max_length=100,
),
),
migrations.AlterField(
model_name="scriptsnippet",
name="shell",
field=models.CharField(
choices=[
("powershell", "Powershell"),
("cmd", "Batch (CMD)"),
("python", "Python"),
("shell", "Shell"),
("nushell", "Nushell"),
("deno", "Deno"),
],
default="powershell",
max_length=15,
),
),
]

View File

@@ -1,5 +1,7 @@
import asyncio
from django.conf import settings
from agents.models import Agent, AgentHistory
from scripts.models import Script
from tacticalrmm.celery import app
@@ -78,6 +80,8 @@ def bulk_script_task(
},
"run_as_user": run_as_user,
"env_vars": script.parse_script_env_vars(agent, script.shell, env_vars),
"nushell_enable_config": settings.NUSHELL_ENABLE_CONFIG,
"deno_default_permissions": settings.DENO_DEFAULT_PERMISSIONS,
}
tup = (agent.agent_id, data)
items.append(tup)

View File

@@ -5,6 +5,7 @@ from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from django.conf import settings
from agents.permissions import RunScriptPerms
from tacticalrmm.constants import ScriptShell, ScriptType
@@ -153,7 +154,7 @@ class TestScript(APIView):
)
data = {
"func": "runscript",
"func": "runscriptfull",
"timeout": request.data["timeout"],
"script_args": parsed_args,
"payload": {
@@ -162,6 +163,8 @@ class TestScript(APIView):
},
"run_as_user": request.data["run_as_user"],
"env_vars": parsed_env_vars,
"nushell_enable_config": settings.NUSHELL_ENABLE_CONFIG,
"deno_default_permissions": settings.DENO_DEFAULT_PERMISSIONS,
}
r = asyncio.run(
@@ -190,6 +193,10 @@ def download(request, pk):
ext = ".py"
case ScriptShell.SHELL:
ext = ".sh"
case ScriptShell.NUSHELL:
ext = ".nu"
case ScriptShell.DENO:
ext = ".ts"
case _:
ext = ""

View File

@@ -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 = [
("software", "0003_delete_chocolog"),
]
operations = [
migrations.AlterField(
model_name="installedsoftware",
name="id",
field=models.BigAutoField(primary_key=True, serialize=False),
),
]

View File

@@ -15,6 +15,7 @@ class ChocoSoftware(models.Model):
class InstalledSoftware(models.Model):
objects = PermissionQuerySet.as_manager()
id = models.BigAutoField(primary_key=True)
agent = models.ForeignKey(Agent, on_delete=models.CASCADE)
software = models.JSONField()

View File

@@ -9,13 +9,15 @@ from django.conf import settings
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tacticalrmm.settings")
app = Celery("tacticalrmm", backend="redis://" + settings.REDIS_HOST, broker="redis://" + settings.REDIS_HOST) # type: ignore
redis_host = f"redis://{settings.REDIS_HOST}"
app = Celery("tacticalrmm", backend=redis_host, broker=redis_host)
app.accept_content = ["application/json"]
app.result_serializer = "json"
app.task_serializer = "json"
app.conf.task_track_started = True
app.conf.worker_proc_alive_timeout = 30
app.conf.worker_max_tasks_per_child = 2
app.conf.broker_connection_retry_on_startup = True
app.autodiscover_tasks()
app.conf.beat_schedule = {
@@ -33,7 +35,7 @@ app.conf.beat_schedule = {
},
"remove-orphaned-tasks": {
"task": "autotasks.tasks.remove_orphaned_win_tasks",
"schedule": crontab(minute=50, hour="12"),
"schedule": crontab(minute=50, hour="*/2"),
},
"agent-outages-task": {
"task": "agents.tasks.agent_outages_task",
@@ -55,6 +57,10 @@ app.conf.beat_schedule = {
"task": "core.tasks.sync_scheduled_tasks",
"schedule": crontab(minute="*/2", hour="*"),
},
"sync-mesh-perms-task": {
"task": "core.tasks.sync_mesh_perms_task",
"schedule": crontab(minute="*/4", hour="*"),
},
"resolve-pending-actions": {
"task": "core.tasks.resolve_pending_actions",
"schedule": timedelta(seconds=100.0),

View File

@@ -1,6 +1,7 @@
import zoneinfo
from enum import Enum
from django.conf import settings
from django.db import models
@@ -30,6 +31,10 @@ RESOLVE_ALERTS_LOCK = "resolve-alerts-lock-key"
SYNC_SCHED_TASK_LOCK = "sync-sched-tasks-lock-key"
AGENT_OUTAGES_LOCK = "agent-outages-task-lock-key"
ORPHANED_WIN_TASK_LOCK = "orphaned-win-task-lock-key"
SYNC_MESH_PERMS_TASK_LOCK = "sync-mesh-perms-lock-key"
TRMM_WS_MAX_SIZE = getattr(settings, "TRMM_WS_MAX_SIZE", 100 * 2**20)
TRMM_MAX_REQUEST_SIZE = getattr(settings, "TRMM_MAX_REQUEST_SIZE", 10 * 2**20)
class GoArch(models.TextChoices):
@@ -132,6 +137,8 @@ class ScriptShell(models.TextChoices):
CMD = "cmd", "Batch (CMD)"
PYTHON = "python", "Python"
SHELL = "shell", "Shell"
NUSHELL = "nushell", "Nushell"
DENO = "deno", "Deno"
class ScriptType(models.TextChoices):

View File

@@ -1,3 +1,4 @@
import os
import random
import secrets
import string
@@ -43,10 +44,49 @@ def get_nats_ports() -> tuple[int, int]:
def get_nats_internal_protocol() -> str:
if getattr(settings, "TRMM_INSECURE", False):
return "nats"
if getattr(settings, "USE_NATS_STANDARD", False):
return "tls"
return "tls"
return "nats"
def get_nats_hosts() -> tuple[str, str, str]:
std_bind_host = "0.0.0.0"
ws_bind_host = "0.0.0.0"
connect_host = settings.ALLOWED_HOSTS[0]
# standard install
if not settings.DOCKER_BUILD and not getattr(settings, "USE_NATS_STANDARD", False):
std_bind_host, ws_bind_host, connect_host = (
"127.0.0.1",
"127.0.0.1",
"127.0.0.1",
)
# allow customizing all nats hosts
if "NATS_STD_BIND_HOST" in os.environ:
std_bind_host = os.getenv("NATS_STD_BIND_HOST")
elif hasattr(settings, "NATS_STD_BIND_HOST"):
std_bind_host = settings.NATS_STD_BIND_HOST
if "NATS_WS_BIND_HOST" in os.environ:
ws_bind_host = os.getenv("NATS_WS_BIND_HOST")
elif hasattr(settings, "NATS_WS_BIND_HOST"):
ws_bind_host = settings.NATS_WS_BIND_HOST
if "NATS_CONNECT_HOST" in os.environ:
connect_host = os.getenv("NATS_CONNECT_HOST")
elif hasattr(settings, "NATS_CONNECT_HOST"):
connect_host = settings.NATS_CONNECT_HOST
return std_bind_host, ws_bind_host, connect_host
def get_nats_url() -> str:
_, _, connect_host = get_nats_hosts()
proto = get_nats_internal_protocol()
port, _ = get_nats_ports()
return f"{proto}://{connect_host}:{port}"
def date_is_in_past(*, datetime_obj: "datetime", agent_tz: str) -> bool:
@@ -72,10 +112,8 @@ def rand_range(min: int, max: int) -> float:
def setup_nats_options() -> dict[str, Any]:
nats_std_port, _ = get_nats_ports()
proto = get_nats_internal_protocol()
opts = {
"servers": f"{proto}://{settings.ALLOWED_HOSTS[0]}:{nats_std_port}",
"servers": get_nats_url(),
"user": "tacticalrmm",
"name": "trmm-django",
"password": settings.SECRET_KEY,

View File

@@ -0,0 +1,3 @@
import logging
logger = logging.getLogger("trmm")

View File

@@ -4,6 +4,7 @@ from django.db.models import Q
from django.shortcuts import get_object_or_404
from agents.models import Agent
from tacticalrmm.constants import AGENT_DEFER
if TYPE_CHECKING:
from accounts.models import User
@@ -33,7 +34,10 @@ def _has_perm_on_agent(user: "User", agent_id: str) -> bool:
elif not role:
return False
agent = get_object_or_404(Agent, agent_id=agent_id)
agent = get_object_or_404(
Agent.objects.defer(*AGENT_DEFER).select_related("site__client"),
agent_id=agent_id,
)
can_view_clients = role.can_view_clients.all() if role else None
can_view_sites = role.can_view_sites.all() if role else None

View File

@@ -1,4 +1,5 @@
import os
import sys
from contextlib import suppress
from datetime import timedelta
from pathlib import Path
@@ -20,27 +21,70 @@ MAC_UNINSTALL = BASE_DIR / "core" / "mac_uninstall.sh"
AUTH_USER_MODEL = "accounts.User"
# latest release
TRMM_VERSION = "0.17.2"
TRMM_VERSION = "0.18.2"
# https://github.com/amidaware/tacticalrmm-web
WEB_VERSION = "0.101.37"
WEB_VERSION = "0.101.44"
# bump this version everytime vue code is changed
# to alert user they need to manually refresh their browser
APP_VER = "0.0.188"
APP_VER = "0.0.192"
# https://github.com/amidaware/rmmagent
LATEST_AGENT_VER = "2.6.0"
LATEST_AGENT_VER = "2.7.0"
MESH_VER = "1.1.9"
MESH_VER = "1.1.21"
NATS_SERVER_VER = "2.10.5"
NATS_SERVER_VER = "2.10.12"
# Install Nushell on the agent
# https://github.com/nushell/nushell
INSTALL_NUSHELL = True
# GitHub version to download. The file will be downloaded from GitHub, extracted and installed.
# Version to download. If INSTALL_NUSHELL_URL is not provided, the file will be downloaded from GitHub,
# extracted and installed.
INSTALL_NUSHELL_VERSION = "0.92.1"
# URL to download directly. This is expected to be the direct URL, unauthenticated, uncompressed, ready to be installed.
# Use {OS}, {ARCH} and {VERSION} to specify the GOOS, GOARCH and INSTALL_NUSHELL_VERSION respectively.
# Windows: The ".exe" extension will be added automatically.
# Examples:
# https://examplle.com/download/nushell/{OS}/{ARCH}/{VERSION}/nu
# https://examplle.com/download/nushell/nu-{VERSION}-{OS}-{ARCH}
INSTALL_NUSHELL_URL = ""
# Enable Nushell config on the agent
# The default is to not enable the config because it could change how scripts run.
# However, disabling the config prevents plugins from being registered.
# https://github.com/nushell/nushell/issues/10754
# False: --no-config-file option is added to the command line.
# True: --config and --env-config options are added to the command line and point to the Agent's directory.
NUSHELL_ENABLE_CONFIG = False
# Install Deno on the agent
# https://github.com/denoland/deno
INSTALL_DENO = True
# Version to download. If INSTALL_DENO_URL is not provided, the file will be downloaded from GitHub,
# extracted and installed.
INSTALL_DENO_VERSION = "v1.42.1"
# URL to download directly. This is expected to be the direct URL, unauthenticated, uncompressed, ready to be installed.
# Use {OS}, {ARCH} and {VERSION} to specify the GOOS, GOARCH and INSTALL_DENO_VERSION respectively.
# Windows: The ".exe" extension will be added automatically.
# Examples:
# https://examplle.com/download/deno/{OS}/{ARCH}/{VERSION}/deno
# https://examplle.com/download/deno/deno-{VERSION}-{OS}-{ARCH}
INSTALL_DENO_URL = ""
# Default permissions for Deno
# Space separated list of permissions as listed in the documentation.
# https://docs.deno.com/runtime/manual/basics/permissions#permissions
# Examples:
# DENO_DEFAULT_PERMISSIONS = "--allow-sys --allow-net --allow-env"
# DENO_DEFAULT_PERMISSIONS = "--allow-all"
DENO_DEFAULT_PERMISSIONS = "--allow-all"
# for the update script, bump when need to recreate venv
PIP_VER = "40"
PIP_VER = "43"
SETUPTOOLS_VER = "68.2.2"
WHEEL_VER = "0.41.3"
SETUPTOOLS_VER = "69.2.0"
WHEEL_VER = "0.43.0"
AGENT_BASE_URL = "https://agents.tacticalrmm.com"
@@ -71,6 +115,7 @@ HOSTED = False
SWAGGER_ENABLED = False
REDIS_HOST = "127.0.0.1"
TRMM_LOG_LEVEL = "ERROR"
TRMM_LOG_TO = "file"
with suppress(ImportError):
from .local_settings import * # noqa
@@ -156,7 +201,7 @@ CACHES = {
"BACKEND": "tacticalrmm.cache.TacticalRedisCache",
"LOCATION": f"redis://{REDIS_HOST}:6379",
"OPTIONS": {
"parser_class": "redis.connection.HiredisParser",
"parser_class": "redis.connection._HiredisParser",
"pool_class": "redis.BlockingConnectionPool",
"db": "10",
},
@@ -240,6 +285,24 @@ def get_log_level() -> str:
return TRMM_LOG_LEVEL
def configure_logging_handler():
cfg = {
"level": get_log_level(),
"formatter": "verbose",
}
log_to = os.getenv("TRMM_LOG_TO", TRMM_LOG_TO)
if log_to == "stdout":
cfg["class"] = "logging.StreamHandler"
cfg["stream"] = sys.stdout
else:
cfg["class"] = "logging.FileHandler"
cfg["filename"] = os.path.join(LOG_DIR, "trmm_debug.log")
return cfg
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
@@ -256,12 +319,7 @@ LOGGING = {
"filename": os.path.join(LOG_DIR, "django_debug.log"),
"formatter": "verbose",
},
"trmm": {
"level": get_log_level(),
"class": "logging.FileHandler",
"filename": os.path.join(LOG_DIR, "trmm_debug.log"),
"formatter": "verbose",
},
"trmm": configure_logging_handler(),
},
"loggers": {
"django.request": {"handlers": ["file"], "level": "ERROR", "propagate": True},

View File

@@ -18,3 +18,11 @@ class AgentCheckInConfig(TRMMStruct):
checkin_wmi: int
checkin_syncmesh: int
limit_data: bool
install_nushell: bool
install_nushell_version: str
install_nushell_url: str
nushell_enable_config: bool
install_deno: bool
install_deno_version: str
install_deno_url: str
deno_default_permissions: str

View File

@@ -5,6 +5,7 @@ from knox import views as knox_views
from accounts.views import CheckCreds, LoginView
from agents.consumers import SendCMD
from core.consumers import DashInfo
from core.views import home
class AgentIDConverter:
@@ -20,6 +21,7 @@ class AgentIDConverter:
register_converter(AgentIDConverter, "agent")
urlpatterns = [
path("", home),
path("checkcreds/", CheckCreds.as_view()),
path("login/", LoginView.as_view()),
path("logout/", knox_views.LogoutView.as_view()),

View File

@@ -33,6 +33,7 @@ from tacticalrmm.constants import (
)
from tacticalrmm.helpers import (
get_certs,
get_nats_hosts,
get_nats_internal_protocol,
get_nats_ports,
notify_error,
@@ -206,13 +207,16 @@ def reload_nats() -> None:
)
cert_file, key_file = get_certs()
nats_std_host, nats_ws_host, _ = get_nats_hosts()
nats_std_port, nats_ws_port = get_nats_ports()
config = {
"authorization": {"users": users},
"max_payload": 67108864,
"host": nats_std_host,
"port": nats_std_port, # internal only
"websocket": {
"host": nats_ws_host,
"port": nats_ws_port,
"no_tls": True, # TLS is handled by nginx, so not needed here
},
@@ -399,6 +403,17 @@ def replace_arg_db_values(
elif value is True or value is False:
return format_shell_bool(value, shell)
elif isinstance(value, dict):
return json.dumps(value)
# return str for everything else
try:
ret = str(value)
except Exception:
ret = ""
return ret
def format_shell_array(value: list[str]) -> str:
temp_string = ""

View File

@@ -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 = [
("winupdate", "0012_auto_20220227_0554"),
]
operations = [
migrations.AlterField(
model_name="winupdate",
name="id",
field=models.BigAutoField(primary_key=True, serialize=False),
),
]

View File

@@ -39,6 +39,7 @@ SCHEDULE_FREQUENCY_CHOICES = [
class WinUpdate(models.Model):
id = models.BigAutoField(primary_key=True)
agent = models.ForeignKey(
Agent, related_name="winupdates", on_delete=models.CASCADE
)

View File

@@ -1,4 +1,4 @@
FROM node:16-alpine
FROM node:20-alpine
WORKDIR /home/node/app

View File

@@ -1,4 +1,4 @@
FROM nats:2.10.3-alpine
FROM nats:2.10.12-alpine
ENV TACTICAL_DIR /opt/tactical
ENV TACTICAL_READY_FILE ${TACTICAL_DIR}/tmp/tactical.ready

View File

@@ -1,5 +1,5 @@
# creates python virtual env
FROM python:3.11.6-slim AS CREATE_VENV_STAGE
FROM python:3.11.8-slim AS CREATE_VENV_STAGE
ARG DEBIAN_FRONTEND=noninteractive
@@ -21,14 +21,14 @@ RUN apt-get update && \
pip install --no-cache-dir -r ${TACTICAL_TMP_DIR}/api/requirements.txt
# 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
# runtime image
FROM python:3.11.6-slim
FROM python:3.11.8-slim
# set env variables
ENV VIRTUAL_ENV /opt/venv

View File

@@ -159,7 +159,7 @@ fi
if [ "$1" = 'tactical-celery' ]; then
check_tactical_ready
celery -A tacticalrmm worker --autoscale=30,5 -l info
celery -A tacticalrmm worker --autoscale=20,2 -l info
fi
if [ "$1" = 'tactical-celerybeat' ]; then

View File

@@ -52,7 +52,7 @@ services:
container_name: trmm-init
image: ${IMAGE_REPO}tactical:${VERSION}
restart: on-failure
command: [ "tactical-init" ]
command: ["tactical-init"]
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASS: ${POSTGRES_PASS}
@@ -84,8 +84,6 @@ services:
restart: always
environment:
API_HOST: ${API_HOST}
ports:
- "4222:4222"
volumes:
- tactical_data:/opt/tactical
networks:
@@ -151,7 +149,7 @@ services:
container_name: trmm-backend
image: ${IMAGE_REPO}tactical:${VERSION}
user: 1000:1000
command: [ "tactical-backend" ]
command: ["tactical-backend"]
restart: always
networks:
- proxy
@@ -167,7 +165,7 @@ services:
container_name: trmm-websockets
image: ${IMAGE_REPO}tactical:${VERSION}
user: 1000:1000
command: [ "tactical-websockets" ]
command: ["tactical-websockets"]
restart: always
networks:
- proxy
@@ -205,7 +203,7 @@ services:
container_name: trmm-celery
image: ${IMAGE_REPO}tactical:${VERSION}
user: 1000:1000
command: [ "tactical-celery" ]
command: ["tactical-celery"]
restart: always
networks:
- redis
@@ -222,7 +220,7 @@ services:
container_name: trmm-celerybeat
image: ${IMAGE_REPO}tactical:${VERSION}
user: 1000:1000
command: [ "tactical-celerybeat" ]
command: ["tactical-celerybeat"]
restart: always
networks:
- proxy

12
go.mod
View File

@@ -1,12 +1,12 @@
module github.com/amidaware/tacticalrmm
go 1.21.4
go 1.21.8
require (
github.com/jmoiron/sqlx v1.3.5
github.com/lib/pq v1.10.9
github.com/nats-io/nats.go v1.31.0
github.com/ugorji/go/codec v1.2.11
github.com/nats-io/nats.go v1.34.0
github.com/ugorji/go/codec v1.2.12
github.com/wh1te909/trmm-shared v0.0.0-20220227075846-f9f757361139
)
@@ -14,10 +14,10 @@ require github.com/sirupsen/logrus v1.9.3
require (
github.com/klauspost/compress v1.17.2 // indirect
github.com/nats-io/nkeys v0.4.6 // indirect
github.com/nats-io/nkeys v0.4.7 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/stretchr/testify v1.7.1 // indirect
golang.org/x/crypto v0.14.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/crypto v0.18.0 // indirect
golang.org/x/sys v0.16.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

20
go.sum
View File

@@ -12,10 +12,10 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/nats-io/nats.go v1.31.0 h1:/WFBHEc/dOKBF6qf1TZhrdEfTmOZ5JzdJ+Y3m6Y/p7E=
github.com/nats-io/nats.go v1.31.0/go.mod h1:di3Bm5MLsoB4Bx61CBTsxuarI36WbhAwOm8QrW39+i8=
github.com/nats-io/nkeys v0.4.6 h1:IzVe95ru2CT6ta874rt9saQRkWfe2nFj1NtvYSLqMzY=
github.com/nats-io/nkeys v0.4.6/go.mod h1:4DxZNzenSVd1cYQoAa8948QY3QDjrHfcfVADymtkpts=
github.com/nats-io/nats.go v1.34.0 h1:fnxnPCNiwIG5w08rlMcEKTUw4AV/nKyGCOJE8TdhSPk=
github.com/nats-io/nats.go v1.34.0/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8=
github.com/nats-io/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI=
github.com/nats-io/nkeys v0.4.7/go.mod h1:kqXRgRDPlGy7nGaEDMuYzmiJCIAAWDK0IMBtDmGD0nc=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -26,15 +26,15 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/wh1te909/trmm-shared v0.0.0-20220227075846-f9f757361139 h1:PfOl03o+Y+svWrfXAAu1QWUDePu1yqTq0pf4rpnN8eA=
github.com/wh1te909/trmm-shared v0.0.0-20220227075846-f9f757361139/go.mod h1:ILUz1utl5KgwrxmNHv0RpgMtKeh8gPAABvK2MiXBqv8=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@@ -1,9 +1,12 @@
#!/usr/bin/env bash
SCRIPT_VERSION="80"
SCRIPT_VERSION="83"
SCRIPT_URL="https://raw.githubusercontent.com/amidaware/tacticalrmm/master/install.sh"
sudo apt install -y curl wget dirmngr gnupg lsb-release ca-certificates
sudo apt install -y software-properties-common
sudo apt update
sudo apt install -y openssl
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
@@ -12,7 +15,7 @@ RED='\033[0;31m'
NC='\033[0m'
SCRIPTS_DIR='/opt/trmm-community-scripts'
PYTHON_VER='3.11.6'
PYTHON_VER='3.11.8'
SETTINGS_FILE='/rmm/api/tacticalrmm/tacticalrmm/settings.py'
local_settings='/rmm/api/tacticalrmm/tacticalrmm/local_settings.py'
@@ -30,6 +33,11 @@ fi
rm -f $TMP_FILE
if [ -d /rmm/api/tacticalrmm ]; then
echo -ne "${RED}ERROR: Existing trmm installation found. The install script must be run on a clean server.${NC}\n"
exit 1
fi
arch=$(uname -m)
if [[ "$arch" != "x86_64" ]] && [[ "$arch" != "aarch64" ]]; then
echo -ne "${RED}ERROR: Only x86_64 and aarch64 is supported, not ${arch}${NC}\n"
@@ -113,6 +121,14 @@ print_green() {
printf >&2 "\n"
}
print_error() {
printf >&2 "${RED}${1}${NC}\n"
}
print_yellow() {
printf >&2 "${YELLOW}${1}${NC}\n"
}
cls
while [[ $rmmdomain != *[.]*[.]* ]]; do
@@ -138,6 +154,34 @@ while [[ $letsemail != *[@]*[.]* ]]; do
read letsemail
done
byocert=false
if [[ $* == *--use-own-cert* ]]; then
byocert=true
fi
if [[ "$byocert" = true ]]; then
while true; do
print_yellow "Please enter the full path to your fullchain.pem file:"
read -r fullchain_path
print_yellow "Please enter the full path to your privkey.pem file:"
read -r privkey_path
if [[ ! -f "$fullchain_path" || ! -f "$privkey_path" ]]; then
print_error "One or both files do not exist. Please try again."
continue
fi
openssl x509 -in "$fullchain_path" -noout >/dev/null
if [[ $? -ne 0 ]]; then
print_error "ERROR: The provided file is not a valid certificate."
exit 1
fi
break
done
fi
if grep -q manage_etc_hosts /etc/hosts; then
sudo sed -i '/manage_etc_hosts: true/d' /etc/cloud/cloud.cfg >/dev/null
echo -e "\nmanage_etc_hosts: false" | sudo tee --append /etc/cloud/cloud.cfg >/dev/null
@@ -167,10 +211,6 @@ if [[ $* == *--insecure* ]]; then
insecure=true
fi
sudo apt install -y software-properties-common
sudo apt update
sudo apt install -y openssl
if [[ "$insecure" = true ]]; then
print_green 'Generating self-signed cert'
certdir='/etc/ssl/tactical'
@@ -183,6 +223,10 @@ if [[ "$insecure" = true ]]; then
-nodes -keyout ${CERT_PRIV_KEY} -out ${CERT_PUB_KEY} -subj "/CN=${rootdomain}" \
-addext "subjectAltName=DNS:${rootdomain},DNS:*.${rootdomain}"
elif [[ "$byocert" = true ]]; then
CERT_PRIV_KEY=$privkey_path
CERT_PUB_KEY=$fullchain_path
sudo chown ${USER}:${USER} $CERT_PRIV_KEY $CERT_PUB_KEY
else
sudo apt install -y certbot
print_green 'Getting wildcard cert'
@@ -193,10 +237,9 @@ else
done
CERT_PRIV_KEY=/etc/letsencrypt/live/${rootdomain}/privkey.pem
CERT_PUB_KEY=/etc/letsencrypt/live/${rootdomain}/fullchain.pem
sudo chown ${USER}:${USER} -R /etc/letsencrypt
fi
sudo chown ${USER}:${USER} -R /etc/letsencrypt
print_green 'Installing Nginx'
sudo mkdir -p /etc/apt/keyrings
@@ -232,7 +275,7 @@ http {
sendfile on;
tcp_nopush on;
types_hash_max_size 2048;
server_names_hash_bucket_size 64;
server_names_hash_bucket_size 256;
include /etc/nginx/mime.types;
default_type application/octet-stream;
ssl_protocols TLSv1.2 TLSv1.3;
@@ -254,7 +297,7 @@ done
print_green 'Installing NodeJS'
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
NODE_MAJOR=18
NODE_MAJOR=20
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list
sudo apt update
sudo apt install -y gcc g++ make
@@ -295,25 +338,25 @@ done
print_green 'Creating database for trmm'
sudo -u postgres psql -c "CREATE DATABASE tacticalrmm"
sudo -u postgres psql -c "CREATE USER ${pgusername} WITH PASSWORD '${pgpw}'"
sudo -u postgres psql -c "ALTER ROLE ${pgusername} SET client_encoding TO 'utf8'"
sudo -u postgres psql -c "ALTER ROLE ${pgusername} SET default_transaction_isolation TO 'read committed'"
sudo -u postgres psql -c "ALTER ROLE ${pgusername} SET timezone TO 'UTC'"
sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE tacticalrmm TO ${pgusername}"
sudo -u postgres psql -c "ALTER DATABASE tacticalrmm OWNER TO ${pgusername}"
sudo -u postgres psql -c "GRANT USAGE, CREATE ON SCHEMA PUBLIC TO ${pgusername}"
sudo -iu postgres psql -c "CREATE DATABASE tacticalrmm"
sudo -iu postgres psql -c "CREATE USER ${pgusername} WITH PASSWORD '${pgpw}'"
sudo -iu postgres psql -c "ALTER ROLE ${pgusername} SET client_encoding TO 'utf8'"
sudo -iu postgres psql -c "ALTER ROLE ${pgusername} SET default_transaction_isolation TO 'read committed'"
sudo -iu postgres psql -c "ALTER ROLE ${pgusername} SET timezone TO 'UTC'"
sudo -iu postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE tacticalrmm TO ${pgusername}"
sudo -iu postgres psql -c "ALTER DATABASE tacticalrmm OWNER TO ${pgusername}"
sudo -iu postgres psql -c "GRANT USAGE, CREATE ON SCHEMA PUBLIC TO ${pgusername}"
print_green 'Creating database for meshcentral'
sudo -u postgres psql -c "CREATE DATABASE meshcentral"
sudo -u postgres psql -c "CREATE USER ${MESHPGUSER} WITH PASSWORD '${MESHPGPWD}'"
sudo -u postgres psql -c "ALTER ROLE ${MESHPGUSER} SET client_encoding TO 'utf8'"
sudo -u postgres psql -c "ALTER ROLE ${MESHPGUSER} SET default_transaction_isolation TO 'read committed'"
sudo -u postgres psql -c "ALTER ROLE ${MESHPGUSER} SET timezone TO 'UTC'"
sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE meshcentral TO ${MESHPGUSER}"
sudo -u postgres psql -c "ALTER DATABASE meshcentral OWNER TO ${MESHPGUSER}"
sudo -u postgres psql -c "GRANT USAGE, CREATE ON SCHEMA PUBLIC TO ${MESHPGUSER}"
sudo -iu postgres psql -c "CREATE DATABASE meshcentral"
sudo -iu postgres psql -c "CREATE USER ${MESHPGUSER} WITH PASSWORD '${MESHPGPWD}'"
sudo -iu postgres psql -c "ALTER ROLE ${MESHPGUSER} SET client_encoding TO 'utf8'"
sudo -iu postgres psql -c "ALTER ROLE ${MESHPGUSER} SET default_transaction_isolation TO 'read committed'"
sudo -iu postgres psql -c "ALTER ROLE ${MESHPGUSER} SET timezone TO 'UTC'"
sudo -iu postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE meshcentral TO ${MESHPGUSER}"
sudo -iu postgres psql -c "ALTER DATABASE meshcentral OWNER TO ${MESHPGUSER}"
sudo -iu postgres psql -c "GRANT USAGE, CREATE ON SCHEMA PUBLIC TO ${MESHPGUSER}"
print_green 'Cloning repos'
@@ -448,7 +491,6 @@ DATABASES = {
MESH_USERNAME = "${meshusername}"
MESH_SITE = "https://${meshdomain}"
REDIS_HOST = "localhost"
ADMIN_ENABLED = True
EOF
)"
@@ -458,6 +500,16 @@ if [[ "$insecure" = true ]]; then
echo "TRMM_INSECURE = True" | tee --append $local_settings >/dev/null
fi
if [[ "$byocert" = true ]]; then
owncerts="$(
cat <<EOF
CERT_FILE = "${CERT_PUB_KEY}"
KEY_FILE = "${CERT_PRIV_KEY}"
EOF
)"
echo "${owncerts}" | tee --append $local_settings >/dev/null
fi
if [ "$arch" = "x86_64" ]; then
natsapi='nats-api'
else
@@ -971,6 +1023,7 @@ cd /rmm/api/tacticalrmm
source /rmm/api/env/bin/activate
python manage.py initial_db_setup
python manage.py reload_nats
python manage.py sync_mesh_with_trmm
deactivate
sudo systemctl start nats.service

View File

@@ -12,7 +12,7 @@ import (
)
var (
version = "3.5.0"
version = "3.5.4"
log = logrus.New()
)

Binary file not shown.

Binary file not shown.

View File

@@ -4,7 +4,7 @@ import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"os"
"github.com/jmoiron/sqlx"
_ "github.com/lib/pq"
@@ -20,7 +20,7 @@ func GetConfig(cfg string) (db *sqlx.DB, r DjangoConfig, err error) {
}
}
jret, _ := ioutil.ReadFile(cfg)
jret, _ := os.ReadFile(cfg)
err = json.Unmarshal(jret, &r)
if err != nil {
return

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env bash
SCRIPT_VERSION="54"
SCRIPT_VERSION="57"
SCRIPT_URL='https://raw.githubusercontent.com/amidaware/tacticalrmm/master/restore.sh'
sudo apt update
@@ -13,7 +13,7 @@ RED='\033[0;31m'
NC='\033[0m'
SCRIPTS_DIR='/opt/trmm-community-scripts'
PYTHON_VER='3.11.6'
PYTHON_VER='3.11.8'
SETTINGS_FILE='/rmm/api/tacticalrmm/tacticalrmm/settings.py'
TMP_FILE=$(mktemp -p "" "rmmrestore_XXXXXXXXXX")
@@ -29,6 +29,11 @@ fi
rm -f $TMP_FILE
if [ -d /rmm/api/tacticalrmm ]; then
echo -ne "${RED}ERROR: Existing trmm installation found. The restore script must be run on a clean server, please re-read the docs.${NC}\n"
exit 1
fi
arch=$(uname -m)
if [[ "$arch" != "x86_64" ]] && [[ "$arch" != "aarch64" ]]; then
echo -ne "${RED}ERROR: Only x86_64 and aarch64 is supported, not ${arch}${NC}\n"
@@ -124,7 +129,7 @@ print_green 'Installing NodeJS'
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
NODE_MAJOR=18
NODE_MAJOR=20
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list
sudo apt update
sudo apt install -y gcc g++ make
@@ -164,7 +169,7 @@ http {
sendfile on;
tcp_nopush on;
types_hash_max_size 2048;
server_names_hash_bucket_size 64;
server_names_hash_bucket_size 256;
include /etc/nginx/mime.types;
default_type application/octet-stream;
ssl_protocols TLSv1.2 TLSv1.3;
@@ -353,14 +358,14 @@ fi
print_green 'Creating MeshCentral DB'
sudo -u postgres psql -c "CREATE DATABASE meshcentral"
sudo -u postgres psql -c "CREATE USER ${MESH_POSTGRES_USER} WITH PASSWORD '${MESH_POSTGRES_PW}'"
sudo -u postgres psql -c "ALTER ROLE ${MESH_POSTGRES_USER} SET client_encoding TO 'utf8'"
sudo -u postgres psql -c "ALTER ROLE ${MESH_POSTGRES_USER} SET default_transaction_isolation TO 'read committed'"
sudo -u postgres psql -c "ALTER ROLE ${MESH_POSTGRES_USER} SET timezone TO 'UTC'"
sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE meshcentral TO ${MESH_POSTGRES_USER}"
sudo -u postgres psql -c "ALTER DATABASE meshcentral OWNER TO ${MESH_POSTGRES_USER}"
sudo -u postgres psql -c "GRANT USAGE, CREATE ON SCHEMA PUBLIC TO ${MESH_POSTGRES_USER}"
sudo -iu postgres psql -c "CREATE DATABASE meshcentral"
sudo -iu postgres psql -c "CREATE USER ${MESH_POSTGRES_USER} WITH PASSWORD '${MESH_POSTGRES_PW}'"
sudo -iu postgres psql -c "ALTER ROLE ${MESH_POSTGRES_USER} SET client_encoding TO 'utf8'"
sudo -iu postgres psql -c "ALTER ROLE ${MESH_POSTGRES_USER} SET default_transaction_isolation TO 'read committed'"
sudo -iu postgres psql -c "ALTER ROLE ${MESH_POSTGRES_USER} SET timezone TO 'UTC'"
sudo -iu postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE meshcentral TO ${MESH_POSTGRES_USER}"
sudo -iu postgres psql -c "ALTER DATABASE meshcentral OWNER TO ${MESH_POSTGRES_USER}"
sudo -iu postgres psql -c "GRANT USAGE, CREATE ON SCHEMA PUBLIC TO ${MESH_POSTGRES_USER}"
if [ "$FROM_MONGO" = true ]; then
print_green 'Converting mesh mongo to postgres'
@@ -431,14 +436,14 @@ print_green 'Restoring the trmm database'
pgusername=$(grep -w USER /rmm/api/tacticalrmm/tacticalrmm/local_settings.py | sed 's/^.*: //' | sed 's/.//' | sed -r 's/.{2}$//')
pgpw=$(grep -w PASSWORD /rmm/api/tacticalrmm/tacticalrmm/local_settings.py | sed 's/^.*: //' | sed 's/.//' | sed -r 's/.{2}$//')
sudo -u postgres psql -c "CREATE DATABASE tacticalrmm"
sudo -u postgres psql -c "CREATE USER ${pgusername} WITH PASSWORD '${pgpw}'"
sudo -u postgres psql -c "ALTER ROLE ${pgusername} SET client_encoding TO 'utf8'"
sudo -u postgres psql -c "ALTER ROLE ${pgusername} SET default_transaction_isolation TO 'read committed'"
sudo -u postgres psql -c "ALTER ROLE ${pgusername} SET timezone TO 'UTC'"
sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE tacticalrmm TO ${pgusername}"
sudo -u postgres psql -c "ALTER DATABASE tacticalrmm OWNER TO ${pgusername}"
sudo -u postgres psql -c "GRANT USAGE, CREATE ON SCHEMA PUBLIC TO ${pgusername}"
sudo -iu postgres psql -c "CREATE DATABASE tacticalrmm"
sudo -iu postgres psql -c "CREATE USER ${pgusername} WITH PASSWORD '${pgpw}'"
sudo -iu postgres psql -c "ALTER ROLE ${pgusername} SET client_encoding TO 'utf8'"
sudo -iu postgres psql -c "ALTER ROLE ${pgusername} SET default_transaction_isolation TO 'read committed'"
sudo -iu postgres psql -c "ALTER ROLE ${pgusername} SET timezone TO 'UTC'"
sudo -iu postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE tacticalrmm TO ${pgusername}"
sudo -iu postgres psql -c "ALTER DATABASE tacticalrmm OWNER TO ${pgusername}"
sudo -iu postgres psql -c "GRANT USAGE, CREATE ON SCHEMA PUBLIC TO ${pgusername}"
gzip -d $tmp_dir/postgres/db*.psql.gz
PGPASSWORD=${pgpw} psql -h localhost -U ${pgusername} -d tacticalrmm -f $tmp_dir/postgres/db*.psql

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env bash
SCRIPT_VERSION="149"
SCRIPT_VERSION="152"
SCRIPT_URL='https://raw.githubusercontent.com/amidaware/tacticalrmm/master/update.sh'
LATEST_SETTINGS_URL='https://raw.githubusercontent.com/amidaware/tacticalrmm/master/api/tacticalrmm/tacticalrmm/settings.py'
YELLOW='\033[1;33m'
@@ -10,8 +10,9 @@ NC='\033[0m'
THIS_SCRIPT=$(readlink -f "$0")
SCRIPTS_DIR='/opt/trmm-community-scripts'
PYTHON_VER='3.11.6'
PYTHON_VER='3.11.8'
SETTINGS_FILE='/rmm/api/tacticalrmm/tacticalrmm/settings.py'
local_settings='/rmm/api/tacticalrmm/tacticalrmm/local_settings.py'
TMP_FILE=$(mktemp -p "" "rmmupdate_XXXXXXXXXX")
curl -s -L "${SCRIPT_URL}" >${TMP_FILE}
@@ -186,7 +187,7 @@ if ! [[ $CHECK_NGINX_NOLIMIT ]]; then
/' $nginxdefaultconf
fi
sudo sed -i 's/# server_names_hash_bucket_size.*/server_names_hash_bucket_size 64;/g' $nginxdefaultconf
sudo sed -i 's/# server_names_hash_bucket_size.*/server_names_hash_bucket_size 256;/g' $nginxdefaultconf
if ! sudo nginx -t >/dev/null 2>&1; then
sudo nginx -t
@@ -245,8 +246,54 @@ if [ -d ~/.config ]; then
sudo chown -R $USER:$GROUP ~/.config
fi
if ! which npm >/dev/null; then
sudo apt install -y npm
fi
# older distros still might not have npm after above command, due to recent changes to node apt packages which replaces nodesource with official node
# if we still don't have npm, force a switch to nodesource
if ! which npm >/dev/null; then
sudo systemctl stop meshcentral
sudo chown ${USER}:${USER} -R /meshcentral
sudo apt remove -y nodejs
sudo rm -rf /usr/lib/node_modules
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - && sudo apt-get install -y nodejs
sudo npm install -g npm
cd /meshcentral
rm -rf node_modules/ package-lock.json
npm install
sudo systemctl start meshcentral
fi
sudo npm install -g npm
CURRENT_MESH_VER=$(cd /meshcentral/node_modules/meshcentral && node -p -e "require('./package.json').version")
if [[ "${CURRENT_MESH_VER}" != "${LATEST_MESH_VER}" ]] || [[ "$force" = true ]]; then
printf >&2 "${GREEN}Updating meshcentral from ${CURRENT_MESH_VER} to ${LATEST_MESH_VER}${NC}\n"
sudo systemctl stop meshcentral
sudo chown ${USER}:${USER} -R /meshcentral
cd /meshcentral
rm -rf node_modules/ package.json package-lock.json
mesh_pkg="$(
cat <<EOF
{
"dependencies": {
"archiver": "5.3.1",
"meshcentral": "${LATEST_MESH_VER}",
"otplib": "10.2.3",
"pg": "8.7.1",
"pgtools": "0.3.2"
}
}
EOF
)"
echo "${mesh_pkg}" >/meshcentral/package.json
npm install
sudo systemctl start meshcentral
fi
# update from main repo
cd /rmm
git config user.email "admin@example.com"
@@ -294,14 +341,14 @@ if ! [[ $CHECK_CELERY_CONFIG ]]; then
sed -i 's/CELERYD_OPTS=.*/CELERYD_OPTS="--time-limit=86400 --autoscale=20,2"/g' /etc/conf.d/celery.conf
fi
CHECK_ADMIN_ENABLED=$(grep ADMIN_ENABLED /rmm/api/tacticalrmm/tacticalrmm/local_settings.py)
CHECK_ADMIN_ENABLED=$(grep ADMIN_ENABLED $local_settings)
if ! [[ $CHECK_ADMIN_ENABLED ]]; then
adminenabled="$(
cat <<EOF
ADMIN_ENABLED = False
EOF
)"
echo "${adminenabled}" | tee --append /rmm/api/tacticalrmm/tacticalrmm/local_settings.py >/dev/null
echo "${adminenabled}" | tee --append $local_settings >/dev/null
fi
if [ "$arch" = "x86_64" ]; then
@@ -338,10 +385,13 @@ if [ ! -d /opt/tactical/reporting/schemas ]; then
sudo mkdir /opt/tactical/reporting/schemas
fi
sed -i '/^REDIS_HOST/d' $local_settings
sudo chown -R ${USER}:${USER} /opt/tactical
python manage.py pre_update_tasks
celery -A tacticalrmm purge -f
printf >&2 "${GREEN}Running database migrations (this might take a long time)...${NC}\n"
python manage.py migrate
python manage.py generate_json_schemas
python manage.py delete_tokens
@@ -506,30 +556,5 @@ for i in nats nats-api rmm daphne celery celerybeat nginx; do
sudo systemctl start ${i}
done
CURRENT_MESH_VER=$(cd /meshcentral/node_modules/meshcentral && node -p -e "require('./package.json').version")
if [[ "${CURRENT_MESH_VER}" != "${LATEST_MESH_VER}" ]] || [[ "$force" = true ]]; then
printf >&2 "${GREEN}Updating meshcentral from ${CURRENT_MESH_VER} to ${LATEST_MESH_VER}${NC}\n"
sudo systemctl stop meshcentral
sudo chown ${USER}:${USER} -R /meshcentral
cd /meshcentral
rm -rf node_modules/ package.json package-lock.json
mesh_pkg="$(
cat <<EOF
{
"dependencies": {
"archiver": "5.3.1",
"meshcentral": "${LATEST_MESH_VER}",
"otplib": "10.2.3",
"pg": "8.7.1",
"pgtools": "0.3.2"
}
}
EOF
)"
echo "${mesh_pkg}" >/meshcentral/package.json
npm install
sudo systemctl start meshcentral
fi
rm -f $TMP_SETTINGS
printf >&2 "${GREEN}Update finished!${NC}\n"