Compare commits
295 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba42c5e367 | ||
|
|
6a06734192 | ||
|
|
5e26a406b7 | ||
|
|
b6dd03138d | ||
|
|
cf03ee03ee | ||
|
|
0e665b6bf0 | ||
|
|
e3d0de7313 | ||
|
|
bcf3a543a1 | ||
|
|
b27f17c74a | ||
|
|
75d864771e | ||
|
|
6420060f2a | ||
|
|
c149ae71b9 | ||
|
|
3a49dd034c | ||
|
|
b26d7e82e3 | ||
|
|
415abdf0ce | ||
|
|
f7f6f6ecb2 | ||
|
|
43d54f134a | ||
|
|
0d2606a13b | ||
|
|
1deb10dc88 | ||
|
|
1236d55544 | ||
|
|
ecccf39455 | ||
|
|
8e0316825a | ||
|
|
aa45fa87af | ||
|
|
71e78bd0c5 | ||
|
|
4766477c58 | ||
|
|
d97e49ff2b | ||
|
|
6b9d775cb9 | ||
|
|
e521f580d7 | ||
|
|
25e7cf7db0 | ||
|
|
0cab33787d | ||
|
|
bc6faf817f | ||
|
|
d46ae55863 | ||
|
|
bbd900ab25 | ||
|
|
129ae93e2b | ||
|
|
44dd59fa3f | ||
|
|
ec4e7559b0 | ||
|
|
dce40611cf | ||
|
|
e71b8546f9 | ||
|
|
f827348467 | ||
|
|
f3978343db | ||
|
|
2654a7ea70 | ||
|
|
1068bf4ef7 | ||
|
|
e7fccc97cc | ||
|
|
733e289852 | ||
|
|
29d71a104c | ||
|
|
05200420ad | ||
|
|
eb762d4bfd | ||
|
|
58ace9eda1 | ||
|
|
eeb2623be0 | ||
|
|
cfa242c2fe | ||
|
|
ec0441ccc2 | ||
|
|
ae2782a8fe | ||
|
|
58ff570251 | ||
|
|
7b554b12c7 | ||
|
|
58f7603d4f | ||
|
|
8895994c54 | ||
|
|
de8f7e36d5 | ||
|
|
88d7a50265 | ||
|
|
21e19fc7e5 | ||
|
|
faf4935a69 | ||
|
|
71a1f9d74a | ||
|
|
bd8d523e10 | ||
|
|
60cae0e3ac | ||
|
|
5a342ac012 | ||
|
|
bb8767dfc3 | ||
|
|
fcb2779c15 | ||
|
|
77dd6c1f61 | ||
|
|
8118eef300 | ||
|
|
802d1489fe | ||
|
|
443a029185 | ||
|
|
4ee508fdd0 | ||
|
|
aa5608f7e8 | ||
|
|
cc472b4613 | ||
|
|
764b945ddc | ||
|
|
fd2206ce4c | ||
|
|
48c0ac9f00 | ||
|
|
84eb4fe9ed | ||
|
|
4a5428812c | ||
|
|
023f98a89d | ||
|
|
66893dd0c1 | ||
|
|
25a6666e35 | ||
|
|
19d75309b5 | ||
|
|
11110d65c1 | ||
|
|
a348f58fe2 | ||
|
|
13851dd976 | ||
|
|
2ec37c5da9 | ||
|
|
8c127160de | ||
|
|
2af820de9a | ||
|
|
55fb0bb3a0 | ||
|
|
9f9ecc521f | ||
|
|
dfd01df5ba | ||
|
|
474090698c | ||
|
|
6b71cdeea4 | ||
|
|
581e974236 | ||
|
|
ba3c3a42ce | ||
|
|
c8bc5671c5 | ||
|
|
ff9401a040 | ||
|
|
5e1bc1989f | ||
|
|
a1dc91cd7d | ||
|
|
99f2772bb3 | ||
|
|
e5d0e42655 | ||
|
|
2c914cc374 | ||
|
|
9bceb62381 | ||
|
|
de7518a800 | ||
|
|
304fb63453 | ||
|
|
0f7ef60ca0 | ||
|
|
07c74e4641 | ||
|
|
de7f325cfb | ||
|
|
42cdf70cb4 | ||
|
|
6beb6be131 | ||
|
|
fa4fc2a708 | ||
|
|
2db9758260 | ||
|
|
715982e40a | ||
|
|
d00cd4453a | ||
|
|
429c08c24a | ||
|
|
6a71490e20 | ||
|
|
9bceda0646 | ||
|
|
a1027a6773 | ||
|
|
302d4b75f9 | ||
|
|
5f6ee0e883 | ||
|
|
27f9720de1 | ||
|
|
22aa3fdbbc | ||
|
|
069ecdd33f | ||
|
|
dd545ae933 | ||
|
|
6650b705c4 | ||
|
|
59b0350289 | ||
|
|
1ad159f820 | ||
|
|
0bf42190e9 | ||
|
|
d2fa836232 | ||
|
|
c387774093 | ||
|
|
e99736ba3c | ||
|
|
16cb54fcc9 | ||
|
|
5aa15c51ec | ||
|
|
a8aedd9cf3 | ||
|
|
b851b632bc | ||
|
|
541e07fb65 | ||
|
|
6ad16a897d | ||
|
|
72f1053a93 | ||
|
|
fb15a2762c | ||
|
|
9165248b91 | ||
|
|
add18b29db | ||
|
|
1971653548 | ||
|
|
392cd64d7b | ||
|
|
b5affbb7c8 | ||
|
|
71d1206277 | ||
|
|
26e6a8c409 | ||
|
|
eb54fae11a | ||
|
|
ee773e5966 | ||
|
|
7218ccdba8 | ||
|
|
332400e48a | ||
|
|
ad1a5d3702 | ||
|
|
3006b4184d | ||
|
|
84eb84a080 | ||
|
|
60beea548b | ||
|
|
5f9c149e59 | ||
|
|
53367c6f04 | ||
|
|
d7f817ee44 | ||
|
|
d33a87da54 | ||
|
|
3aebfb12b7 | ||
|
|
1d6c55ffa6 | ||
|
|
5e7080aac3 | ||
|
|
fad739bc01 | ||
|
|
c6b7f23884 | ||
|
|
a6f7e446de | ||
|
|
89d95d3ae1 | ||
|
|
764208698f | ||
|
|
57129cf934 | ||
|
|
aae1a842d5 | ||
|
|
623f35aec7 | ||
|
|
870bf842cf | ||
|
|
07f2d7dd5c | ||
|
|
f223f2edc5 | ||
|
|
e848a9a577 | ||
|
|
7569d98e07 | ||
|
|
596dee2f24 | ||
|
|
9970403964 | ||
|
|
07a88ae00d | ||
|
|
5475b4d287 | ||
|
|
6631dcfd3e | ||
|
|
0dd3f337f3 | ||
|
|
8eb27b5875 | ||
|
|
2d1863031c | ||
|
|
9feb76ca81 | ||
|
|
993e8f4ab3 | ||
|
|
e08ae95d4f | ||
|
|
15359e8846 | ||
|
|
d1457b312b | ||
|
|
c9dd2af196 | ||
|
|
564ef4e688 | ||
|
|
a33e6e8bb5 | ||
|
|
cf34f33f04 | ||
|
|
827cfe4e8f | ||
|
|
2ce1c2383c | ||
|
|
6fc0a665ae | ||
|
|
4f16d01263 | ||
|
|
67cc37354a | ||
|
|
e388243ef4 | ||
|
|
3dc92763c7 | ||
|
|
dfe97dd466 | ||
|
|
2803cee29b | ||
|
|
3a03020e54 | ||
|
|
64443cc703 | ||
|
|
4d1aa6ed18 | ||
|
|
84837e88d2 | ||
|
|
ff49c936ea | ||
|
|
e6e0901329 | ||
|
|
23b6284b51 | ||
|
|
33dfbcbe32 | ||
|
|
700c23d537 | ||
|
|
369fac9e38 | ||
|
|
2229eb1167 | ||
|
|
a3dec841b6 | ||
|
|
b17620bdb6 | ||
|
|
f39cd5ae2f | ||
|
|
83a19e005b | ||
|
|
a9dd01b0c8 | ||
|
|
eb59afa1d1 | ||
|
|
2adcfce9d0 | ||
|
|
314ab9b304 | ||
|
|
8576fb82c7 | ||
|
|
0f95a6bb2f | ||
|
|
ad5104567d | ||
|
|
ece68ba1d5 | ||
|
|
acccd3a586 | ||
|
|
8ebef1c1ca | ||
|
|
28abc0d5ed | ||
|
|
1efe25d3ec | ||
|
|
c40e4f8e4b | ||
|
|
baca84092d | ||
|
|
346d4da059 | ||
|
|
ade64d6c0a | ||
|
|
8204bdfc5f | ||
|
|
1a9bb3e986 | ||
|
|
49356479e5 | ||
|
|
c44e9a7292 | ||
|
|
21771a593f | ||
|
|
84458dfc4c | ||
|
|
5835632dab | ||
|
|
67aa7229ef | ||
|
|
b72dc3ed3a | ||
|
|
0f93d4a5bd | ||
|
|
106320b035 | ||
|
|
63951705cd | ||
|
|
a8d56921d5 | ||
|
|
10bc133cf1 | ||
|
|
adeb5b35c9 | ||
|
|
589ff46ea5 | ||
|
|
656fcb9fe7 | ||
|
|
1cb9353006 | ||
|
|
57bf16ba07 | ||
|
|
659846ed88 | ||
|
|
25894044e0 | ||
|
|
e7a0826beb | ||
|
|
1f7ddee23b | ||
|
|
7e186730db | ||
|
|
6713a50208 | ||
|
|
7c9d8fcfec | ||
|
|
33bfc8cfe8 | ||
|
|
ca735bc14a | ||
|
|
4ba748a18b | ||
|
|
f1845106f8 | ||
|
|
67e7156c4b | ||
|
|
4a476adebf | ||
|
|
918798f8cc | ||
|
|
5a3f868866 | ||
|
|
feea2c6396 | ||
|
|
707b4c46d9 | ||
|
|
89ca39fc2b | ||
|
|
204281b12d | ||
|
|
a8538a7e95 | ||
|
|
dee1b471e9 | ||
|
|
aa04e9b01f | ||
|
|
350f0dc604 | ||
|
|
6021f2efd6 | ||
|
|
51838ec25a | ||
|
|
54768a121e | ||
|
|
8ff72cdca3 | ||
|
|
2cb53ad06b | ||
|
|
b8349de31d | ||
|
|
d7e11af7f8 | ||
|
|
dd8d39e698 | ||
|
|
afb1316daa | ||
|
|
04d7017536 | ||
|
|
6a1c75b060 | ||
|
|
5c94611f3b | ||
|
|
4e5676e80f | ||
|
|
c96d688a9c | ||
|
|
804242e9a5 | ||
|
|
0ec9760b17 | ||
|
|
d481ae3da4 | ||
|
|
4742c14fc1 | ||
|
|
509b0d501b | ||
|
|
d4c9b04d4e | ||
|
|
16fb4d331b | ||
|
|
e9e5bf31a7 |
@@ -26,3 +26,6 @@ POSTGRES_PASS=postgrespass
|
||||
APP_PORT=80
|
||||
API_PORT=80
|
||||
HTTP_PROTOCOL=https
|
||||
DOCKER_NETWORK="172.21.0.0/24"
|
||||
DOCKER_NGINX_IP="172.21.0.20"
|
||||
NATS_PORTS="4222:4222"
|
||||
|
||||
@@ -46,7 +46,7 @@ services:
|
||||
API_PORT: ${API_PORT}
|
||||
DEV: 1
|
||||
ports:
|
||||
- "4222:4222"
|
||||
- "${NATS_PORTS}"
|
||||
volumes:
|
||||
- tactical-data-dev:/opt/tactical
|
||||
- ..:/workspace:cached
|
||||
@@ -67,7 +67,7 @@ services:
|
||||
MESH_PASS: ${MESH_PASS}
|
||||
MONGODB_USER: ${MONGODB_USER}
|
||||
MONGODB_PASSWORD: ${MONGODB_PASSWORD}
|
||||
NGINX_HOST_IP: 172.21.0.20
|
||||
NGINX_HOST_IP: ${DOCKER_NGINX_IP}
|
||||
networks:
|
||||
dev:
|
||||
aliases:
|
||||
@@ -115,7 +115,10 @@ services:
|
||||
redis-dev:
|
||||
container_name: trmm-redis-dev
|
||||
restart: always
|
||||
command: redis-server --appendonly yes
|
||||
image: redis:6.0-alpine
|
||||
volumes:
|
||||
- redis-data-dev:/data
|
||||
networks:
|
||||
dev:
|
||||
aliases:
|
||||
@@ -220,7 +223,7 @@ services:
|
||||
API_PORT: ${API_PORT}
|
||||
networks:
|
||||
dev:
|
||||
ipv4_address: 172.21.0.20
|
||||
ipv4_address: ${DOCKER_NGINX_IP}
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
@@ -247,6 +250,7 @@ volumes:
|
||||
postgres-data-dev:
|
||||
mongo-dev-data:
|
||||
mesh-data-dev:
|
||||
redis-data-dev:
|
||||
|
||||
networks:
|
||||
dev:
|
||||
@@ -254,4 +258,4 @@ networks:
|
||||
ipam:
|
||||
driver: default
|
||||
config:
|
||||
- subnet: 172.21.0.0/24
|
||||
- subnet: ${DOCKER_NETWORK}
|
||||
|
||||
@@ -114,6 +114,7 @@ EOF
|
||||
"${VIRTUAL_ENV}"/bin/python manage.py load_chocos
|
||||
"${VIRTUAL_ENV}"/bin/python manage.py load_community_scripts
|
||||
"${VIRTUAL_ENV}"/bin/python manage.py reload_nats
|
||||
"${VIRTUAL_ENV}"/bin/python manage.py create_installer_user
|
||||
|
||||
# create super user
|
||||
echo "from accounts.models import User; User.objects.create_superuser('${TRMM_USER}', 'admin@example.com', '${TRMM_PASS}') if not User.objects.filter(username='${TRMM_USER}').exists() else 0;" | python manage.py shell
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
asyncio-nats-client
|
||||
celery
|
||||
channels
|
||||
channels_redis
|
||||
Django
|
||||
django-cors-headers
|
||||
django-rest-knox
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -47,3 +47,4 @@ docs/.vuepress/dist
|
||||
nats-rmm.conf
|
||||
.mypy_cache
|
||||
docs/site/
|
||||
reset_db.sh
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from django.contrib import admin
|
||||
from rest_framework.authtoken.admin import TokenAdmin
|
||||
|
||||
from .models import User
|
||||
from .models import User, Role
|
||||
|
||||
admin.site.register(User)
|
||||
TokenAdmin.raw_id_fields = ("user",)
|
||||
admin.site.register(Role)
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import uuid
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from accounts.models import User
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Creates the installer user"
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
if User.objects.filter(is_installer_user=True).exists():
|
||||
return
|
||||
|
||||
User.objects.create_user( # type: ignore
|
||||
username=uuid.uuid4().hex,
|
||||
is_installer_user=True,
|
||||
password=User.objects.make_random_password(60), # type: ignore
|
||||
)
|
||||
@@ -0,0 +1,25 @@
|
||||
# Generated by Django 3.2.1 on 2021-05-07 15:26
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0022_urlaction'),
|
||||
('accounts', '0015_user_loading_bar_color'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='url_action',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='user', to='core.urlaction'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='agent_dblclick_action',
|
||||
field=models.CharField(choices=[('editagent', 'Edit Agent'), ('takecontrol', 'Take Control'), ('remotebg', 'Remote Background'), ('urlaction', 'URL Action')], default='editagent', max_length=50),
|
||||
),
|
||||
]
|
||||
173
api/tacticalrmm/accounts/migrations/0017_auto_20210508_1716.py
Normal file
173
api/tacticalrmm/accounts/migrations/0017_auto_20210508_1716.py
Normal file
@@ -0,0 +1,173 @@
|
||||
# Generated by Django 3.2.1 on 2021-05-08 17:16
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0016_auto_20210507_1526'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_code_sign',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_do_server_maint',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_edit_agent',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_edit_core_settings',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_install_agents',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_manage_accounts',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_manage_alerts',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_manage_automation_policies',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_manage_autotasks',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_manage_checks',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_manage_clients',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_manage_deployments',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_manage_notes',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_manage_pendingactions',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_manage_procs',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_manage_scripts',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_manage_sites',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_manage_software',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_manage_winsvcs',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_manage_winupdates',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_reboot_agents',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_run_autotasks',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_run_bulk',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_run_checks',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_run_scripts',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_send_cmd',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_uninstall_agents',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_update_agents',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_use_mesh',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_view_auditlogs',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_view_debuglogs',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_view_eventlogs',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
181
api/tacticalrmm/accounts/migrations/0018_auto_20210511_0233.py
Normal file
181
api/tacticalrmm/accounts/migrations/0018_auto_20210511_0233.py
Normal file
@@ -0,0 +1,181 @@
|
||||
# Generated by Django 3.2.1 on 2021-05-11 02:33
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0017_auto_20210508_1716'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Role',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255, unique=True)),
|
||||
('is_superuser', models.BooleanField(default=False)),
|
||||
('can_use_mesh', models.BooleanField(default=False)),
|
||||
('can_uninstall_agents', models.BooleanField(default=False)),
|
||||
('can_update_agents', models.BooleanField(default=False)),
|
||||
('can_edit_agent', models.BooleanField(default=False)),
|
||||
('can_manage_procs', models.BooleanField(default=False)),
|
||||
('can_view_eventlogs', models.BooleanField(default=False)),
|
||||
('can_send_cmd', models.BooleanField(default=False)),
|
||||
('can_reboot_agents', models.BooleanField(default=False)),
|
||||
('can_install_agents', models.BooleanField(default=False)),
|
||||
('can_run_scripts', models.BooleanField(default=False)),
|
||||
('can_run_bulk', models.BooleanField(default=False)),
|
||||
('can_manage_notes', models.BooleanField(default=False)),
|
||||
('can_edit_core_settings', models.BooleanField(default=False)),
|
||||
('can_do_server_maint', models.BooleanField(default=False)),
|
||||
('can_code_sign', models.BooleanField(default=False)),
|
||||
('can_manage_checks', models.BooleanField(default=False)),
|
||||
('can_run_checks', models.BooleanField(default=False)),
|
||||
('can_manage_clients', models.BooleanField(default=False)),
|
||||
('can_manage_sites', models.BooleanField(default=False)),
|
||||
('can_manage_deployments', models.BooleanField(default=False)),
|
||||
('can_manage_automation_policies', models.BooleanField(default=False)),
|
||||
('can_manage_autotasks', models.BooleanField(default=False)),
|
||||
('can_run_autotasks', models.BooleanField(default=False)),
|
||||
('can_view_auditlogs', models.BooleanField(default=False)),
|
||||
('can_manage_pendingactions', models.BooleanField(default=False)),
|
||||
('can_view_debuglogs', models.BooleanField(default=False)),
|
||||
('can_manage_scripts', models.BooleanField(default=False)),
|
||||
('can_manage_alerts', models.BooleanField(default=False)),
|
||||
('can_manage_winsvcs', models.BooleanField(default=False)),
|
||||
('can_manage_software', models.BooleanField(default=False)),
|
||||
('can_manage_winupdates', models.BooleanField(default=False)),
|
||||
('can_manage_accounts', models.BooleanField(default=False)),
|
||||
],
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_code_sign',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_do_server_maint',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_edit_agent',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_edit_core_settings',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_install_agents',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_manage_accounts',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_manage_alerts',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_manage_automation_policies',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_manage_autotasks',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_manage_checks',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_manage_clients',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_manage_deployments',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_manage_notes',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_manage_pendingactions',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_manage_procs',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_manage_scripts',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_manage_sites',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_manage_software',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_manage_winsvcs',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_manage_winupdates',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_reboot_agents',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_run_autotasks',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_run_bulk',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_run_checks',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_run_scripts',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_send_cmd',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_uninstall_agents',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_update_agents',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_use_mesh',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_view_auditlogs',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_view_debuglogs',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_view_eventlogs',
|
||||
),
|
||||
]
|
||||
25
api/tacticalrmm/accounts/migrations/0019_user_role.py
Normal file
25
api/tacticalrmm/accounts/migrations/0019_user_role.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# Generated by Django 3.2.1 on 2021-05-11 02:33
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("accounts", "0018_auto_20210511_0233"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="user",
|
||||
name="role",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="roles",
|
||||
to="accounts.role",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.1 on 2021-05-11 17:37
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0019_user_role'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='role',
|
||||
name='can_manage_roles',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.4 on 2021-06-17 04:29
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0020_role_can_manage_roles'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='role',
|
||||
name='can_view_core_settings',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.4 on 2021-06-28 05:01
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0021_role_can_view_core_settings'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='clear_search_when_switching',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.4 on 2021-06-30 03:22
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0022_user_clear_search_when_switching'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='is_installer_user',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -7,6 +7,7 @@ AGENT_DBLCLICK_CHOICES = [
|
||||
("editagent", "Edit Agent"),
|
||||
("takecontrol", "Take Control"),
|
||||
("remotebg", "Remote Background"),
|
||||
("urlaction", "URL Action"),
|
||||
]
|
||||
|
||||
AGENT_TBL_TAB_CHOICES = [
|
||||
@@ -29,6 +30,13 @@ class User(AbstractUser, BaseAuditModel):
|
||||
agent_dblclick_action = models.CharField(
|
||||
max_length=50, choices=AGENT_DBLCLICK_CHOICES, default="editagent"
|
||||
)
|
||||
url_action = models.ForeignKey(
|
||||
"core.URLAction",
|
||||
related_name="user",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
)
|
||||
default_agent_tbl_tab = models.CharField(
|
||||
max_length=50, choices=AGENT_TBL_TAB_CHOICES, default="server"
|
||||
)
|
||||
@@ -38,6 +46,8 @@ class User(AbstractUser, BaseAuditModel):
|
||||
)
|
||||
client_tree_splitter = models.PositiveIntegerField(default=11)
|
||||
loading_bar_color = models.CharField(max_length=255, default="red")
|
||||
clear_search_when_switching = models.BooleanField(default=True)
|
||||
is_installer_user = models.BooleanField(default=False)
|
||||
|
||||
agent = models.OneToOneField(
|
||||
"agents.Agent",
|
||||
@@ -47,9 +57,125 @@ class User(AbstractUser, BaseAuditModel):
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
|
||||
role = models.ForeignKey(
|
||||
"accounts.Role",
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="roles",
|
||||
on_delete=models.SET_NULL,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def serialize(user):
|
||||
# serializes the task and returns json
|
||||
from .serializers import UserSerializer
|
||||
|
||||
return UserSerializer(user).data
|
||||
|
||||
|
||||
class Role(models.Model):
|
||||
name = models.CharField(max_length=255, unique=True)
|
||||
is_superuser = models.BooleanField(default=False)
|
||||
|
||||
# agents
|
||||
can_use_mesh = models.BooleanField(default=False)
|
||||
can_uninstall_agents = models.BooleanField(default=False)
|
||||
can_update_agents = models.BooleanField(default=False)
|
||||
can_edit_agent = models.BooleanField(default=False)
|
||||
can_manage_procs = models.BooleanField(default=False)
|
||||
can_view_eventlogs = models.BooleanField(default=False)
|
||||
can_send_cmd = models.BooleanField(default=False)
|
||||
can_reboot_agents = models.BooleanField(default=False)
|
||||
can_install_agents = models.BooleanField(default=False)
|
||||
can_run_scripts = models.BooleanField(default=False)
|
||||
can_run_bulk = models.BooleanField(default=False)
|
||||
|
||||
# core
|
||||
can_manage_notes = models.BooleanField(default=False)
|
||||
can_view_core_settings = models.BooleanField(default=False)
|
||||
can_edit_core_settings = models.BooleanField(default=False)
|
||||
can_do_server_maint = models.BooleanField(default=False)
|
||||
can_code_sign = models.BooleanField(default=False)
|
||||
|
||||
# checks
|
||||
can_manage_checks = models.BooleanField(default=False)
|
||||
can_run_checks = models.BooleanField(default=False)
|
||||
|
||||
# clients
|
||||
can_manage_clients = models.BooleanField(default=False)
|
||||
can_manage_sites = models.BooleanField(default=False)
|
||||
can_manage_deployments = models.BooleanField(default=False)
|
||||
|
||||
# automation
|
||||
can_manage_automation_policies = models.BooleanField(default=False)
|
||||
|
||||
# automated tasks
|
||||
can_manage_autotasks = models.BooleanField(default=False)
|
||||
can_run_autotasks = models.BooleanField(default=False)
|
||||
|
||||
# logs
|
||||
can_view_auditlogs = models.BooleanField(default=False)
|
||||
can_manage_pendingactions = models.BooleanField(default=False)
|
||||
can_view_debuglogs = models.BooleanField(default=False)
|
||||
|
||||
# scripts
|
||||
can_manage_scripts = models.BooleanField(default=False)
|
||||
|
||||
# alerts
|
||||
can_manage_alerts = models.BooleanField(default=False)
|
||||
|
||||
# win services
|
||||
can_manage_winsvcs = models.BooleanField(default=False)
|
||||
|
||||
# software
|
||||
can_manage_software = models.BooleanField(default=False)
|
||||
|
||||
# windows updates
|
||||
can_manage_winupdates = models.BooleanField(default=False)
|
||||
|
||||
# accounts
|
||||
can_manage_accounts = models.BooleanField(default=False)
|
||||
can_manage_roles = models.BooleanField(default=False)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@staticmethod
|
||||
def perms():
|
||||
return [
|
||||
"is_superuser",
|
||||
"can_use_mesh",
|
||||
"can_uninstall_agents",
|
||||
"can_update_agents",
|
||||
"can_edit_agent",
|
||||
"can_manage_procs",
|
||||
"can_view_eventlogs",
|
||||
"can_send_cmd",
|
||||
"can_reboot_agents",
|
||||
"can_install_agents",
|
||||
"can_run_scripts",
|
||||
"can_run_bulk",
|
||||
"can_manage_notes",
|
||||
"can_view_core_settings",
|
||||
"can_edit_core_settings",
|
||||
"can_do_server_maint",
|
||||
"can_code_sign",
|
||||
"can_manage_checks",
|
||||
"can_run_checks",
|
||||
"can_manage_clients",
|
||||
"can_manage_sites",
|
||||
"can_manage_deployments",
|
||||
"can_manage_automation_policies",
|
||||
"can_manage_autotasks",
|
||||
"can_run_autotasks",
|
||||
"can_view_auditlogs",
|
||||
"can_manage_pendingactions",
|
||||
"can_view_debuglogs",
|
||||
"can_manage_scripts",
|
||||
"can_manage_alerts",
|
||||
"can_manage_winsvcs",
|
||||
"can_manage_software",
|
||||
"can_manage_winupdates",
|
||||
"can_manage_accounts",
|
||||
"can_manage_roles",
|
||||
]
|
||||
|
||||
19
api/tacticalrmm/accounts/permissions.py
Normal file
19
api/tacticalrmm/accounts/permissions.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from rest_framework import permissions
|
||||
|
||||
from tacticalrmm.permissions import _has_perm
|
||||
|
||||
|
||||
class AccountsPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
if r.method == "GET":
|
||||
return True
|
||||
|
||||
return _has_perm(r, "can_manage_accounts")
|
||||
|
||||
|
||||
class RolesPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
if r.method == "GET":
|
||||
return True
|
||||
|
||||
return _has_perm(r, "can_manage_roles")
|
||||
@@ -1,7 +1,7 @@
|
||||
import pyotp
|
||||
from rest_framework.serializers import ModelSerializer, SerializerMethodField
|
||||
|
||||
from .models import User
|
||||
from .models import User, Role
|
||||
|
||||
|
||||
class UserUISerializer(ModelSerializer):
|
||||
@@ -11,17 +11,19 @@ class UserUISerializer(ModelSerializer):
|
||||
"dark_mode",
|
||||
"show_community_scripts",
|
||||
"agent_dblclick_action",
|
||||
"url_action",
|
||||
"default_agent_tbl_tab",
|
||||
"client_tree_sort",
|
||||
"client_tree_splitter",
|
||||
"loading_bar_color",
|
||||
"clear_search_when_switching",
|
||||
]
|
||||
|
||||
|
||||
class UserSerializer(ModelSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = (
|
||||
fields = [
|
||||
"id",
|
||||
"username",
|
||||
"first_name",
|
||||
@@ -29,7 +31,8 @@ class UserSerializer(ModelSerializer):
|
||||
"email",
|
||||
"is_active",
|
||||
"last_login",
|
||||
)
|
||||
"role",
|
||||
]
|
||||
|
||||
|
||||
class TOTPSetupSerializer(ModelSerializer):
|
||||
@@ -48,3 +51,9 @@ class TOTPSetupSerializer(ModelSerializer):
|
||||
return pyotp.totp.TOTP(obj.totp_key).provisioning_uri(
|
||||
obj.username, issuer_name="Tactical RMM"
|
||||
)
|
||||
|
||||
|
||||
class RoleSerializer(ModelSerializer):
|
||||
class Meta:
|
||||
model = Role
|
||||
fields = "__all__"
|
||||
|
||||
@@ -280,6 +280,7 @@ class TestUserAction(TacticalTestCase):
|
||||
"client_tree_sort": "alpha",
|
||||
"client_tree_splitter": 14,
|
||||
"loading_bar_color": "green",
|
||||
"clear_search_when_switching": False,
|
||||
}
|
||||
r = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
@@ -9,4 +9,7 @@ urlpatterns = [
|
||||
path("users/reset_totp/", views.UserActions.as_view()),
|
||||
path("users/setup_totp/", views.TOTPSetup.as_view()),
|
||||
path("users/ui/", views.UserUI.as_view()),
|
||||
path("permslist/", views.PermsList.as_view()),
|
||||
path("roles/", views.GetAddRoles.as_view()),
|
||||
path("<int:pk>/role/", views.GetUpdateDeleteRole.as_view()),
|
||||
]
|
||||
|
||||
@@ -6,15 +6,21 @@ from django.shortcuts import get_object_or_404
|
||||
from knox.views import LoginView as KnoxLoginView
|
||||
from rest_framework import status
|
||||
from rest_framework.authtoken.serializers import AuthTokenSerializer
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from logs.models import AuditLog
|
||||
from tacticalrmm.utils import notify_error
|
||||
|
||||
from .models import User
|
||||
from .serializers import TOTPSetupSerializer, UserSerializer, UserUISerializer
|
||||
from .models import User, Role
|
||||
from .permissions import AccountsPerms, RolesPerms
|
||||
from .serializers import (
|
||||
TOTPSetupSerializer,
|
||||
UserSerializer,
|
||||
UserUISerializer,
|
||||
RoleSerializer,
|
||||
)
|
||||
|
||||
|
||||
def _is_root_user(request, user) -> bool:
|
||||
@@ -78,8 +84,10 @@ class LoginView(KnoxLoginView):
|
||||
|
||||
|
||||
class GetAddUsers(APIView):
|
||||
permission_classes = [IsAuthenticated, AccountsPerms]
|
||||
|
||||
def get(self, request):
|
||||
users = User.objects.filter(agent=None)
|
||||
users = User.objects.filter(agent=None, is_installer_user=False)
|
||||
|
||||
return Response(UserSerializer(users, many=True).data)
|
||||
|
||||
@@ -98,13 +106,17 @@ class GetAddUsers(APIView):
|
||||
|
||||
user.first_name = request.data["first_name"]
|
||||
user.last_name = request.data["last_name"]
|
||||
# Can be changed once permissions and groups are introduced
|
||||
user.is_superuser = True
|
||||
if "role" in request.data.keys() and isinstance(request.data["role"], int):
|
||||
role = get_object_or_404(Role, pk=request.data["role"])
|
||||
user.role = role
|
||||
|
||||
user.save()
|
||||
return Response(user.username)
|
||||
|
||||
|
||||
class GetUpdateDeleteUser(APIView):
|
||||
permission_classes = [IsAuthenticated, AccountsPerms]
|
||||
|
||||
def get(self, request, pk):
|
||||
user = get_object_or_404(User, pk=pk)
|
||||
|
||||
@@ -133,7 +145,7 @@ class GetUpdateDeleteUser(APIView):
|
||||
|
||||
|
||||
class UserActions(APIView):
|
||||
|
||||
permission_classes = [IsAuthenticated, AccountsPerms]
|
||||
# reset password
|
||||
def post(self, request):
|
||||
user = get_object_or_404(User, pk=request.data["id"])
|
||||
@@ -182,3 +194,42 @@ class UserUI(APIView):
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
return Response("ok")
|
||||
|
||||
|
||||
class PermsList(APIView):
|
||||
def get(self, request):
|
||||
return Response(Role.perms())
|
||||
|
||||
|
||||
class GetAddRoles(APIView):
|
||||
permission_classes = [IsAuthenticated, RolesPerms]
|
||||
|
||||
def get(self, request):
|
||||
roles = Role.objects.all()
|
||||
return Response(RoleSerializer(roles, many=True).data)
|
||||
|
||||
def post(self, request):
|
||||
serializer = RoleSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
return Response("ok")
|
||||
|
||||
|
||||
class GetUpdateDeleteRole(APIView):
|
||||
permission_classes = [IsAuthenticated, RolesPerms]
|
||||
|
||||
def get(self, request, pk):
|
||||
role = get_object_or_404(Role, pk=pk)
|
||||
return Response(RoleSerializer(role).data)
|
||||
|
||||
def put(self, request, pk):
|
||||
role = get_object_or_404(Role, pk=pk)
|
||||
serializer = RoleSerializer(instance=role, data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
return Response("ok")
|
||||
|
||||
def delete(self, request, pk):
|
||||
role = get_object_or_404(Role, pk=pk)
|
||||
role.delete()
|
||||
return Response("ok")
|
||||
|
||||
23
api/tacticalrmm/agents/migrations/0037_auto_20210627_0014.py
Normal file
23
api/tacticalrmm/agents/migrations/0037_auto_20210627_0014.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.2.4 on 2021-06-27 00:14
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('agents', '0036_agent_block_policy_inheritance'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='agent',
|
||||
name='has_patches_pending',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='agent',
|
||||
name='pending_actions_count',
|
||||
field=models.PositiveIntegerField(default=0),
|
||||
),
|
||||
]
|
||||
@@ -64,6 +64,8 @@ class Agent(BaseAuditModel):
|
||||
)
|
||||
maintenance_mode = models.BooleanField(default=False)
|
||||
block_policy_inheritance = models.BooleanField(default=False)
|
||||
pending_actions_count = models.PositiveIntegerField(default=0)
|
||||
has_patches_pending = models.BooleanField(default=False)
|
||||
alert_template = models.ForeignKey(
|
||||
"alerts.AlertTemplate",
|
||||
related_name="agents",
|
||||
@@ -95,10 +97,12 @@ class Agent(BaseAuditModel):
|
||||
# check if new agent has been created
|
||||
# or check if policy have changed on agent
|
||||
# or if site has changed on agent and if so generate-policies
|
||||
# or if agent was changed from server or workstation
|
||||
if (
|
||||
not old_agent
|
||||
or (old_agent and old_agent.policy != self.policy)
|
||||
or (old_agent.site != self.site)
|
||||
or (old_agent.monitoring_type != self.monitoring_type)
|
||||
or (old_agent.block_policy_inheritance != self.block_policy_inheritance)
|
||||
):
|
||||
self.generate_checks_from_policies()
|
||||
@@ -161,10 +165,6 @@ class Agent(BaseAuditModel):
|
||||
else:
|
||||
return "offline"
|
||||
|
||||
@property
|
||||
def has_patches_pending(self):
|
||||
return self.winupdates.filter(action="approve").filter(installed=False).exists() # type: ignore
|
||||
|
||||
@property
|
||||
def checks(self):
|
||||
total, passing, failing, warning, info = 0, 0, 0, 0, 0
|
||||
@@ -263,6 +263,11 @@ class Agent(BaseAuditModel):
|
||||
make = [x["Manufacturer"] for x in mobo if "Manufacturer" in x][0]
|
||||
model = [x["Product"] for x in mobo if "Product" in x][0]
|
||||
|
||||
if make.lower() == "lenovo":
|
||||
sysfam = [x["SystemFamily"] for x in comp_sys if "SystemFamily" in x][0]
|
||||
if "to be filled" not in sysfam.lower():
|
||||
model = sysfam
|
||||
|
||||
return f"{make} {model}"
|
||||
except:
|
||||
pass
|
||||
|
||||
63
api/tacticalrmm/agents/permissions.py
Normal file
63
api/tacticalrmm/agents/permissions.py
Normal file
@@ -0,0 +1,63 @@
|
||||
from rest_framework import permissions
|
||||
|
||||
from tacticalrmm.permissions import _has_perm
|
||||
|
||||
|
||||
class MeshPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
return _has_perm(r, "can_use_mesh")
|
||||
|
||||
|
||||
class UninstallPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
return _has_perm(r, "can_uninstall_agents")
|
||||
|
||||
|
||||
class UpdateAgentPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
return _has_perm(r, "can_update_agents")
|
||||
|
||||
|
||||
class EditAgentPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
return _has_perm(r, "can_edit_agent")
|
||||
|
||||
|
||||
class ManageProcPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
return _has_perm(r, "can_manage_procs")
|
||||
|
||||
|
||||
class EvtLogPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
return _has_perm(r, "can_view_eventlogs")
|
||||
|
||||
|
||||
class SendCMDPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
return _has_perm(r, "can_send_cmd")
|
||||
|
||||
|
||||
class RebootAgentPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
return _has_perm(r, "can_reboot_agents")
|
||||
|
||||
|
||||
class InstallAgentPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
return _has_perm(r, "can_install_agents")
|
||||
|
||||
|
||||
class RunScriptPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
return _has_perm(r, "can_run_scripts")
|
||||
|
||||
|
||||
class ManageNotesPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
return _has_perm(r, "can_manage_notes")
|
||||
|
||||
|
||||
class RunBulkPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
return _has_perm(r, "can_run_bulk")
|
||||
@@ -9,7 +9,6 @@ from .models import Agent, AgentCustomField, Note
|
||||
|
||||
class AgentSerializer(serializers.ModelSerializer):
|
||||
# for vue
|
||||
patches_pending = serializers.ReadOnlyField(source="has_patches_pending")
|
||||
winupdatepolicy = WinUpdatePolicySerializer(many=True, read_only=True)
|
||||
status = serializers.ReadOnlyField()
|
||||
cpu_model = serializers.ReadOnlyField()
|
||||
@@ -45,8 +44,6 @@ class AgentOverdueActionSerializer(serializers.ModelSerializer):
|
||||
|
||||
|
||||
class AgentTableSerializer(serializers.ModelSerializer):
|
||||
patches_pending = serializers.ReadOnlyField(source="has_patches_pending")
|
||||
pending_actions = serializers.SerializerMethodField()
|
||||
status = serializers.ReadOnlyField()
|
||||
checks = serializers.ReadOnlyField()
|
||||
last_seen = serializers.SerializerMethodField()
|
||||
@@ -69,9 +66,6 @@ class AgentTableSerializer(serializers.ModelSerializer):
|
||||
"always_alert": obj.alert_template.agent_always_alert,
|
||||
}
|
||||
|
||||
def get_pending_actions(self, obj):
|
||||
return obj.pendingactions.filter(status="pending").count()
|
||||
|
||||
def get_last_seen(self, obj) -> str:
|
||||
if obj.time_zone is not None:
|
||||
agent_tz = pytz.timezone(obj.time_zone)
|
||||
@@ -103,8 +97,8 @@ class AgentTableSerializer(serializers.ModelSerializer):
|
||||
"monitoring_type",
|
||||
"description",
|
||||
"needs_reboot",
|
||||
"patches_pending",
|
||||
"pending_actions",
|
||||
"has_patches_pending",
|
||||
"pending_actions_count",
|
||||
"status",
|
||||
"overdue_text_alert",
|
||||
"overdue_email_alert",
|
||||
@@ -173,11 +167,6 @@ class AgentEditSerializer(serializers.ModelSerializer):
|
||||
|
||||
|
||||
class WinAgentSerializer(serializers.ModelSerializer):
|
||||
# for the windows agent
|
||||
patches_pending = serializers.ReadOnlyField(source="has_patches_pending")
|
||||
winupdatepolicy = WinUpdatePolicySerializer(many=True, read_only=True)
|
||||
status = serializers.ReadOnlyField()
|
||||
|
||||
class Meta:
|
||||
model = Agent
|
||||
fields = "__all__"
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import asyncio
|
||||
import datetime as dt
|
||||
import random
|
||||
import tempfile
|
||||
import json
|
||||
import subprocess
|
||||
import urllib.parse
|
||||
from time import sleep
|
||||
from typing import Union
|
||||
@@ -20,7 +23,7 @@ from tacticalrmm.utils import run_nats_api_cmd
|
||||
logger.configure(**settings.LOG_CONFIG)
|
||||
|
||||
|
||||
def agent_update(pk: int, codesigntoken: str = None) -> str:
|
||||
def agent_update(pk: int, codesigntoken: str = None, force: bool = False) -> str:
|
||||
from agents.utils import get_exegen_url
|
||||
|
||||
agent = Agent.objects.get(pk=pk)
|
||||
@@ -45,22 +48,23 @@ def agent_update(pk: int, codesigntoken: str = None) -> str:
|
||||
else:
|
||||
url = agent.winagent_dl
|
||||
|
||||
if agent.pendingactions.filter(
|
||||
action_type="agentupdate", status="pending"
|
||||
).exists():
|
||||
agent.pendingactions.filter(
|
||||
if not force:
|
||||
if agent.pendingactions.filter(
|
||||
action_type="agentupdate", status="pending"
|
||||
).delete()
|
||||
).exists():
|
||||
agent.pendingactions.filter(
|
||||
action_type="agentupdate", status="pending"
|
||||
).delete()
|
||||
|
||||
PendingAction.objects.create(
|
||||
agent=agent,
|
||||
action_type="agentupdate",
|
||||
details={
|
||||
"url": url,
|
||||
"version": version,
|
||||
"inno": inno,
|
||||
},
|
||||
)
|
||||
PendingAction.objects.create(
|
||||
agent=agent,
|
||||
action_type="agentupdate",
|
||||
details={
|
||||
"url": url,
|
||||
"version": version,
|
||||
"inno": inno,
|
||||
},
|
||||
)
|
||||
|
||||
nats_data = {
|
||||
"func": "agentupdate",
|
||||
@@ -74,6 +78,21 @@ def agent_update(pk: int, codesigntoken: str = None) -> str:
|
||||
return "created"
|
||||
|
||||
|
||||
@app.task
|
||||
def force_code_sign(pks: list[int]) -> None:
|
||||
try:
|
||||
token = CodeSignToken.objects.first().token
|
||||
except:
|
||||
return
|
||||
|
||||
chunks = (pks[i : i + 50] for i in range(0, len(pks), 50))
|
||||
for chunk in chunks:
|
||||
for pk in chunk:
|
||||
agent_update(pk=pk, codesigntoken=token, force=True)
|
||||
sleep(0.05)
|
||||
sleep(4)
|
||||
|
||||
|
||||
@app.task
|
||||
def send_agent_update_task(pks: list[int]) -> None:
|
||||
try:
|
||||
@@ -195,6 +214,7 @@ def agent_outages_task() -> None:
|
||||
|
||||
agents = Agent.objects.only(
|
||||
"pk",
|
||||
"agent_id",
|
||||
"last_seen",
|
||||
"offline_time",
|
||||
"overdue_time",
|
||||
@@ -262,6 +282,34 @@ def run_script_email_results_task(
|
||||
logger.error(e)
|
||||
|
||||
|
||||
@app.task
|
||||
def clear_faults_task(older_than_days: int) -> None:
|
||||
# https://github.com/wh1te909/tacticalrmm/issues/484
|
||||
agents = Agent.objects.exclude(last_seen__isnull=True).filter(
|
||||
last_seen__lt=djangotime.now() - djangotime.timedelta(days=older_than_days)
|
||||
)
|
||||
for agent in agents:
|
||||
if agent.agentchecks.exists():
|
||||
for check in agent.agentchecks.all():
|
||||
# reset check status
|
||||
check.status = "passing"
|
||||
check.save(update_fields=["status"])
|
||||
if check.alert.filter(resolved=False).exists():
|
||||
check.alert.get(resolved=False).resolve()
|
||||
|
||||
# reset overdue alerts
|
||||
agent.overdue_email_alert = False
|
||||
agent.overdue_text_alert = False
|
||||
agent.overdue_dashboard_alert = False
|
||||
agent.save(
|
||||
update_fields=[
|
||||
"overdue_email_alert",
|
||||
"overdue_text_alert",
|
||||
"overdue_dashboard_alert",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@app.task
|
||||
def monitor_agents_task() -> None:
|
||||
agents = Agent.objects.only(
|
||||
@@ -277,4 +325,23 @@ def get_wmi_task() -> None:
|
||||
"pk", "agent_id", "last_seen", "overdue_time", "offline_time"
|
||||
)
|
||||
ids = [i.agent_id for i in agents if i.status == "online"]
|
||||
run_nats_api_cmd("wmi", ids)
|
||||
run_nats_api_cmd("wmi", ids, timeout=45)
|
||||
|
||||
|
||||
@app.task
|
||||
def agent_checkin_task() -> None:
|
||||
db = settings.DATABASES["default"]
|
||||
config = {
|
||||
"key": settings.SECRET_KEY,
|
||||
"natsurl": f"tls://{settings.ALLOWED_HOSTS[0]}:4222",
|
||||
"user": db["USER"],
|
||||
"pass": db["PASSWORD"],
|
||||
"host": db["HOST"],
|
||||
"port": int(db["PORT"]),
|
||||
"dbname": db["NAME"],
|
||||
}
|
||||
with tempfile.NamedTemporaryFile() as fp:
|
||||
with open(fp.name, "w") as f:
|
||||
json.dump(config, f)
|
||||
cmd = ["/usr/local/bin/nats-api", "-c", fp.name, "-m", "checkin"]
|
||||
subprocess.run(cmd, timeout=30)
|
||||
|
||||
@@ -152,8 +152,9 @@ class TestAgentViews(TacticalTestCase):
|
||||
|
||||
self.check_not_authenticated("post", url)
|
||||
|
||||
@patch("time.sleep")
|
||||
@patch("agents.models.Agent.nats_cmd")
|
||||
def test_ping(self, nats_cmd):
|
||||
def test_ping(self, nats_cmd, mock_sleep):
|
||||
url = f"/agents/{self.agent.pk}/ping/"
|
||||
|
||||
nats_cmd.return_value = "timeout"
|
||||
@@ -753,7 +754,7 @@ class TestAgentViews(TacticalTestCase):
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertIn(self.agent.hostname, r.data) # type: ignore
|
||||
nats_cmd.assert_called_with(
|
||||
{"func": "recover", "payload": {"mode": "mesh"}}, timeout=45
|
||||
{"func": "recover", "payload": {"mode": "mesh"}}, timeout=90
|
||||
)
|
||||
|
||||
nats_cmd.return_value = "timeout"
|
||||
|
||||
@@ -3,6 +3,7 @@ import datetime as dt
|
||||
import os
|
||||
import random
|
||||
import string
|
||||
import time
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import HttpResponse
|
||||
@@ -10,7 +11,8 @@ from django.shortcuts import get_object_or_404
|
||||
from loguru import logger
|
||||
from packaging import version as pyver
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import api_view
|
||||
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
|
||||
|
||||
@@ -23,6 +25,20 @@ from winupdate.serializers import WinUpdatePolicySerializer
|
||||
from winupdate.tasks import bulk_check_for_updates_task, bulk_install_updates_task
|
||||
|
||||
from .models import Agent, AgentCustomField, Note, RecoveryAction
|
||||
from .permissions import (
|
||||
EditAgentPerms,
|
||||
EvtLogPerms,
|
||||
InstallAgentPerms,
|
||||
ManageNotesPerms,
|
||||
ManageProcPerms,
|
||||
MeshPerms,
|
||||
RebootAgentPerms,
|
||||
RunBulkPerms,
|
||||
RunScriptPerms,
|
||||
SendCMDPerms,
|
||||
UninstallPerms,
|
||||
UpdateAgentPerms,
|
||||
)
|
||||
from .serializers import (
|
||||
AgentCustomFieldSerializer,
|
||||
AgentEditSerializer,
|
||||
@@ -50,6 +66,7 @@ def get_agent_versions(request):
|
||||
|
||||
|
||||
@api_view(["POST"])
|
||||
@permission_classes([IsAuthenticated, UpdateAgentPerms])
|
||||
def update_agents(request):
|
||||
q = Agent.objects.filter(pk__in=request.data["pks"]).only("pk", "version")
|
||||
pks: list[int] = [
|
||||
@@ -62,21 +79,31 @@ def update_agents(request):
|
||||
|
||||
|
||||
@api_view()
|
||||
@permission_classes([IsAuthenticated, UninstallPerms])
|
||||
def ping(request, pk):
|
||||
agent = get_object_or_404(Agent, pk=pk)
|
||||
status = "offline"
|
||||
r = asyncio.run(agent.nats_cmd({"func": "ping"}, timeout=5))
|
||||
if r == "pong":
|
||||
status = "online"
|
||||
attempts = 0
|
||||
while 1:
|
||||
r = asyncio.run(agent.nats_cmd({"func": "ping"}, timeout=2))
|
||||
if r == "pong":
|
||||
status = "online"
|
||||
break
|
||||
else:
|
||||
attempts += 1
|
||||
time.sleep(1)
|
||||
|
||||
if attempts >= 5:
|
||||
break
|
||||
|
||||
return Response({"name": agent.hostname, "status": status})
|
||||
|
||||
|
||||
@api_view(["DELETE"])
|
||||
@permission_classes([IsAuthenticated, UninstallPerms])
|
||||
def uninstall(request):
|
||||
agent = get_object_or_404(Agent, pk=request.data["pk"])
|
||||
asyncio.run(agent.nats_cmd({"func": "uninstall"}, wait=False))
|
||||
|
||||
name = agent.hostname
|
||||
agent.delete()
|
||||
reload_nats()
|
||||
@@ -84,6 +111,7 @@ def uninstall(request):
|
||||
|
||||
|
||||
@api_view(["PATCH", "PUT"])
|
||||
@permission_classes([IsAuthenticated, EditAgentPerms])
|
||||
def edit_agent(request):
|
||||
agent = get_object_or_404(Agent, pk=request.data["id"])
|
||||
|
||||
@@ -126,6 +154,7 @@ def edit_agent(request):
|
||||
|
||||
|
||||
@api_view()
|
||||
@permission_classes([IsAuthenticated, MeshPerms])
|
||||
def meshcentral(request, pk):
|
||||
agent = get_object_or_404(Agent, pk=pk)
|
||||
core = CoreSettings.objects.first()
|
||||
@@ -171,6 +200,7 @@ def get_processes(request, pk):
|
||||
|
||||
|
||||
@api_view()
|
||||
@permission_classes([IsAuthenticated, ManageProcPerms])
|
||||
def kill_proc(request, pk, pid):
|
||||
agent = get_object_or_404(Agent, pk=pk)
|
||||
r = asyncio.run(
|
||||
@@ -186,6 +216,7 @@ def kill_proc(request, pk, pid):
|
||||
|
||||
|
||||
@api_view()
|
||||
@permission_classes([IsAuthenticated, EvtLogPerms])
|
||||
def get_event_log(request, pk, logtype, days):
|
||||
agent = get_object_or_404(Agent, pk=pk)
|
||||
timeout = 180 if logtype == "Security" else 30
|
||||
@@ -205,6 +236,7 @@ def get_event_log(request, pk, logtype, days):
|
||||
|
||||
|
||||
@api_view(["POST"])
|
||||
@permission_classes([IsAuthenticated, SendCMDPerms])
|
||||
def send_raw_cmd(request):
|
||||
agent = get_object_or_404(Agent, pk=request.data["pk"])
|
||||
timeout = int(request.data["timeout"])
|
||||
@@ -270,6 +302,8 @@ class AgentsTableList(APIView):
|
||||
"last_logged_in_user",
|
||||
"time_zone",
|
||||
"maintenance_mode",
|
||||
"pending_actions_count",
|
||||
"has_patches_pending",
|
||||
)
|
||||
ctx = {"default_tz": get_default_timezone()}
|
||||
serializer = AgentTableSerializer(queryset, many=True, context=ctx)
|
||||
@@ -300,6 +334,7 @@ def overdue_action(request):
|
||||
|
||||
|
||||
class Reboot(APIView):
|
||||
permission_classes = [IsAuthenticated, RebootAgentPerms]
|
||||
# reboot now
|
||||
def post(self, request):
|
||||
agent = get_object_or_404(Agent, pk=request.data["pk"])
|
||||
@@ -352,8 +387,10 @@ class Reboot(APIView):
|
||||
|
||||
|
||||
@api_view(["POST"])
|
||||
@permission_classes([IsAuthenticated, InstallAgentPerms])
|
||||
def install_agent(request):
|
||||
from knox.models import AuthToken
|
||||
from accounts.models import User
|
||||
|
||||
from agents.utils import get_winagent_url
|
||||
|
||||
@@ -379,8 +416,10 @@ def install_agent(request):
|
||||
)
|
||||
download_url = get_winagent_url(arch)
|
||||
|
||||
installer_user = User.objects.filter(is_installer_user=True).first()
|
||||
|
||||
_, token = AuthToken.objects.create(
|
||||
user=request.user, expiry=dt.timedelta(hours=request.data["expires"])
|
||||
user=installer_user, expiry=dt.timedelta(hours=request.data["expires"])
|
||||
)
|
||||
|
||||
if request.data["installMethod"] == "exe":
|
||||
@@ -524,6 +563,7 @@ def recover(request):
|
||||
|
||||
|
||||
@api_view(["POST"])
|
||||
@permission_classes([IsAuthenticated, RunScriptPerms])
|
||||
def run_script(request):
|
||||
agent = get_object_or_404(Agent, pk=request.data["pk"])
|
||||
script = get_object_or_404(Script, pk=request.data["scriptPK"])
|
||||
@@ -564,7 +604,7 @@ def run_script(request):
|
||||
def recover_mesh(request, pk):
|
||||
agent = get_object_or_404(Agent, pk=pk)
|
||||
data = {"func": "recover", "payload": {"mode": "mesh"}}
|
||||
r = asyncio.run(agent.nats_cmd(data, timeout=45))
|
||||
r = asyncio.run(agent.nats_cmd(data, timeout=90))
|
||||
if r != "ok":
|
||||
return notify_error("Unable to contact the agent")
|
||||
|
||||
@@ -606,6 +646,8 @@ class GetAddNotes(APIView):
|
||||
|
||||
|
||||
class GetEditDeleteNote(APIView):
|
||||
permission_classes = [IsAuthenticated, ManageNotesPerms]
|
||||
|
||||
def get(self, request, pk):
|
||||
note = get_object_or_404(Note, pk=pk)
|
||||
return Response(NoteSerializer(note).data)
|
||||
@@ -624,6 +666,7 @@ class GetEditDeleteNote(APIView):
|
||||
|
||||
|
||||
@api_view(["POST"])
|
||||
@permission_classes([IsAuthenticated, RunBulkPerms])
|
||||
def bulk(request):
|
||||
if request.data["target"] == "agents" and not request.data["agentPKs"]:
|
||||
return notify_error("Must select at least 1 agent")
|
||||
|
||||
@@ -444,12 +444,12 @@ class Alert(models.Model):
|
||||
name = match.group(1)
|
||||
|
||||
if hasattr(self, name):
|
||||
value = getattr(self, name)
|
||||
value = f"'{getattr(self, name)}'"
|
||||
else:
|
||||
continue
|
||||
|
||||
try:
|
||||
temp_args.append(re.sub("\\{\\{.*\\}\\}", "'" + value + "'", arg)) # type: ignore
|
||||
temp_args.append(re.sub("\\{\\{.*\\}\\}", value, arg)) # type: ignore
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
continue
|
||||
|
||||
11
api/tacticalrmm/alerts/permissions.py
Normal file
11
api/tacticalrmm/alerts/permissions.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from rest_framework import permissions
|
||||
|
||||
from tacticalrmm.permissions import _has_perm
|
||||
|
||||
|
||||
class ManageAlertsPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
if r.method == "GET" or r.method == "PATCH":
|
||||
return True
|
||||
|
||||
return _has_perm(r, "can_manage_alerts")
|
||||
@@ -1387,3 +1387,14 @@ class TestAlertTasks(TacticalTestCase):
|
||||
self.assertEqual(alert.resolved_action_execution_time, "5.0000")
|
||||
self.assertEqual(alert.resolved_action_stdout, "success!")
|
||||
self.assertEqual(alert.resolved_action_stderr, "")
|
||||
|
||||
def test_parse_script_args(self):
|
||||
alert = baker.make("alerts.Alert")
|
||||
|
||||
args = ["-Parameter", "-Another {{alert.id}}"]
|
||||
|
||||
# test default value
|
||||
self.assertEqual(
|
||||
["-Parameter", f"-Another '{alert.id}'"], # type: ignore
|
||||
alert.parse_script_args(args=args), # type: ignore
|
||||
)
|
||||
|
||||
@@ -3,12 +3,14 @@ from datetime import datetime as dt
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils import timezone as djangotime
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from tacticalrmm.utils import notify_error
|
||||
|
||||
from .models import Alert, AlertTemplate
|
||||
from .permissions import ManageAlertsPerms
|
||||
from .serializers import (
|
||||
AlertSerializer,
|
||||
AlertTemplateRelationSerializer,
|
||||
@@ -18,6 +20,8 @@ from .tasks import cache_agents_alert_template
|
||||
|
||||
|
||||
class GetAddAlerts(APIView):
|
||||
permission_classes = [IsAuthenticated, ManageAlertsPerms]
|
||||
|
||||
def patch(self, request):
|
||||
|
||||
# top 10 alerts for dashboard icon
|
||||
@@ -109,6 +113,8 @@ class GetAddAlerts(APIView):
|
||||
|
||||
|
||||
class GetUpdateDeleteAlert(APIView):
|
||||
permission_classes = [IsAuthenticated, ManageAlertsPerms]
|
||||
|
||||
def get(self, request, pk):
|
||||
alert = get_object_or_404(Alert, pk=pk)
|
||||
|
||||
@@ -163,6 +169,8 @@ class GetUpdateDeleteAlert(APIView):
|
||||
|
||||
|
||||
class BulkAlerts(APIView):
|
||||
permission_classes = [IsAuthenticated, ManageAlertsPerms]
|
||||
|
||||
def post(self, request):
|
||||
if request.data["bulk_action"] == "resolve":
|
||||
Alert.objects.filter(id__in=request.data["alerts"]).update(
|
||||
@@ -185,6 +193,8 @@ class BulkAlerts(APIView):
|
||||
|
||||
|
||||
class GetAddAlertTemplates(APIView):
|
||||
permission_classes = [IsAuthenticated, ManageAlertsPerms]
|
||||
|
||||
def get(self, request):
|
||||
alert_templates = AlertTemplate.objects.all()
|
||||
|
||||
@@ -202,6 +212,8 @@ class GetAddAlertTemplates(APIView):
|
||||
|
||||
|
||||
class GetUpdateDeleteAlertTemplate(APIView):
|
||||
permission_classes = [IsAuthenticated, ManageAlertsPerms]
|
||||
|
||||
def get(self, request, pk):
|
||||
alert_template = get_object_or_404(AlertTemplate, pk=pk)
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ from unittest.mock import patch
|
||||
from django.conf import settings
|
||||
from django.utils import timezone as djangotime
|
||||
from model_bakery import baker
|
||||
from autotasks.models import AutomatedTask
|
||||
|
||||
from autotasks.models import AutomatedTask
|
||||
from tacticalrmm.test import TacticalTestCase
|
||||
|
||||
|
||||
@@ -213,7 +213,8 @@ class TestAPIv3(TacticalTestCase):
|
||||
|
||||
# setup data
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
task = baker.make("autotasks.AutomatedTask", agent=agent)
|
||||
script = baker.make_recipe("scripts.script")
|
||||
task = baker.make("autotasks.AutomatedTask", agent=agent, script=script)
|
||||
|
||||
url = f"/api/v3/{task.pk}/{agent.agent_id}/taskrunner/" # type: ignore
|
||||
|
||||
|
||||
@@ -304,10 +304,11 @@ class CheckRunner(APIView):
|
||||
< djangotime.now()
|
||||
- djangotime.timedelta(seconds=check.run_interval)
|
||||
)
|
||||
# if check interval isn't set, make sure the agent's check interval has passed before running
|
||||
)
|
||||
# if check interval isn't set, make sure the agent's check interval has passed before running
|
||||
or (
|
||||
check.last_run
|
||||
not check.run_interval
|
||||
and check.last_run
|
||||
< djangotime.now() - djangotime.timedelta(seconds=agent.check_interval)
|
||||
)
|
||||
]
|
||||
@@ -320,11 +321,16 @@ class CheckRunner(APIView):
|
||||
|
||||
def patch(self, request):
|
||||
check = get_object_or_404(Check, pk=request.data["id"])
|
||||
if pyver.parse(check.agent.version) < pyver.parse("1.5.7"):
|
||||
return notify_error("unsupported")
|
||||
|
||||
check.last_run = djangotime.now()
|
||||
check.save(update_fields=["last_run"])
|
||||
status = check.handle_checkv2(request.data)
|
||||
status = check.handle_check(request.data)
|
||||
if status == "failing" and check.assignedtask.exists(): # type: ignore
|
||||
check.handle_assigned_task()
|
||||
|
||||
return Response(status)
|
||||
return Response("ok")
|
||||
|
||||
|
||||
class CheckRunnerInterval(APIView):
|
||||
@@ -377,9 +383,18 @@ class TaskRunner(APIView):
|
||||
)
|
||||
|
||||
# get last line of stdout
|
||||
value = new_task.stdout.split("\n")[-1].strip()
|
||||
value = (
|
||||
new_task.stdout
|
||||
if task.collector_all_output
|
||||
else new_task.stdout.split("\n")[-1].strip()
|
||||
)
|
||||
|
||||
if task.custom_field.type in ["text", "number", "single", "datetime"]:
|
||||
if task.custom_field.type in [
|
||||
"text",
|
||||
"number",
|
||||
"single",
|
||||
"datetime",
|
||||
]:
|
||||
agent_field.string_value = value
|
||||
agent_field.save()
|
||||
elif task.custom_field.type == "multiple":
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from django.db import models
|
||||
|
||||
from agents.models import Agent
|
||||
from core.models import CoreSettings
|
||||
from django.db import models
|
||||
from logs.models import BaseAuditModel
|
||||
|
||||
|
||||
@@ -28,7 +29,6 @@ class Policy(BaseAuditModel):
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
from alerts.tasks import cache_agents_alert_template
|
||||
|
||||
from automation.tasks import generate_agent_checks_task
|
||||
|
||||
# get old policy if exists
|
||||
@@ -430,11 +430,12 @@ class Policy(BaseAuditModel):
|
||||
|
||||
# remove policy checks from agent that fell out of policy scope
|
||||
agent.agentchecks.filter(
|
||||
managed_by_policy=True,
|
||||
parent_check__in=[
|
||||
checkpk
|
||||
for checkpk in agent_checks_parent_pks
|
||||
if checkpk not in [check.pk for check in final_list]
|
||||
]
|
||||
],
|
||||
).delete()
|
||||
|
||||
return [
|
||||
|
||||
11
api/tacticalrmm/automation/permissions.py
Normal file
11
api/tacticalrmm/automation/permissions.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from rest_framework import permissions
|
||||
|
||||
from tacticalrmm.permissions import _has_perm
|
||||
|
||||
|
||||
class AutomationPolicyPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
if r.method == "GET":
|
||||
return True
|
||||
|
||||
return _has_perm(r, "can_manage_automation_policies")
|
||||
@@ -83,6 +83,7 @@ class PolicyCheckSerializer(ModelSerializer):
|
||||
class AutoTasksFieldSerializer(ModelSerializer):
|
||||
assigned_check = PolicyCheckSerializer(read_only=True)
|
||||
script = ReadOnlyField(source="script.id")
|
||||
custom_field = ReadOnlyField(source="custom_field.id")
|
||||
|
||||
class Meta:
|
||||
model = AutomatedTask
|
||||
|
||||
@@ -3,7 +3,7 @@ from typing import Any, Dict, List, Union
|
||||
from tacticalrmm.celery import app
|
||||
|
||||
|
||||
@app.task
|
||||
@app.task(retry_backoff=5, retry_jitter=True, retry_kwargs={"max_retries": 5})
|
||||
def generate_agent_checks_task(
|
||||
policy: int = None,
|
||||
site: int = None,
|
||||
@@ -13,7 +13,6 @@ def generate_agent_checks_task(
|
||||
create_tasks: bool = False,
|
||||
) -> Union[str, None]:
|
||||
from agents.models import Agent
|
||||
|
||||
from automation.models import Policy
|
||||
|
||||
p = Policy.objects.get(pk=policy) if policy else None
|
||||
@@ -58,7 +57,9 @@ def generate_agent_checks_task(
|
||||
return "ok"
|
||||
|
||||
|
||||
@app.task
|
||||
@app.task(
|
||||
acks_late=True, retry_backoff=5, retry_jitter=True, retry_kwargs={"max_retries": 5}
|
||||
)
|
||||
# updates policy managed check fields on agents
|
||||
def update_policy_check_fields_task(check: int) -> str:
|
||||
from checks.models import Check
|
||||
@@ -74,11 +75,10 @@ def update_policy_check_fields_task(check: int) -> str:
|
||||
return "ok"
|
||||
|
||||
|
||||
@app.task
|
||||
@app.task(retry_backoff=5, retry_jitter=True, retry_kwargs={"max_retries": 5})
|
||||
# generates policy tasks on agents affected by a policy
|
||||
def generate_agent_autotasks_task(policy: int = None) -> str:
|
||||
from agents.models import Agent
|
||||
|
||||
from automation.models import Policy
|
||||
|
||||
p: Policy = Policy.objects.get(pk=policy)
|
||||
@@ -102,7 +102,12 @@ def generate_agent_autotasks_task(policy: int = None) -> str:
|
||||
return "ok"
|
||||
|
||||
|
||||
@app.task
|
||||
@app.task(
|
||||
acks_late=True,
|
||||
retry_backoff=5,
|
||||
retry_jitter=True,
|
||||
retry_kwargs={"max_retries": 5},
|
||||
)
|
||||
def delete_policy_autotasks_task(task: int) -> str:
|
||||
from autotasks.models import AutomatedTask
|
||||
|
||||
@@ -122,7 +127,12 @@ def run_win_policy_autotasks_task(task: int) -> str:
|
||||
return "ok"
|
||||
|
||||
|
||||
@app.task
|
||||
@app.task(
|
||||
acks_late=True,
|
||||
retry_backoff=5,
|
||||
retry_jitter=True,
|
||||
retry_kwargs={"max_retries": 5},
|
||||
)
|
||||
def update_policy_autotasks_fields_task(task: int, update_agent: bool = False) -> str:
|
||||
from autotasks.models import AutomatedTask
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
from itertools import cycle
|
||||
from unittest.mock import patch
|
||||
|
||||
from model_bakery import baker, seq
|
||||
|
||||
from agents.models import Agent
|
||||
from core.models import CoreSettings
|
||||
from model_bakery import baker, seq
|
||||
from tacticalrmm.test import TacticalTestCase
|
||||
from winupdate.models import WinUpdatePolicy
|
||||
|
||||
@@ -54,6 +53,8 @@ class TestPolicyViews(TacticalTestCase):
|
||||
|
||||
@patch("autotasks.models.AutomatedTask.create_task_on_agent")
|
||||
def test_add_policy(self, create_task):
|
||||
from automation.models import Policy
|
||||
|
||||
url = "/automation/policies/"
|
||||
|
||||
data = {
|
||||
@@ -72,8 +73,12 @@ class TestPolicyViews(TacticalTestCase):
|
||||
|
||||
# create policy with tasks and checks
|
||||
policy = baker.make("automation.Policy")
|
||||
self.create_checks(policy=policy)
|
||||
baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3)
|
||||
checks = self.create_checks(policy=policy)
|
||||
tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3)
|
||||
|
||||
# assign a task to a check
|
||||
tasks[0].assigned_check = checks[0] # type: ignore
|
||||
tasks[0].save() # type: ignore
|
||||
|
||||
# test copy tasks and checks to another policy
|
||||
data = {
|
||||
@@ -86,8 +91,16 @@ class TestPolicyViews(TacticalTestCase):
|
||||
|
||||
resp = self.client.post(f"/automation/policies/", data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(policy.autotasks.count(), 3) # type: ignore
|
||||
self.assertEqual(policy.policychecks.count(), 7) # type: ignore
|
||||
|
||||
copied_policy = Policy.objects.get(name=data["name"])
|
||||
|
||||
self.assertEqual(copied_policy.autotasks.count(), 3) # type: ignore
|
||||
self.assertEqual(copied_policy.policychecks.count(), 7) # type: ignore
|
||||
|
||||
# make sure correct task was assign to the check
|
||||
self.assertEqual(copied_policy.autotasks.get(name=tasks[0].name).assigned_check.check_type, checks[0].check_type) # type: ignore
|
||||
|
||||
create_task.assert_not_called()
|
||||
|
||||
self.check_not_authenticated("post", url)
|
||||
|
||||
@@ -110,7 +123,7 @@ class TestPolicyViews(TacticalTestCase):
|
||||
resp = self.client.put(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# only called if active or enforced are updated
|
||||
# only called if active, enforced, or excluded objects are updated
|
||||
generate_agent_checks_task.assert_not_called()
|
||||
|
||||
data = {
|
||||
@@ -120,6 +133,23 @@ class TestPolicyViews(TacticalTestCase):
|
||||
"enforced": False,
|
||||
}
|
||||
|
||||
resp = self.client.put(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
generate_agent_checks_task.assert_called_with(
|
||||
policy=policy.pk, create_tasks=True # type: ignore
|
||||
)
|
||||
generate_agent_checks_task.reset_mock()
|
||||
|
||||
# make sure policies are re-evaluated when excluded changes
|
||||
agents = baker.make_recipe("agents.agent", _quantity=2)
|
||||
clients = baker.make("clients.Client", _quantity=2)
|
||||
sites = baker.make("clients.Site", _quantity=2)
|
||||
data = {
|
||||
"excluded_agents": [agent.pk for agent in agents], # type: ignore
|
||||
"excluded_sites": [site.pk for site in sites], # type: ignore
|
||||
"excluded_clients": [client.pk for client in clients], # type: ignore
|
||||
}
|
||||
|
||||
resp = self.client.put(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
generate_agent_checks_task.assert_called_with(
|
||||
@@ -771,6 +801,7 @@ class TestPolicyTasks(TacticalTestCase):
|
||||
@patch("automation.tasks.generate_agent_checks_task.delay")
|
||||
def test_generating_policy_checks_for_all_agents(self, generate_agent_checks_mock):
|
||||
from core.models import CoreSettings
|
||||
|
||||
from .tasks import generate_agent_checks_task
|
||||
|
||||
# setup data
|
||||
|
||||
@@ -5,6 +5,7 @@ from checks.models import Check
|
||||
from clients.models import Client
|
||||
from clients.serializers import ClientSerializer, SiteSerializer
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from tacticalrmm.utils import notify_error
|
||||
@@ -12,6 +13,7 @@ from winupdate.models import WinUpdatePolicy
|
||||
from winupdate.serializers import WinUpdatePolicySerializer
|
||||
|
||||
from .models import Policy
|
||||
from .permissions import AutomationPolicyPerms
|
||||
from .serializers import (
|
||||
AutoTasksFieldSerializer,
|
||||
PolicyCheckSerializer,
|
||||
@@ -24,6 +26,8 @@ from .serializers import (
|
||||
|
||||
|
||||
class GetAddPolicies(APIView):
|
||||
permission_classes = [IsAuthenticated, AutomationPolicyPerms]
|
||||
|
||||
def get(self, request):
|
||||
policies = Policy.objects.all()
|
||||
|
||||
@@ -51,18 +55,30 @@ class GetAddPolicies(APIView):
|
||||
|
||||
|
||||
class GetUpdateDeletePolicy(APIView):
|
||||
permission_classes = [IsAuthenticated, AutomationPolicyPerms]
|
||||
|
||||
def get(self, request, pk):
|
||||
policy = get_object_or_404(Policy, pk=pk)
|
||||
|
||||
return Response(PolicySerializer(policy).data)
|
||||
|
||||
def put(self, request, pk):
|
||||
from .tasks import generate_agent_checks_task
|
||||
|
||||
policy = get_object_or_404(Policy, pk=pk)
|
||||
|
||||
serializer = PolicySerializer(instance=policy, data=request.data, partial=True)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
|
||||
# check for excluding objects and in the request and if present generate policies
|
||||
if (
|
||||
"excluded_sites" in request.data.keys()
|
||||
or "excluded_clients" in request.data.keys()
|
||||
or "excluded_agents" in request.data.keys()
|
||||
):
|
||||
generate_agent_checks_task.delay(policy=pk, create_tasks=True)
|
||||
|
||||
return Response("ok")
|
||||
|
||||
def delete(self, request, pk):
|
||||
@@ -86,7 +102,7 @@ class PolicySync(APIView):
|
||||
|
||||
|
||||
class PolicyAutoTask(APIView):
|
||||
|
||||
permission_classes = [IsAuthenticated, AutomationPolicyPerms]
|
||||
# tasks associated with policy
|
||||
def get(self, request, pk):
|
||||
tasks = AutomatedTask.objects.filter(policy=pk)
|
||||
@@ -106,6 +122,8 @@ class PolicyAutoTask(APIView):
|
||||
|
||||
|
||||
class PolicyCheck(APIView):
|
||||
permission_classes = [IsAuthenticated, AutomationPolicyPerms]
|
||||
|
||||
def get(self, request, pk):
|
||||
checks = Check.objects.filter(policy__pk=pk, agent=None)
|
||||
return Response(PolicyCheckSerializer(checks, many=True).data)
|
||||
@@ -178,7 +196,7 @@ class GetRelated(APIView):
|
||||
|
||||
|
||||
class UpdatePatchPolicy(APIView):
|
||||
|
||||
permission_classes = [IsAuthenticated, AutomationPolicyPerms]
|
||||
# create new patch policy
|
||||
def post(self, request):
|
||||
policy = get_object_or_404(Policy, pk=request.data["policy"])
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Generated by Django 3.1.7 on 2021-04-04 00:32
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Generated by Django 3.1.7 on 2021-04-27 14:11
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.1 on 2021-05-29 03:26
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('autotasks', '0021_alter_automatedtask_custom_field'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='automatedtask',
|
||||
name='collector_all_output',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -10,6 +10,7 @@ from django.conf import settings
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.db import models
|
||||
from django.db.models.fields import DateTimeField
|
||||
from django.db.utils import DatabaseError
|
||||
from django.utils import timezone as djangotime
|
||||
from logs.models import BaseAuditModel
|
||||
from loguru import logger
|
||||
@@ -104,6 +105,7 @@ class AutomatedTask(BaseAuditModel):
|
||||
task_type = models.CharField(
|
||||
max_length=100, choices=TASK_TYPE_CHOICES, default="manual"
|
||||
)
|
||||
collector_all_output = models.BooleanField(default=False)
|
||||
run_time_date = DateTimeField(null=True, blank=True)
|
||||
remove_if_not_scheduled = models.BooleanField(default=False)
|
||||
run_asap_after_missed = models.BooleanField(default=False) # added in agent v1.4.7
|
||||
@@ -182,6 +184,7 @@ class AutomatedTask(BaseAuditModel):
|
||||
"remove_if_not_scheduled",
|
||||
"run_asap_after_missed",
|
||||
"custom_field",
|
||||
"collector_all_output",
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
@@ -196,33 +199,19 @@ class AutomatedTask(BaseAuditModel):
|
||||
|
||||
return TaskSerializer(task).data
|
||||
|
||||
def create_policy_task(self, agent=None, policy=None):
|
||||
def create_policy_task(self, agent=None, policy=None, assigned_check=None):
|
||||
|
||||
# if policy is present, then this task is being copied to another policy
|
||||
# if agent is present, then this task is being created on an agent from a policy
|
||||
# exit if neither are set or if both are set
|
||||
if not agent and not policy or agent and policy:
|
||||
# also exit if assigned_check is set because this task will be created when the check is
|
||||
if (
|
||||
(not agent and not policy)
|
||||
or (agent and policy)
|
||||
or (self.assigned_check and not assigned_check)
|
||||
):
|
||||
return
|
||||
|
||||
assigned_check = None
|
||||
|
||||
# get correct assigned check to task if set
|
||||
if agent and self.assigned_check:
|
||||
# check if there is a matching check on the agent
|
||||
if agent.agentchecks.filter(parent_check=self.assigned_check.pk).exists():
|
||||
assigned_check = agent.agentchecks.filter(
|
||||
parent_check=self.assigned_check.pk
|
||||
).first()
|
||||
elif policy and self.assigned_check:
|
||||
if policy.policychecks.filter(name=self.assigned_check.name).exists():
|
||||
assigned_check = policy.policychecks.filter(
|
||||
name=self.assigned_check.name
|
||||
).first()
|
||||
else:
|
||||
assigned_check = policy.policychecks.filter(
|
||||
check_type=self.assigned_check.check_type
|
||||
).first()
|
||||
|
||||
task = AutomatedTask.objects.create(
|
||||
agent=agent,
|
||||
policy=policy,
|
||||
@@ -232,11 +221,13 @@ class AutomatedTask(BaseAuditModel):
|
||||
)
|
||||
|
||||
for field in self.policy_fields_to_copy:
|
||||
setattr(task, field, getattr(self, field))
|
||||
if field != "assigned_check":
|
||||
setattr(task, field, getattr(self, field))
|
||||
|
||||
task.save()
|
||||
|
||||
task.create_task_on_agent()
|
||||
if agent:
|
||||
task.create_task_on_agent()
|
||||
|
||||
def create_task_on_agent(self):
|
||||
from agents.models import Agent
|
||||
@@ -375,9 +366,14 @@ class AutomatedTask(BaseAuditModel):
|
||||
|
||||
if r != "ok" and "The system cannot find the file specified" not in r:
|
||||
self.sync_status = "pendingdeletion"
|
||||
self.save(update_fields=["sync_status"])
|
||||
|
||||
try:
|
||||
self.save(update_fields=["sync_status"])
|
||||
except DatabaseError:
|
||||
pass
|
||||
|
||||
logger.warning(
|
||||
f"{agent.hostname} task {self.name} was successfully modified"
|
||||
f"{agent.hostname} task {self.name} will be deleted on next checkin"
|
||||
)
|
||||
return "timeout"
|
||||
else:
|
||||
@@ -417,9 +413,9 @@ class AutomatedTask(BaseAuditModel):
|
||||
from core.models import CoreSettings
|
||||
|
||||
CORE = CoreSettings.objects.first()
|
||||
|
||||
# Format of Email sent when Task has email alert
|
||||
if self.agent:
|
||||
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Failed"
|
||||
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self.agent.hostname} - {self} Failed"
|
||||
else:
|
||||
subject = f"{self} Failed"
|
||||
|
||||
@@ -431,13 +427,12 @@ class AutomatedTask(BaseAuditModel):
|
||||
CORE.send_mail(subject, body, self.agent.alert_template)
|
||||
|
||||
def send_sms(self):
|
||||
|
||||
from core.models import CoreSettings
|
||||
|
||||
CORE = CoreSettings.objects.first()
|
||||
|
||||
# Format of SMS sent when Task has SMS alert
|
||||
if self.agent:
|
||||
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Failed"
|
||||
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self.agent.hostname} - {self} Failed"
|
||||
else:
|
||||
subject = f"{self} Failed"
|
||||
|
||||
|
||||
16
api/tacticalrmm/autotasks/permissions.py
Normal file
16
api/tacticalrmm/autotasks/permissions.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from rest_framework import permissions
|
||||
|
||||
from tacticalrmm.permissions import _has_perm
|
||||
|
||||
|
||||
class ManageAutoTaskPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
if r.method == "GET":
|
||||
return True
|
||||
|
||||
return _has_perm(r, "can_manage_autotasks")
|
||||
|
||||
|
||||
class RunAutoTaskPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
return _has_perm(r, "can_run_autotasks")
|
||||
@@ -68,6 +68,12 @@ class TaskRunnerGetSerializer(serializers.ModelSerializer):
|
||||
|
||||
class TaskGOGetSerializer(serializers.ModelSerializer):
|
||||
script = ScriptCheckSerializer(read_only=True)
|
||||
script_args = serializers.SerializerMethodField()
|
||||
|
||||
def get_script_args(self, obj):
|
||||
return Script.parse_script_args(
|
||||
agent=obj.agent, shell=obj.script.shell, args=obj.script_args
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = AutomatedTask
|
||||
|
||||
@@ -7,9 +7,9 @@ from typing import Union
|
||||
from django.conf import settings
|
||||
from django.utils import timezone as djangotime
|
||||
from loguru import logger
|
||||
from tacticalrmm.celery import app
|
||||
|
||||
from autotasks.models import AutomatedTask
|
||||
from tacticalrmm.celery import app
|
||||
|
||||
logger.configure(**settings.LOG_CONFIG)
|
||||
|
||||
|
||||
@@ -171,6 +171,7 @@ class TestAutotaskViews(TacticalTestCase):
|
||||
url = f"/tasks/{policy_task.id}/automatedtasks/" # type: ignore
|
||||
resp = self.client.delete(url, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertFalse(AutomatedTask.objects.filter(pk=policy_task.id)) # type: ignore
|
||||
delete_policy_autotasks_task.assert_called_with(task=policy_task.id) # type: ignore
|
||||
|
||||
self.check_not_authenticated("delete", url)
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
from agents.models import Agent
|
||||
from checks.models import Check
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework.decorators import api_view
|
||||
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 agents.models import Agent
|
||||
from checks.models import Check
|
||||
from scripts.models import Script
|
||||
from tacticalrmm.utils import get_bit_days, get_default_timezone, notify_error
|
||||
|
||||
from .models import AutomatedTask
|
||||
from .permissions import ManageAutoTaskPerms, RunAutoTaskPerms
|
||||
from .serializers import AutoTaskSerializer, TaskSerializer
|
||||
|
||||
|
||||
class AddAutoTask(APIView):
|
||||
permission_classes = [IsAuthenticated, ManageAutoTaskPerms]
|
||||
|
||||
def post(self, request):
|
||||
from automation.models import Policy
|
||||
from automation.tasks import generate_agent_autotasks_task
|
||||
|
||||
from autotasks.tasks import create_win_task_schedule
|
||||
|
||||
data = request.data
|
||||
@@ -59,6 +63,8 @@ class AddAutoTask(APIView):
|
||||
|
||||
|
||||
class AutoTask(APIView):
|
||||
permission_classes = [IsAuthenticated, ManageAutoTaskPerms]
|
||||
|
||||
def get(self, request, pk):
|
||||
|
||||
agent = get_object_or_404(Agent, pk=pk)
|
||||
@@ -122,6 +128,7 @@ class AutoTask(APIView):
|
||||
|
||||
|
||||
@api_view()
|
||||
@permission_classes([IsAuthenticated, RunAutoTaskPerms])
|
||||
def run_task(request, pk):
|
||||
from autotasks.tasks import run_win_task
|
||||
|
||||
|
||||
22
api/tacticalrmm/checks/migrations/0024_auto_20210606_1632.py
Normal file
22
api/tacticalrmm/checks/migrations/0024_auto_20210606_1632.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# Generated by Django 3.2.1 on 2021-06-06 16:32
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('checks', '0023_check_run_interval'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='checkhistory',
|
||||
name='check_history',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='checkhistory',
|
||||
name='check_id',
|
||||
field=models.PositiveIntegerField(default=0),
|
||||
),
|
||||
]
|
||||
@@ -1,4 +1,3 @@
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import string
|
||||
@@ -15,7 +14,6 @@ from django.db import models
|
||||
from logs.models import BaseAuditModel
|
||||
from loguru import logger
|
||||
|
||||
from .utils import bytes2human
|
||||
|
||||
logger.configure(**settings.LOG_CONFIG)
|
||||
|
||||
@@ -315,9 +313,9 @@ class Check(BaseAuditModel):
|
||||
)
|
||||
|
||||
def add_check_history(self, value: int, more_info: Any = None) -> None:
|
||||
CheckHistory.objects.create(check_history=self, y=value, results=more_info)
|
||||
CheckHistory.objects.create(check_id=self.pk, y=value, results=more_info)
|
||||
|
||||
def handle_checkv2(self, data):
|
||||
def handle_check(self, data):
|
||||
from alerts.models import Alert
|
||||
|
||||
# cpuload or mem checks
|
||||
@@ -348,9 +346,6 @@ class Check(BaseAuditModel):
|
||||
elif self.check_type == "diskspace":
|
||||
if data["exists"]:
|
||||
percent_used = round(data["percent_used"])
|
||||
total = bytes2human(data["total"])
|
||||
free = bytes2human(data["free"])
|
||||
|
||||
if self.error_threshold and (100 - percent_used) < self.error_threshold:
|
||||
self.status = "failing"
|
||||
self.alert_severity = "error"
|
||||
@@ -364,7 +359,7 @@ class Check(BaseAuditModel):
|
||||
else:
|
||||
self.status = "passing"
|
||||
|
||||
self.more_info = f"Total: {total}B, Free: {free}B"
|
||||
self.more_info = data["more_info"]
|
||||
|
||||
# add check history
|
||||
self.add_check_history(100 - percent_used)
|
||||
@@ -380,12 +375,7 @@ class Check(BaseAuditModel):
|
||||
self.stdout = data["stdout"]
|
||||
self.stderr = data["stderr"]
|
||||
self.retcode = data["retcode"]
|
||||
try:
|
||||
# python agent
|
||||
self.execution_time = "{:.4f}".format(data["stop"] - data["start"])
|
||||
except:
|
||||
# golang agent
|
||||
self.execution_time = "{:.4f}".format(data["runtime"])
|
||||
self.execution_time = "{:.4f}".format(data["runtime"])
|
||||
|
||||
if data["retcode"] in self.info_return_codes:
|
||||
self.alert_severity = "info"
|
||||
@@ -421,18 +411,8 @@ class Check(BaseAuditModel):
|
||||
|
||||
# ping checks
|
||||
elif self.check_type == "ping":
|
||||
success = ["Reply", "bytes", "time", "TTL"]
|
||||
output = data["output"]
|
||||
|
||||
if data["has_stdout"]:
|
||||
if all(x in output for x in success):
|
||||
self.status = "passing"
|
||||
else:
|
||||
self.status = "failing"
|
||||
elif data["has_stderr"]:
|
||||
self.status = "failing"
|
||||
|
||||
self.more_info = output
|
||||
self.status = data["status"]
|
||||
self.more_info = data["output"]
|
||||
self.save(update_fields=["more_info"])
|
||||
|
||||
self.add_check_history(
|
||||
@@ -441,41 +421,8 @@ class Check(BaseAuditModel):
|
||||
|
||||
# windows service checks
|
||||
elif self.check_type == "winsvc":
|
||||
svc_stat = data["status"]
|
||||
self.more_info = f"Status {svc_stat.upper()}"
|
||||
|
||||
if data["exists"]:
|
||||
if svc_stat == "running":
|
||||
self.status = "passing"
|
||||
elif svc_stat == "start_pending" and self.pass_if_start_pending:
|
||||
self.status = "passing"
|
||||
else:
|
||||
if self.agent and self.restart_if_stopped:
|
||||
nats_data = {
|
||||
"func": "winsvcaction",
|
||||
"payload": {"name": self.svc_name, "action": "start"},
|
||||
}
|
||||
r = asyncio.run(self.agent.nats_cmd(nats_data, timeout=32))
|
||||
if r == "timeout" or r == "natsdown":
|
||||
self.status = "failing"
|
||||
elif not r["success"] and r["errormsg"]:
|
||||
self.status = "failing"
|
||||
elif r["success"]:
|
||||
self.status = "passing"
|
||||
self.more_info = f"Status RUNNING"
|
||||
else:
|
||||
self.status = "failing"
|
||||
else:
|
||||
self.status = "failing"
|
||||
|
||||
else:
|
||||
if self.pass_if_svc_not_exist:
|
||||
self.status = "passing"
|
||||
else:
|
||||
self.status = "failing"
|
||||
|
||||
self.more_info = f"Service {self.svc_name} does not exist"
|
||||
|
||||
self.status = data["status"]
|
||||
self.more_info = data["more_info"]
|
||||
self.save(update_fields=["more_info"])
|
||||
|
||||
self.add_check_history(
|
||||
@@ -483,49 +430,7 @@ class Check(BaseAuditModel):
|
||||
)
|
||||
|
||||
elif self.check_type == "eventlog":
|
||||
log = []
|
||||
is_wildcard = self.event_id_is_wildcard
|
||||
eventType = self.event_type
|
||||
eventID = self.event_id
|
||||
source = self.event_source
|
||||
message = self.event_message
|
||||
r = data["log"]
|
||||
|
||||
for i in r:
|
||||
if i["eventType"] == eventType:
|
||||
if not is_wildcard and not int(i["eventID"]) == eventID:
|
||||
continue
|
||||
|
||||
if not source and not message:
|
||||
if is_wildcard:
|
||||
log.append(i)
|
||||
elif int(i["eventID"]) == eventID:
|
||||
log.append(i)
|
||||
continue
|
||||
|
||||
if source and message:
|
||||
if is_wildcard:
|
||||
if source in i["source"] and message in i["message"]:
|
||||
log.append(i)
|
||||
|
||||
elif int(i["eventID"]) == eventID:
|
||||
if source in i["source"] and message in i["message"]:
|
||||
log.append(i)
|
||||
|
||||
continue
|
||||
|
||||
if source and source in i["source"]:
|
||||
if is_wildcard:
|
||||
log.append(i)
|
||||
elif int(i["eventID"]) == eventID:
|
||||
log.append(i)
|
||||
|
||||
if message and message in i["message"]:
|
||||
if is_wildcard:
|
||||
log.append(i)
|
||||
elif int(i["eventID"]) == eventID:
|
||||
log.append(i)
|
||||
|
||||
log = data["log"]
|
||||
if self.fail_when == "contains":
|
||||
if log and len(log) >= self.number_of_events_b4_alert:
|
||||
self.status = "failing"
|
||||
@@ -562,6 +467,11 @@ class Check(BaseAuditModel):
|
||||
|
||||
return self.status
|
||||
|
||||
def handle_assigned_task(self) -> None:
|
||||
for task in self.assignedtask.all(): # type: ignore
|
||||
if task.enabled:
|
||||
task.run_win_task()
|
||||
|
||||
@staticmethod
|
||||
def serialize(check):
|
||||
# serializes the check and returns json
|
||||
@@ -598,6 +508,14 @@ class Check(BaseAuditModel):
|
||||
script=self.script,
|
||||
)
|
||||
|
||||
for task in self.assignedtask.all(): # type: ignore
|
||||
if policy or (
|
||||
agent and not agent.autotasks.filter(parent_task=task.pk).exists()
|
||||
):
|
||||
task.create_policy_task(
|
||||
agent=agent, policy=policy, assigned_check=check
|
||||
)
|
||||
|
||||
for field in self.policy_fields_to_copy:
|
||||
setattr(check, field, getattr(self, field))
|
||||
|
||||
@@ -770,14 +688,10 @@ class Check(BaseAuditModel):
|
||||
|
||||
|
||||
class CheckHistory(models.Model):
|
||||
check_history = models.ForeignKey(
|
||||
Check,
|
||||
related_name="check_history",
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
check_id = models.PositiveIntegerField(default=0)
|
||||
x = models.DateTimeField(auto_now_add=True)
|
||||
y = models.PositiveIntegerField(null=True, blank=True, default=None)
|
||||
results = models.JSONField(null=True, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.check_history.readable_desc
|
||||
return self.x
|
||||
|
||||
16
api/tacticalrmm/checks/permissions.py
Normal file
16
api/tacticalrmm/checks/permissions.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from rest_framework import permissions
|
||||
|
||||
from tacticalrmm.permissions import _has_perm
|
||||
|
||||
|
||||
class ManageChecksPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
if r.method == "GET":
|
||||
return True
|
||||
|
||||
return _has_perm(r, "can_manage_checks")
|
||||
|
||||
|
||||
class RunChecksPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
return _has_perm(r, "can_run_checks")
|
||||
@@ -6,6 +6,7 @@ from autotasks.models import AutomatedTask
|
||||
from scripts.serializers import ScriptCheckSerializer, ScriptSerializer
|
||||
|
||||
from .models import Check, CheckHistory
|
||||
from scripts.models import Script
|
||||
|
||||
|
||||
class AssignedTaskField(serializers.ModelSerializer):
|
||||
@@ -158,13 +159,16 @@ class AssignedTaskCheckRunnerField(serializers.ModelSerializer):
|
||||
|
||||
class CheckRunnerGetSerializer(serializers.ModelSerializer):
|
||||
# only send data needed for agent to run a check
|
||||
assigned_tasks = serializers.SerializerMethodField()
|
||||
script = ScriptCheckSerializer(read_only=True)
|
||||
script_args = serializers.SerializerMethodField()
|
||||
|
||||
def get_assigned_tasks(self, obj):
|
||||
if obj.assignedtask.exists():
|
||||
tasks = obj.assignedtask.all()
|
||||
return AssignedTaskCheckRunnerField(tasks, many=True).data
|
||||
def get_script_args(self, obj):
|
||||
if obj.check_type != "script":
|
||||
return []
|
||||
|
||||
return Script.parse_script_args(
|
||||
agent=obj.agent, shell=obj.script.shell, args=obj.script_args
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Check
|
||||
@@ -193,6 +197,7 @@ class CheckRunnerGetSerializer(serializers.ModelSerializer):
|
||||
"modified_by",
|
||||
"modified_time",
|
||||
"history",
|
||||
"dashboard_alert",
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -363,10 +363,10 @@ class TestCheckViews(TacticalTestCase):
|
||||
# setup data
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
check = baker.make_recipe("checks.diskspace_check", agent=agent)
|
||||
baker.make("checks.CheckHistory", check_history=check, _quantity=30)
|
||||
baker.make("checks.CheckHistory", check_id=check.id, _quantity=30)
|
||||
check_history_data = baker.make(
|
||||
"checks.CheckHistory",
|
||||
check_history=check,
|
||||
check_id=check.id,
|
||||
_quantity=30,
|
||||
)
|
||||
|
||||
@@ -400,17 +400,17 @@ class TestCheckTasks(TacticalTestCase):
|
||||
def setUp(self):
|
||||
self.authenticate()
|
||||
self.setup_coresettings()
|
||||
self.agent = baker.make_recipe("agents.agent")
|
||||
self.agent = baker.make_recipe("agents.agent", version="1.5.7")
|
||||
|
||||
def test_prune_check_history(self):
|
||||
from .tasks import prune_check_history
|
||||
|
||||
# setup data
|
||||
check = baker.make_recipe("checks.diskspace_check")
|
||||
baker.make("checks.CheckHistory", check_history=check, _quantity=30)
|
||||
baker.make("checks.CheckHistory", check_id=check.id, _quantity=30)
|
||||
check_history_data = baker.make(
|
||||
"checks.CheckHistory",
|
||||
check_history=check,
|
||||
check_id=check.id,
|
||||
_quantity=30,
|
||||
)
|
||||
|
||||
@@ -526,6 +526,7 @@ class TestCheckTasks(TacticalTestCase):
|
||||
"percent_used": 85,
|
||||
"total": 500,
|
||||
"free": 400,
|
||||
"more_info": "More info",
|
||||
}
|
||||
|
||||
resp = self.client.patch(url, data, format="json")
|
||||
@@ -543,6 +544,7 @@ class TestCheckTasks(TacticalTestCase):
|
||||
"percent_used": 95,
|
||||
"total": 500,
|
||||
"free": 400,
|
||||
"more_info": "More info",
|
||||
}
|
||||
|
||||
resp = self.client.patch(url, data, format="json")
|
||||
@@ -573,6 +575,7 @@ class TestCheckTasks(TacticalTestCase):
|
||||
"percent_used": 95,
|
||||
"total": 500,
|
||||
"free": 400,
|
||||
"more_info": "More info",
|
||||
}
|
||||
|
||||
resp = self.client.patch(url, data, format="json")
|
||||
@@ -592,6 +595,7 @@ class TestCheckTasks(TacticalTestCase):
|
||||
"percent_used": 95,
|
||||
"total": 500,
|
||||
"free": 400,
|
||||
"more_info": "More info",
|
||||
}
|
||||
|
||||
resp = self.client.patch(url, data, format="json")
|
||||
@@ -608,6 +612,7 @@ class TestCheckTasks(TacticalTestCase):
|
||||
"percent_used": 50,
|
||||
"total": 500,
|
||||
"free": 400,
|
||||
"more_info": "More info",
|
||||
}
|
||||
|
||||
resp = self.client.patch(url, data, format="json")
|
||||
@@ -791,12 +796,7 @@ class TestCheckTasks(TacticalTestCase):
|
||||
)
|
||||
|
||||
# test failing info
|
||||
data = {
|
||||
"id": ping.id,
|
||||
"output": "Reply from 192.168.1.27: Destination host unreachable",
|
||||
"has_stdout": True,
|
||||
"has_stderr": False,
|
||||
}
|
||||
data = {"id": ping.id, "status": "failing", "output": "reply from a.com"}
|
||||
|
||||
resp = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
@@ -806,13 +806,6 @@ class TestCheckTasks(TacticalTestCase):
|
||||
self.assertEqual(new_check.alert_severity, "info")
|
||||
|
||||
# test failing warning
|
||||
data = {
|
||||
"id": ping.id,
|
||||
"output": "Reply from 192.168.1.27: Destination host unreachable",
|
||||
"has_stdout": True,
|
||||
"has_stderr": False,
|
||||
}
|
||||
|
||||
ping.alert_severity = "warning"
|
||||
ping.save()
|
||||
|
||||
@@ -824,13 +817,6 @@ class TestCheckTasks(TacticalTestCase):
|
||||
self.assertEqual(new_check.alert_severity, "warning")
|
||||
|
||||
# test failing error
|
||||
data = {
|
||||
"id": ping.id,
|
||||
"output": "Reply from 192.168.1.27: Destination host unreachable",
|
||||
"has_stdout": True,
|
||||
"has_stderr": False,
|
||||
}
|
||||
|
||||
ping.alert_severity = "error"
|
||||
ping.save()
|
||||
|
||||
@@ -842,13 +828,6 @@ class TestCheckTasks(TacticalTestCase):
|
||||
self.assertEqual(new_check.alert_severity, "error")
|
||||
|
||||
# test failing error
|
||||
data = {
|
||||
"id": ping.id,
|
||||
"output": "some output",
|
||||
"has_stdout": False,
|
||||
"has_stderr": True,
|
||||
}
|
||||
|
||||
resp = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
@@ -857,12 +836,7 @@ class TestCheckTasks(TacticalTestCase):
|
||||
self.assertEqual(new_check.alert_severity, "error")
|
||||
|
||||
# test passing
|
||||
data = {
|
||||
"id": ping.id,
|
||||
"output": "Reply from 192.168.1.1: bytes=32 time<1ms TTL=64",
|
||||
"has_stdout": True,
|
||||
"has_stderr": False,
|
||||
}
|
||||
data = {"id": ping.id, "status": "passing", "output": "reply from a.com"}
|
||||
|
||||
resp = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
@@ -881,7 +855,7 @@ class TestCheckTasks(TacticalTestCase):
|
||||
)
|
||||
|
||||
# test passing running
|
||||
data = {"id": winsvc.id, "exists": True, "status": "running"}
|
||||
data = {"id": winsvc.id, "status": "passing", "more_info": "ok"}
|
||||
|
||||
resp = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
@@ -889,20 +863,8 @@ class TestCheckTasks(TacticalTestCase):
|
||||
new_check = Check.objects.get(pk=winsvc.id)
|
||||
self.assertEqual(new_check.status, "passing")
|
||||
|
||||
# test passing start pending
|
||||
winsvc.pass_if_start_pending = True
|
||||
winsvc.save()
|
||||
|
||||
data = {"id": winsvc.id, "exists": True, "status": "start_pending"}
|
||||
|
||||
resp = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
new_check = Check.objects.get(pk=winsvc.id)
|
||||
self.assertEqual(new_check.status, "passing")
|
||||
|
||||
# test failing no start
|
||||
data = {"id": winsvc.id, "exists": True, "status": "not running"}
|
||||
# test failing
|
||||
data = {"id": winsvc.id, "status": "failing", "more_info": "ok"}
|
||||
|
||||
resp = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
@@ -911,7 +873,7 @@ class TestCheckTasks(TacticalTestCase):
|
||||
self.assertEqual(new_check.status, "failing")
|
||||
self.assertEqual(new_check.alert_severity, "info")
|
||||
|
||||
# test failing and attempt start
|
||||
""" # test failing and attempt start
|
||||
winsvc.restart_if_stopped = True
|
||||
winsvc.alert_severity = "warning"
|
||||
winsvc.save()
|
||||
@@ -976,9 +938,9 @@ class TestCheckTasks(TacticalTestCase):
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
new_check = Check.objects.get(pk=winsvc.id)
|
||||
self.assertEqual(new_check.status, "passing")
|
||||
self.assertEqual(new_check.status, "passing") """
|
||||
|
||||
def test_handle_eventlog_check(self):
|
||||
""" def test_handle_eventlog_check(self):
|
||||
from checks.models import Check
|
||||
|
||||
url = "/api/v3/checkrunner/"
|
||||
@@ -1180,4 +1142,4 @@ class TestCheckTasks(TacticalTestCase):
|
||||
|
||||
new_check = Check.objects.get(pk=eventlog.id)
|
||||
|
||||
self.assertEquals(new_check.status, "passing")
|
||||
self.assertEquals(new_check.status, "passing") """
|
||||
|
||||
@@ -8,5 +8,5 @@ urlpatterns = [
|
||||
path("<pk>/loadchecks/", views.load_checks),
|
||||
path("getalldisks/", views.get_disks_for_policies),
|
||||
path("runchecks/<pk>/", views.run_checks),
|
||||
path("history/<int:checkpk>/", views.CheckHistory.as_view()),
|
||||
path("history/<int:checkpk>/", views.GetCheckHistory.as_view()),
|
||||
]
|
||||
|
||||
@@ -1,23 +1,28 @@
|
||||
import asyncio
|
||||
from datetime import datetime as dt
|
||||
|
||||
from agents.models import Agent
|
||||
from automation.models import Policy
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils import timezone as djangotime
|
||||
from packaging import version as pyver
|
||||
from rest_framework.decorators import api_view
|
||||
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 agents.models import Agent
|
||||
from automation.models import Policy
|
||||
from scripts.models import Script
|
||||
from tacticalrmm.utils import notify_error
|
||||
|
||||
from .models import Check
|
||||
from .models import Check, CheckHistory
|
||||
from .permissions import ManageChecksPerms, RunChecksPerms
|
||||
from .serializers import CheckHistorySerializer, CheckSerializer
|
||||
|
||||
|
||||
class AddCheck(APIView):
|
||||
permission_classes = [IsAuthenticated, ManageChecksPerms]
|
||||
|
||||
def post(self, request):
|
||||
from automation.tasks import generate_agent_checks_task
|
||||
|
||||
@@ -76,14 +81,14 @@ class AddCheck(APIView):
|
||||
|
||||
|
||||
class GetUpdateDeleteCheck(APIView):
|
||||
permission_classes = [IsAuthenticated, ManageChecksPerms]
|
||||
|
||||
def get(self, request, pk):
|
||||
check = get_object_or_404(Check, pk=pk)
|
||||
return Response(CheckSerializer(check).data)
|
||||
|
||||
def patch(self, request, pk):
|
||||
from automation.tasks import (
|
||||
update_policy_check_fields_task,
|
||||
)
|
||||
from automation.tasks import update_policy_check_fields_task
|
||||
|
||||
check = get_object_or_404(Check, pk=pk)
|
||||
|
||||
@@ -123,11 +128,12 @@ class GetUpdateDeleteCheck(APIView):
|
||||
from automation.tasks import generate_agent_checks_task
|
||||
|
||||
check = get_object_or_404(Check, pk=pk)
|
||||
|
||||
check.delete()
|
||||
|
||||
# Policy check deleted
|
||||
if check.policy:
|
||||
Check.objects.filter(parent_check=check.pk).delete()
|
||||
Check.objects.filter(managed_by_policy=True, parent_check=pk).delete()
|
||||
|
||||
# Re-evaluate agent checks is policy was enforced
|
||||
if check.policy.enforced:
|
||||
@@ -140,7 +146,7 @@ class GetUpdateDeleteCheck(APIView):
|
||||
return Response(f"{check.readable_desc} was deleted!")
|
||||
|
||||
|
||||
class CheckHistory(APIView):
|
||||
class GetCheckHistory(APIView):
|
||||
def patch(self, request, checkpk):
|
||||
check = get_object_or_404(Check, pk=checkpk)
|
||||
|
||||
@@ -154,7 +160,7 @@ class CheckHistory(APIView):
|
||||
- djangotime.timedelta(days=request.data["timeFilter"]),
|
||||
)
|
||||
|
||||
check_history = check.check_history.filter(timeFilter).order_by("-x") # type: ignore
|
||||
check_history = CheckHistory.objects.filter(check_id=checkpk).filter(timeFilter).order_by("-x") # type: ignore
|
||||
|
||||
return Response(
|
||||
CheckHistorySerializer(
|
||||
@@ -164,6 +170,7 @@ class CheckHistory(APIView):
|
||||
|
||||
|
||||
@api_view()
|
||||
@permission_classes([IsAuthenticated, RunChecksPerms])
|
||||
def run_checks(request, pk):
|
||||
agent = get_object_or_404(Agent, pk=pk)
|
||||
|
||||
|
||||
@@ -87,12 +87,20 @@ class Client(BaseAuditModel):
|
||||
"offline_time",
|
||||
)
|
||||
.filter(site__client=self)
|
||||
.prefetch_related("agentchecks")
|
||||
.prefetch_related("agentchecks", "autotasks")
|
||||
)
|
||||
|
||||
data = {"error": False, "warning": False}
|
||||
|
||||
for agent in agents:
|
||||
if agent.maintenance_mode:
|
||||
break
|
||||
|
||||
if agent.overdue_email_alert or agent.overdue_text_alert:
|
||||
if agent.status == "overdue":
|
||||
data["error"] = True
|
||||
break
|
||||
|
||||
if agent.checks["has_failing_checks"]:
|
||||
|
||||
if agent.checks["warning"]:
|
||||
@@ -102,10 +110,11 @@ class Client(BaseAuditModel):
|
||||
data["error"] = True
|
||||
break
|
||||
|
||||
if agent.overdue_email_alert or agent.overdue_text_alert:
|
||||
if agent.status == "overdue":
|
||||
data["error"] = True
|
||||
break
|
||||
if agent.autotasks.exists(): # type: ignore
|
||||
for i in agent.autotasks.all(): # type: ignore
|
||||
if i.status == "failing" and i.alert_severity == "error":
|
||||
data["error"] = True
|
||||
break
|
||||
|
||||
return data
|
||||
|
||||
@@ -192,12 +201,19 @@ class Site(BaseAuditModel):
|
||||
"offline_time",
|
||||
)
|
||||
.filter(site=self)
|
||||
.prefetch_related("agentchecks")
|
||||
.prefetch_related("agentchecks", "autotasks")
|
||||
)
|
||||
|
||||
data = {"error": False, "warning": False}
|
||||
|
||||
for agent in agents:
|
||||
if agent.maintenance_mode:
|
||||
break
|
||||
|
||||
if agent.overdue_email_alert or agent.overdue_text_alert:
|
||||
if agent.status == "overdue":
|
||||
data["error"] = True
|
||||
break
|
||||
|
||||
if agent.checks["has_failing_checks"]:
|
||||
if agent.checks["warning"]:
|
||||
@@ -207,10 +223,11 @@ class Site(BaseAuditModel):
|
||||
data["error"] = True
|
||||
break
|
||||
|
||||
if agent.overdue_email_alert or agent.overdue_text_alert:
|
||||
if agent.status == "overdue":
|
||||
data["error"] = True
|
||||
break
|
||||
if agent.autotasks.exists(): # type: ignore
|
||||
for i in agent.autotasks.all(): # type: ignore
|
||||
if i.status == "failing" and i.alert_severity == "error":
|
||||
data["error"] = True
|
||||
break
|
||||
|
||||
return data
|
||||
|
||||
|
||||
27
api/tacticalrmm/clients/permissions.py
Normal file
27
api/tacticalrmm/clients/permissions.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from rest_framework import permissions
|
||||
|
||||
from tacticalrmm.permissions import _has_perm
|
||||
|
||||
|
||||
class ManageClientsPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
if r.method == "GET":
|
||||
return True
|
||||
|
||||
return _has_perm(r, "can_manage_clients")
|
||||
|
||||
|
||||
class ManageSitesPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
if r.method == "GET":
|
||||
return True
|
||||
|
||||
return _has_perm(r, "can_manage_sites")
|
||||
|
||||
|
||||
class ManageDeploymentPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
if r.method == "GET":
|
||||
return True
|
||||
|
||||
return _has_perm(r, "can_manage_deployments")
|
||||
@@ -7,7 +7,7 @@ from django.conf import settings
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils import timezone as djangotime
|
||||
from loguru import logger
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
@@ -16,6 +16,7 @@ from core.models import CoreSettings
|
||||
from tacticalrmm.utils import notify_error
|
||||
|
||||
from .models import Client, ClientCustomField, Deployment, Site, SiteCustomField
|
||||
from .permissions import ManageClientsPerms, ManageDeploymentPerms, ManageSitesPerms
|
||||
from .serializers import (
|
||||
ClientCustomFieldSerializer,
|
||||
ClientSerializer,
|
||||
@@ -29,6 +30,8 @@ logger.configure(**settings.LOG_CONFIG)
|
||||
|
||||
|
||||
class GetAddClients(APIView):
|
||||
permission_classes = [IsAuthenticated, ManageClientsPerms]
|
||||
|
||||
def get(self, request):
|
||||
clients = Client.objects.all()
|
||||
return Response(ClientSerializer(clients, many=True).data)
|
||||
@@ -72,6 +75,8 @@ class GetAddClients(APIView):
|
||||
|
||||
|
||||
class GetUpdateClient(APIView):
|
||||
permission_classes = [IsAuthenticated, ManageClientsPerms]
|
||||
|
||||
def get(self, request, pk):
|
||||
client = get_object_or_404(Client, pk=pk)
|
||||
return Response(ClientSerializer(client).data)
|
||||
@@ -110,6 +115,8 @@ class GetUpdateClient(APIView):
|
||||
|
||||
|
||||
class DeleteClient(APIView):
|
||||
permission_classes = [IsAuthenticated, ManageClientsPerms]
|
||||
|
||||
def delete(self, request, pk, sitepk):
|
||||
from automation.tasks import generate_agent_checks_task
|
||||
|
||||
@@ -137,6 +144,8 @@ class GetClientTree(APIView):
|
||||
|
||||
|
||||
class GetAddSites(APIView):
|
||||
permission_classes = [IsAuthenticated, ManageSitesPerms]
|
||||
|
||||
def get(self, request):
|
||||
sites = Site.objects.all()
|
||||
return Response(SiteSerializer(sites, many=True).data)
|
||||
@@ -162,6 +171,8 @@ class GetAddSites(APIView):
|
||||
|
||||
|
||||
class GetUpdateSite(APIView):
|
||||
permission_classes = [IsAuthenticated, ManageSitesPerms]
|
||||
|
||||
def get(self, request, pk):
|
||||
site = get_object_or_404(Site, pk=pk)
|
||||
return Response(SiteSerializer(site).data)
|
||||
@@ -205,6 +216,8 @@ class GetUpdateSite(APIView):
|
||||
|
||||
|
||||
class DeleteSite(APIView):
|
||||
permission_classes = [IsAuthenticated, ManageSitesPerms]
|
||||
|
||||
def delete(self, request, pk, sitepk):
|
||||
from automation.tasks import generate_agent_checks_task
|
||||
|
||||
@@ -230,22 +243,27 @@ class DeleteSite(APIView):
|
||||
|
||||
|
||||
class AgentDeployment(APIView):
|
||||
permission_classes = [IsAuthenticated, ManageDeploymentPerms]
|
||||
|
||||
def get(self, request):
|
||||
deps = Deployment.objects.all()
|
||||
return Response(DeploymentSerializer(deps, many=True).data)
|
||||
|
||||
def post(self, request):
|
||||
from knox.models import AuthToken
|
||||
from accounts.models import User
|
||||
|
||||
client = get_object_or_404(Client, pk=request.data["client"])
|
||||
site = get_object_or_404(Site, pk=request.data["site"])
|
||||
|
||||
installer_user = User.objects.filter(is_installer_user=True).first()
|
||||
|
||||
expires = dt.datetime.strptime(
|
||||
request.data["expires"], "%Y-%m-%d %H:%M"
|
||||
).astimezone(pytz.timezone("UTC"))
|
||||
now = djangotime.now()
|
||||
delta = expires - now
|
||||
obj, token = AuthToken.objects.create(user=request.user, expiry=delta)
|
||||
obj, token = AuthToken.objects.create(user=installer_user, expiry=delta)
|
||||
|
||||
flags = {
|
||||
"power": request.data["power"],
|
||||
|
||||
@@ -53,9 +53,9 @@ If (Get-Service $serviceName -ErrorAction SilentlyContinue) {
|
||||
Write-Output "Waiting for network"
|
||||
Start-Sleep -s 5
|
||||
$X += 1
|
||||
} until(($connectreult = Test-NetConnection $apilink[2] -Port 443 | ? { $_.TcpTestSucceeded }) -or $X -eq 3)
|
||||
} until(($connectresult = Test-NetConnection $apilink[2] -Port 443 | ? { $_.TcpTestSucceeded }) -or $X -eq 3)
|
||||
|
||||
if ($connectreult.TcpTestSucceeded -eq $true){
|
||||
if ($connectresult.TcpTestSucceeded -eq $true){
|
||||
Try
|
||||
{
|
||||
Invoke-WebRequest -Uri $downloadlink -OutFile $OutPath\$output
|
||||
|
||||
@@ -1,30 +1,13 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from agents.models import Agent
|
||||
from scripts.models import Script
|
||||
from logs.models import PendingAction
|
||||
from scripts.models import Script
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Collection of tasks to run after updating the rmm, after migrations"
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
# 10-16-2020 changed the type of the agent's 'disks' model field
|
||||
# from a dict of dicts, to a list of disks in the golang agent
|
||||
# the following will convert dicts to lists for agent's still on the python agent
|
||||
agents = Agent.objects.only("pk", "disks")
|
||||
for agent in agents:
|
||||
if agent.disks is not None and isinstance(agent.disks, dict):
|
||||
new = []
|
||||
for k, v in agent.disks.items():
|
||||
new.append(v)
|
||||
|
||||
agent.disks = new
|
||||
agent.save(update_fields=["disks"])
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(f"Migrated disks on {agent.hostname}")
|
||||
)
|
||||
|
||||
# remove task pending actions. deprecated 4/20/2021
|
||||
PendingAction.objects.filter(action_type="taskaction").delete()
|
||||
|
||||
|
||||
22
api/tacticalrmm/core/migrations/0022_urlaction.py
Normal file
22
api/tacticalrmm/core/migrations/0022_urlaction.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# Generated by Django 3.1.7 on 2021-05-02 02:13
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0021_customfield_hide_in_ui'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='URLAction',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=25)),
|
||||
('desc', models.CharField(blank=True, max_length=100, null=True)),
|
||||
('pattern', models.TextField()),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.2 on 2021-05-14 04:30
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0022_urlaction'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='coresettings',
|
||||
name='clear_faults_days',
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
]
|
||||
@@ -51,6 +51,7 @@ class CoreSettings(BaseAuditModel):
|
||||
)
|
||||
# removes check history older than days
|
||||
check_history_prune_days = models.PositiveIntegerField(default=30)
|
||||
clear_faults_days = models.IntegerField(default=0)
|
||||
mesh_token = models.CharField(max_length=255, null=True, blank=True, default="")
|
||||
mesh_username = models.CharField(max_length=255, null=True, blank=True, default="")
|
||||
mesh_site = models.CharField(max_length=255, null=True, blank=True, default="")
|
||||
@@ -286,6 +287,12 @@ class GlobalKVStore(models.Model):
|
||||
return self.name
|
||||
|
||||
|
||||
class URLAction(models.Model):
|
||||
name = models.CharField(max_length=25)
|
||||
desc = models.CharField(max_length=100, null=True, blank=True)
|
||||
pattern = models.TextField()
|
||||
|
||||
|
||||
RUN_ON_CHOICES = (
|
||||
("client", "Client"),
|
||||
("site", "Site"),
|
||||
|
||||
23
api/tacticalrmm/core/permissions.py
Normal file
23
api/tacticalrmm/core/permissions.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from rest_framework import permissions
|
||||
|
||||
from tacticalrmm.permissions import _has_perm
|
||||
|
||||
|
||||
class ViewCoreSettingsPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
return _has_perm(r, "can_view_core_settings")
|
||||
|
||||
|
||||
class EditCoreSettingsPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
return _has_perm(r, "can_edit_core_settings")
|
||||
|
||||
|
||||
class ServerMaintPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
return _has_perm(r, "can_do_server_maint")
|
||||
|
||||
|
||||
class CodeSignPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
return _has_perm(r, "can_code_sign")
|
||||
@@ -1,7 +1,7 @@
|
||||
import pytz
|
||||
from rest_framework import serializers
|
||||
|
||||
from .models import CodeSignToken, CoreSettings, CustomField, GlobalKVStore
|
||||
from .models import CodeSignToken, CoreSettings, CustomField, GlobalKVStore, URLAction
|
||||
|
||||
|
||||
class CoreSettingsSerializer(serializers.ModelSerializer):
|
||||
@@ -39,3 +39,9 @@ class KeyStoreSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = GlobalKVStore
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class URLActionSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = URLAction
|
||||
fields = "__all__"
|
||||
|
||||
@@ -6,6 +6,7 @@ from loguru import logger
|
||||
from autotasks.models import AutomatedTask
|
||||
from autotasks.tasks import delete_win_task_schedule
|
||||
from checks.tasks import prune_check_history
|
||||
from agents.tasks import clear_faults_task
|
||||
from core.models import CoreSettings
|
||||
from tacticalrmm.celery import app
|
||||
|
||||
@@ -28,6 +29,25 @@ def core_maintenance_tasks():
|
||||
if now > task_time_utc:
|
||||
delete_win_task_schedule.delay(task.pk)
|
||||
|
||||
core = CoreSettings.objects.first()
|
||||
|
||||
# remove old CheckHistory data
|
||||
older_than = CoreSettings.objects.first().check_history_prune_days
|
||||
prune_check_history.delay(older_than)
|
||||
if core.check_history_prune_days > 0:
|
||||
prune_check_history.delay(core.check_history_prune_days)
|
||||
# clear faults
|
||||
if core.clear_faults_days > 0:
|
||||
clear_faults_task.delay(core.clear_faults_days)
|
||||
|
||||
|
||||
@app.task
|
||||
def cache_db_fields_task():
|
||||
from agents.models import Agent
|
||||
|
||||
for agent in Agent.objects.all():
|
||||
agent.pending_actions_count = agent.pendingactions.filter(
|
||||
status="pending"
|
||||
).count()
|
||||
agent.has_patches_pending = (
|
||||
agent.winupdates.filter(action="approve").filter(installed=False).exists()
|
||||
)
|
||||
agent.save(update_fields=["pending_actions_count", "has_patches_pending"])
|
||||
|
||||
@@ -8,8 +8,8 @@ from model_bakery import baker
|
||||
from tacticalrmm.test import TacticalTestCase
|
||||
|
||||
from .consumers import DashInfo
|
||||
from .models import CoreSettings, CustomField, GlobalKVStore
|
||||
from .serializers import CustomFieldSerializer, KeyStoreSerializer
|
||||
from .models import CoreSettings, CustomField, GlobalKVStore, URLAction
|
||||
from .serializers import CustomFieldSerializer, KeyStoreSerializer, URLActionSerializer
|
||||
from .tasks import core_maintenance_tasks
|
||||
|
||||
|
||||
@@ -331,3 +331,89 @@ class TestCoreTasks(TacticalTestCase):
|
||||
self.assertFalse(GlobalKVStore.objects.filter(pk=key.id).exists()) # type: ignore
|
||||
|
||||
self.check_not_authenticated("delete", url)
|
||||
|
||||
def test_get_urlaction(self):
|
||||
url = "/core/urlaction/"
|
||||
|
||||
# setup
|
||||
action = baker.make("core.URLAction", _quantity=2)
|
||||
|
||||
r = self.client.get(url)
|
||||
serializer = URLActionSerializer(action, many=True)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(len(r.data), 2) # type: ignore
|
||||
self.assertEqual(r.data, serializer.data) # type: ignore
|
||||
|
||||
self.check_not_authenticated("get", url)
|
||||
|
||||
def test_add_urlaction(self):
|
||||
url = "/core/urlaction/"
|
||||
|
||||
data = {"name": "name", "desc": "desc", "pattern": "pattern"}
|
||||
r = self.client.post(url, data)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
self.check_not_authenticated("post", url)
|
||||
|
||||
def test_update_urlaction(self):
|
||||
# setup
|
||||
action = baker.make("core.URLAction")
|
||||
|
||||
# test not found
|
||||
r = self.client.put("/core/urlaction/500/")
|
||||
self.assertEqual(r.status_code, 404)
|
||||
|
||||
url = f"/core/urlaction/{action.id}/" # type: ignore
|
||||
data = {"name": "test", "pattern": "text"}
|
||||
r = self.client.put(url, data)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
new_action = URLAction.objects.get(pk=action.id) # type: ignore
|
||||
self.assertEqual(new_action.name, data["name"])
|
||||
self.assertEqual(new_action.pattern, data["pattern"])
|
||||
|
||||
self.check_not_authenticated("put", url)
|
||||
|
||||
def test_delete_urlaction(self):
|
||||
# setup
|
||||
action = baker.make("core.URLAction")
|
||||
|
||||
# test not found
|
||||
r = self.client.delete("/core/urlaction/500/")
|
||||
self.assertEqual(r.status_code, 404)
|
||||
|
||||
url = f"/core/urlaction/{action.id}/" # type: ignore
|
||||
r = self.client.delete(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
self.assertFalse(URLAction.objects.filter(pk=action.id).exists()) # type: ignore
|
||||
|
||||
self.check_not_authenticated("delete", url)
|
||||
|
||||
def test_run_url_action(self):
|
||||
self.maxDiff = None
|
||||
# setup
|
||||
agent = baker.make_recipe(
|
||||
"agents.agent", agent_id="123123-assdss4s-343-sds545-45dfdf|DESKTOP"
|
||||
)
|
||||
baker.make("core.GlobalKVStore", name="Test Name", value="value with space")
|
||||
action = baker.make(
|
||||
"core.URLAction",
|
||||
pattern="https://remote.example.com/connect?globalstore={{global.Test Name}}&client_name={{client.name}}&site id={{site.id}}&agent_id={{agent.agent_id}}",
|
||||
)
|
||||
|
||||
url = "/core/urlaction/run/"
|
||||
# test not found
|
||||
r = self.client.patch(url, {"agent": 500, "action": 500})
|
||||
self.assertEqual(r.status_code, 404)
|
||||
|
||||
data = {"agent": agent.id, "action": action.id} # type: ignore
|
||||
r = self.client.patch(url, data)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
self.assertEqual(
|
||||
r.data, # type: ignore
|
||||
f"https://remote.example.com/connect?globalstore=value%20with%20space&client_name={agent.client.name}&site%20id={agent.site.id}&agent_id=123123-assdss4s-343-sds545-45dfdf%7CDESKTOP",
|
||||
)
|
||||
|
||||
self.check_not_authenticated("patch", url)
|
||||
|
||||
@@ -15,4 +15,8 @@ urlpatterns = [
|
||||
path("codesign/", views.CodeSign.as_view()),
|
||||
path("keystore/", views.GetAddKeyStore.as_view()),
|
||||
path("keystore/<int:pk>/", views.UpdateDeleteKeyStore.as_view()),
|
||||
path("urlaction/", views.GetAddURLAction.as_view()),
|
||||
path("urlaction/<int:pk>/", views.UpdateDeleteURLAction.as_view()),
|
||||
path("urlaction/run/", views.RunURLAction.as_view()),
|
||||
path("smstest/", views.TwilioSMSTest.as_view()),
|
||||
]
|
||||
|
||||
@@ -1,26 +1,38 @@
|
||||
import os
|
||||
import pprint
|
||||
import re
|
||||
|
||||
from django.conf import settings
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.decorators import api_view, permission_classes
|
||||
from rest_framework.exceptions import ParseError
|
||||
from rest_framework.parsers import FileUploadParser
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from agents.permissions import MeshPerms
|
||||
from tacticalrmm.utils import notify_error
|
||||
|
||||
from .models import CodeSignToken, CoreSettings, CustomField, GlobalKVStore
|
||||
from .models import CodeSignToken, CoreSettings, CustomField, GlobalKVStore, URLAction
|
||||
from .permissions import (
|
||||
CodeSignPerms,
|
||||
ViewCoreSettingsPerms,
|
||||
EditCoreSettingsPerms,
|
||||
ServerMaintPerms,
|
||||
)
|
||||
from .serializers import (
|
||||
CodeSignTokenSerializer,
|
||||
CoreSettingsSerializer,
|
||||
CustomFieldSerializer,
|
||||
KeyStoreSerializer,
|
||||
URLActionSerializer,
|
||||
)
|
||||
|
||||
|
||||
class UploadMeshAgent(APIView):
|
||||
permission_classes = [IsAuthenticated, MeshPerms]
|
||||
parser_class = (FileUploadParser,)
|
||||
|
||||
def put(self, request, format=None):
|
||||
@@ -40,12 +52,14 @@ class UploadMeshAgent(APIView):
|
||||
|
||||
|
||||
@api_view()
|
||||
@permission_classes([IsAuthenticated, ViewCoreSettingsPerms])
|
||||
def get_core_settings(request):
|
||||
settings = CoreSettings.objects.first()
|
||||
return Response(CoreSettingsSerializer(settings).data)
|
||||
|
||||
|
||||
@api_view(["PATCH"])
|
||||
@permission_classes([IsAuthenticated, EditCoreSettingsPerms])
|
||||
def edit_settings(request):
|
||||
coresettings = CoreSettings.objects.first()
|
||||
serializer = CoreSettingsSerializer(instance=coresettings, data=request.data)
|
||||
@@ -62,17 +76,24 @@ def version(request):
|
||||
|
||||
@api_view()
|
||||
def dashboard_info(request):
|
||||
from tacticalrmm.utils import get_latest_trmm_ver
|
||||
|
||||
return Response(
|
||||
{
|
||||
"trmm_version": settings.TRMM_VERSION,
|
||||
"latest_trmm_ver": get_latest_trmm_ver(),
|
||||
"dark_mode": request.user.dark_mode,
|
||||
"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,
|
||||
"client_tree_sort": request.user.client_tree_sort,
|
||||
"client_tree_splitter": request.user.client_tree_splitter,
|
||||
"loading_bar_color": request.user.loading_bar_color,
|
||||
"no_code_sign": hasattr(settings, "NOCODESIGN") and settings.NOCODESIGN,
|
||||
"clear_search_when_switching": request.user.clear_search_when_switching,
|
||||
"hosted": hasattr(settings, "HOSTED") and settings.HOSTED,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -91,6 +112,7 @@ def email_test(request):
|
||||
|
||||
|
||||
@api_view(["POST"])
|
||||
@permission_classes([IsAuthenticated, ServerMaintPerms])
|
||||
def server_maintenance(request):
|
||||
from tacticalrmm.utils import reload_nats
|
||||
|
||||
@@ -145,6 +167,8 @@ def server_maintenance(request):
|
||||
|
||||
|
||||
class GetAddCustomFields(APIView):
|
||||
permission_classes = [IsAuthenticated, EditCoreSettingsPerms]
|
||||
|
||||
def get(self, request):
|
||||
fields = CustomField.objects.all()
|
||||
return Response(CustomFieldSerializer(fields, many=True).data)
|
||||
@@ -165,6 +189,8 @@ class GetAddCustomFields(APIView):
|
||||
|
||||
|
||||
class GetUpdateDeleteCustomFields(APIView):
|
||||
permission_classes = [IsAuthenticated, EditCoreSettingsPerms]
|
||||
|
||||
def get(self, request, pk):
|
||||
custom_field = get_object_or_404(CustomField, pk=pk)
|
||||
|
||||
@@ -188,6 +214,8 @@ class GetUpdateDeleteCustomFields(APIView):
|
||||
|
||||
|
||||
class CodeSign(APIView):
|
||||
permission_classes = [IsAuthenticated, CodeSignPerms]
|
||||
|
||||
def get(self, request):
|
||||
token = CodeSignToken.objects.first()
|
||||
return Response(CodeSignTokenSerializer(token).data)
|
||||
@@ -231,8 +259,27 @@ class CodeSign(APIView):
|
||||
ret = "Something went wrong"
|
||||
return notify_error(ret)
|
||||
|
||||
def post(self, request):
|
||||
from agents.models import Agent
|
||||
from agents.tasks import force_code_sign
|
||||
|
||||
err = "A valid token must be saved first"
|
||||
try:
|
||||
t = CodeSignToken.objects.first().token
|
||||
except:
|
||||
return notify_error(err)
|
||||
|
||||
if t is None or t == "":
|
||||
return notify_error(err)
|
||||
|
||||
pks: list[int] = list(Agent.objects.only("pk").values_list("pk", flat=True))
|
||||
force_code_sign.delay(pks=pks)
|
||||
return Response("Agents will be code signed shortly")
|
||||
|
||||
|
||||
class GetAddKeyStore(APIView):
|
||||
permission_classes = [IsAuthenticated, EditCoreSettingsPerms]
|
||||
|
||||
def get(self, request):
|
||||
keys = GlobalKVStore.objects.all()
|
||||
return Response(KeyStoreSerializer(keys, many=True).data)
|
||||
@@ -246,6 +293,8 @@ class GetAddKeyStore(APIView):
|
||||
|
||||
|
||||
class UpdateDeleteKeyStore(APIView):
|
||||
permission_classes = [IsAuthenticated, EditCoreSettingsPerms]
|
||||
|
||||
def put(self, request, pk):
|
||||
key = get_object_or_404(GlobalKVStore, pk=pk)
|
||||
|
||||
@@ -259,3 +308,79 @@ class UpdateDeleteKeyStore(APIView):
|
||||
get_object_or_404(GlobalKVStore, pk=pk).delete()
|
||||
|
||||
return Response("ok")
|
||||
|
||||
|
||||
class GetAddURLAction(APIView):
|
||||
def get(self, request):
|
||||
actions = URLAction.objects.all()
|
||||
return Response(URLActionSerializer(actions, many=True).data)
|
||||
|
||||
def post(self, request):
|
||||
serializer = URLActionSerializer(data=request.data, partial=True)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
|
||||
return Response("ok")
|
||||
|
||||
|
||||
class UpdateDeleteURLAction(APIView):
|
||||
def put(self, request, pk):
|
||||
action = get_object_or_404(URLAction, pk=pk)
|
||||
|
||||
serializer = URLActionSerializer(
|
||||
instance=action, data=request.data, partial=True
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
|
||||
return Response("ok")
|
||||
|
||||
def delete(self, request, pk):
|
||||
get_object_or_404(URLAction, pk=pk).delete()
|
||||
|
||||
return Response("ok")
|
||||
|
||||
|
||||
class RunURLAction(APIView):
|
||||
def patch(self, request):
|
||||
from requests.utils import requote_uri
|
||||
|
||||
from agents.models import Agent
|
||||
from tacticalrmm.utils import replace_db_values
|
||||
|
||||
agent = get_object_or_404(Agent, pk=request.data["agent"])
|
||||
action = get_object_or_404(URLAction, pk=request.data["action"])
|
||||
|
||||
pattern = re.compile("\\{\\{([\\w\\s]+\\.[\\w\\s]+)\\}\\}")
|
||||
|
||||
url_pattern = action.pattern
|
||||
|
||||
for string in re.findall(pattern, action.pattern):
|
||||
value = replace_db_values(string=string, agent=agent, quotes=False)
|
||||
|
||||
url_pattern = re.sub("\\{\\{" + string + "\\}\\}", str(value), url_pattern)
|
||||
|
||||
return Response(requote_uri(url_pattern))
|
||||
|
||||
|
||||
class TwilioSMSTest(APIView):
|
||||
def get(self, request):
|
||||
from twilio.rest import Client as TwClient
|
||||
|
||||
core = CoreSettings.objects.first()
|
||||
if not core.sms_is_configured:
|
||||
return notify_error(
|
||||
"All fields are required, including at least 1 recipient"
|
||||
)
|
||||
|
||||
try:
|
||||
tw_client = TwClient(core.twilio_account_sid, core.twilio_auth_token)
|
||||
tw_client.messages.create(
|
||||
body="TacticalRMM Test SMS",
|
||||
to=core.sms_alert_recipients[0],
|
||||
from_=core.twilio_number,
|
||||
)
|
||||
except Exception as e:
|
||||
return notify_error(pprint.pformat(e))
|
||||
|
||||
return Response("SMS Test OK!")
|
||||
|
||||
21
api/tacticalrmm/logs/permissions.py
Normal file
21
api/tacticalrmm/logs/permissions.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from rest_framework import permissions
|
||||
|
||||
from tacticalrmm.permissions import _has_perm
|
||||
|
||||
|
||||
class AuditLogPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
return _has_perm(r, "can_view_auditlogs")
|
||||
|
||||
|
||||
class ManagePendingActionPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
if r.method == "PATCH":
|
||||
return True
|
||||
|
||||
return _has_perm(r, "can_manage_pendingactions")
|
||||
|
||||
|
||||
class DebugLogPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
return _has_perm(r, "can_view_debuglogs")
|
||||
@@ -9,7 +9,8 @@ from django.http import HttpResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils import timezone as djangotime
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import api_view
|
||||
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
|
||||
|
||||
@@ -20,10 +21,13 @@ from agents.serializers import AgentHostnameSerializer
|
||||
from tacticalrmm.utils import notify_error
|
||||
|
||||
from .models import AuditLog, PendingAction
|
||||
from .permissions import AuditLogPerms, DebugLogPerms, ManagePendingActionPerms
|
||||
from .serializers import AuditLogSerializer, PendingActionSerializer
|
||||
|
||||
|
||||
class GetAuditLogs(APIView):
|
||||
permission_classes = [IsAuthenticated, AuditLogPerms]
|
||||
|
||||
def patch(self, request):
|
||||
from agents.models import Agent
|
||||
from clients.models import Client
|
||||
@@ -92,6 +96,8 @@ class GetAuditLogs(APIView):
|
||||
|
||||
|
||||
class FilterOptionsAuditLog(APIView):
|
||||
permission_classes = [IsAuthenticated, AuditLogPerms]
|
||||
|
||||
def post(self, request):
|
||||
if request.data["type"] == "agent":
|
||||
agents = Agent.objects.filter(hostname__icontains=request.data["pattern"])
|
||||
@@ -99,7 +105,9 @@ class FilterOptionsAuditLog(APIView):
|
||||
|
||||
if request.data["type"] == "user":
|
||||
users = User.objects.filter(
|
||||
username__icontains=request.data["pattern"], agent=None
|
||||
username__icontains=request.data["pattern"],
|
||||
agent=None,
|
||||
is_installer_user=False,
|
||||
)
|
||||
return Response(UserSerializer(users, many=True).data)
|
||||
|
||||
@@ -107,6 +115,8 @@ class FilterOptionsAuditLog(APIView):
|
||||
|
||||
|
||||
class PendingActions(APIView):
|
||||
permission_classes = [IsAuthenticated, ManagePendingActionPerms]
|
||||
|
||||
def patch(self, request):
|
||||
status_filter = "completed" if request.data["showCompleted"] else "pending"
|
||||
if "agentPK" in request.data.keys():
|
||||
@@ -149,6 +159,7 @@ class PendingActions(APIView):
|
||||
|
||||
|
||||
@api_view()
|
||||
@permission_classes([IsAuthenticated, DebugLogPerms])
|
||||
def debug_log(request, mode, hostname, order):
|
||||
log_file = settings.LOG_CONFIG["handlers"][0]["sink"]
|
||||
|
||||
@@ -191,6 +202,7 @@ def debug_log(request, mode, hostname, order):
|
||||
|
||||
|
||||
@api_view()
|
||||
@permission_classes([IsAuthenticated, DebugLogPerms])
|
||||
def download_log(request):
|
||||
log_file = settings.LOG_CONFIG["handlers"][0]["sink"]
|
||||
if settings.DEBUG:
|
||||
|
||||
@@ -6,4 +6,6 @@ mkdocs-material
|
||||
pymdown-extensions
|
||||
Pygments
|
||||
isort
|
||||
mypy
|
||||
mypy
|
||||
types-pytz
|
||||
types-pytz
|
||||
@@ -1,22 +1,22 @@
|
||||
asgiref==3.3.4
|
||||
asyncio-nats-client==0.11.4
|
||||
celery==5.0.5
|
||||
certifi==2020.12.5
|
||||
celery==5.1.1
|
||||
certifi==2021.5.30
|
||||
cffi==1.14.5
|
||||
channels==3.0.3
|
||||
channels_redis==3.2.0
|
||||
chardet==4.0.0
|
||||
cryptography==3.4.7
|
||||
daphne==3.0.2
|
||||
Django==3.2.0
|
||||
Django==3.2.4
|
||||
django-cors-headers==3.7.0
|
||||
django-rest-knox==4.1.0
|
||||
djangorestframework==3.12.4
|
||||
future==0.18.2
|
||||
kombu==5.0.2
|
||||
loguru==0.5.3
|
||||
msgpack==1.0.2
|
||||
packaging==20.9
|
||||
psycopg2-binary==2.8.6
|
||||
psycopg2-binary==2.9.1
|
||||
pycparser==2.20
|
||||
pycryptodome==3.10.1
|
||||
pyotp==2.6.0
|
||||
@@ -25,12 +25,12 @@ pytz==2021.1
|
||||
qrcode==6.1
|
||||
redis==3.5.3
|
||||
requests==2.25.1
|
||||
six==1.15.0
|
||||
six==1.16.0
|
||||
sqlparse==0.4.1
|
||||
twilio==6.56.0
|
||||
urllib3==1.26.4
|
||||
twilio==6.60.0
|
||||
urllib3==1.26.5
|
||||
uWSGI==2.0.19.1
|
||||
validators==0.18.2
|
||||
vine==5.0.0
|
||||
websockets==8.1
|
||||
websockets==9.1
|
||||
zipp==3.4.1
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
"name": "Firefox - Clean Cache",
|
||||
"description": "This script will clean up Mozilla Firefox for all users.",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Browsers"
|
||||
"category": "TRMM (Win):Browsers",
|
||||
"default_timeout": "300"
|
||||
},
|
||||
{
|
||||
"guid": "3ff6a386-11d1-4f9d-8cca-1b0563bb6443",
|
||||
@@ -15,7 +16,8 @@
|
||||
"name": "Chrome - Clear Cache for All Users",
|
||||
"description": "This script will clean up Google Chrome for all users.",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Browsers"
|
||||
"category": "TRMM (Win):Browsers",
|
||||
"default_timeout": "300"
|
||||
},
|
||||
{
|
||||
"guid": "be1de837-f677-4ac5-aa0c-37a0fc9991fc",
|
||||
@@ -24,7 +26,8 @@
|
||||
"name": "Adobe Reader DC - Install",
|
||||
"description": "Installs Adobe Reader DC.",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):3rd Party Software>Chocolatey"
|
||||
"category": "TRMM (Win):3rd Party Software>Chocolatey",
|
||||
"default_timeout": "300"
|
||||
},
|
||||
{
|
||||
"guid": "2ee134d5-76aa-4160-b334-a1efbc62079f",
|
||||
@@ -33,7 +36,8 @@
|
||||
"name": "Duplicati - Install",
|
||||
"description": "This script installs Duplicati 2.0.5.1 as a service.",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):3rd Party Software"
|
||||
"category": "TRMM (Win):3rd Party Software",
|
||||
"default_timeout": "300"
|
||||
},
|
||||
{
|
||||
"guid": "81cc5bcb-01bf-4b0c-89b9-0ac0f3fe0c04",
|
||||
@@ -42,7 +46,8 @@
|
||||
"name": "Windows Update - Reset",
|
||||
"description": "This script will reset all of the Windows Updates components to DEFAULT SETTINGS.",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Updates"
|
||||
"category": "TRMM (Win):Updates",
|
||||
"default_timeout": "300"
|
||||
},
|
||||
{
|
||||
"guid": "8db87ff0-a9b4-4d9d-bc55-377bbcb85b6d",
|
||||
@@ -51,7 +56,8 @@
|
||||
"name": "Disk - Cleanup C: drive",
|
||||
"description": "Cleans the C: drive's Window Temperary files, Windows SoftwareDistribution folder, the local users Temperary folder, IIS logs (if applicable) and empties the recycling bin. All deleted files will go into a log transcript in $env:TEMP. By default this script leaves files that are newer than 7 days old however this variable can be edited.",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Maintenance"
|
||||
"category": "TRMM (Win):Maintenance",
|
||||
"default_timeout": "25000"
|
||||
},
|
||||
{
|
||||
"guid": "2f28e8c1-ae0f-4b46-a826-f513974526a3",
|
||||
@@ -78,7 +84,8 @@
|
||||
"name": "Speed Test - Python",
|
||||
"description": "Runs a Speed Test using Python",
|
||||
"shell": "python",
|
||||
"category": "TRMM (Win):Network"
|
||||
"category": "TRMM (Win):Network",
|
||||
"default_timeout": "120"
|
||||
},
|
||||
{
|
||||
"guid": "9d34f482-1f0c-4b2f-b65f-a9cf3c13ef5f",
|
||||
@@ -152,6 +159,27 @@
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Hardware"
|
||||
},
|
||||
{
|
||||
"guid": "72c56717-28ed-4cc6-b30f-b362d30fb4b6",
|
||||
"filename": "Win_Hardware_SN.ps1",
|
||||
"submittedBy": "https://github.com/subzdev",
|
||||
"name": "Hardware - Get Serial Number",
|
||||
"description": "Returns BIOS Serial Number - Use with Custom Fields for later use",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Collectors"
|
||||
},
|
||||
{
|
||||
"guid": "973c34d7-cab0-4fda-999c-b4933655f946",
|
||||
"filename": "Win_Screenconnect_GetGUID.ps1",
|
||||
"submittedBy": "https://github.com/silversword411",
|
||||
"name": "Screenconnect - Get GUID for client",
|
||||
"description": "Returns Screenconnect GUID for client - Use with Custom Fields for later use. ",
|
||||
"args": [
|
||||
"-serviceName {{client.ScreenConnectService}}"
|
||||
],
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Collectors"
|
||||
},
|
||||
{
|
||||
"guid": "95a2ee6f-b89b-4551-856e-3081b041caa7",
|
||||
"filename": "Win_Power_Profile_Reset_High_Performance_to_Defaults.ps1",
|
||||
@@ -177,7 +205,8 @@
|
||||
"name": "Windows 10 Upgrade",
|
||||
"description": "Forces an upgrade to the latest release of Windows 10.",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Updates"
|
||||
"category": "TRMM (Win):Updates",
|
||||
"default_timeout": "25000"
|
||||
},
|
||||
{
|
||||
"guid": "375323e5-cac6-4f35-a304-bb7cef35902d",
|
||||
@@ -213,7 +242,8 @@
|
||||
"name": "SSH - Install Feature and Enable",
|
||||
"description": "Installs and enabled OpenSSH Server Feature in Win10",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Windows Features"
|
||||
"category": "TRMM (Win):Windows Features",
|
||||
"default_timeout": "300"
|
||||
},
|
||||
{
|
||||
"guid": "2435297a-6263-4e90-8688-1847400d0e22",
|
||||
@@ -242,6 +272,20 @@
|
||||
"shell": "cmd",
|
||||
"category": "TRMM (Win):Active Directory"
|
||||
},
|
||||
{
|
||||
"guid": "5320dfc8-022a-41e7-9e39-11c493545ec9",
|
||||
"filename": "Win_AD_Hudu_ADDS_Documentation.ps1",
|
||||
"submittedBy": "https://github.com/unplugged216",
|
||||
"name": "ADDS - Directory documentation in Hudu",
|
||||
"description": "Auto generates ADDS documentation and submits it to your Hudu instance.",
|
||||
"args": [
|
||||
"-ClientName {{client.name}}",
|
||||
"-HuduBaseDomain {{global.HuduBaseDomain}}",
|
||||
"-HuduApiKey {{global.HuduApiKey}}"
|
||||
],
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Active Directory"
|
||||
},
|
||||
{
|
||||
"guid": "b6b9912f-4274-4162-99cc-9fd47fbcb292",
|
||||
"filename": "Win_ADDC_Sync_Start.bat",
|
||||
@@ -330,7 +374,8 @@
|
||||
"name": "Update Installed Apps",
|
||||
"description": "Update all apps that were installed using Chocolatey.",
|
||||
"shell": "cmd",
|
||||
"category": "TRMM (Win):3rd Party Software>Chocolatey"
|
||||
"category": "TRMM (Win):3rd Party Software>Chocolatey",
|
||||
"default_timeout": "3600"
|
||||
},
|
||||
{
|
||||
"guid": "fff8024d-d72e-4457-84fa-6c780f69a16f",
|
||||
@@ -341,6 +386,15 @@
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Active Directory"
|
||||
},
|
||||
{
|
||||
"guid": "3afd07c0-04fd-4b23-b5f2-88205c0744d4",
|
||||
"filename": "Win_User_Admins_Local_Disable.ps1",
|
||||
"submittedBy": "https://github.com/dinger1986",
|
||||
"name": "Local Administrators - Disables all local admins if joined to domain or AzureAD",
|
||||
"description": "Checks to see if computer is either joined to a AD domain or Azure AD. If it is, it disables all local admin accounts. If not joined to domain/AzureAD, leaves admin accounts in place",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):User Management"
|
||||
},
|
||||
{
|
||||
"guid": "71090fc4-faa6-460b-adb0-95d7863544e1",
|
||||
"filename": "Win_Check_Events_for_Bluescreens.ps1",
|
||||
@@ -405,6 +459,8 @@
|
||||
"args": [
|
||||
"-serviceName {{client.ScreenConnectService}}",
|
||||
"-url {{client.ScreenConnectInstaller}}",
|
||||
"-clientname {{client.name}}",
|
||||
"-sitename {{site.name}}",
|
||||
"-action install"
|
||||
],
|
||||
"default_timeout": "90",
|
||||
@@ -481,6 +537,16 @@
|
||||
"category": "TRMM (Win):Network",
|
||||
"default_timeout": "90"
|
||||
},
|
||||
{
|
||||
"guid": "0caa33bc-89ca-47e0-ad4a-04626ae6384d",
|
||||
"filename": "Win_Network_TCP_Reset_Stack.bat",
|
||||
"submittedBy": "https://github.com/silversword411",
|
||||
"name": "Network - Reset tcp using netsh",
|
||||
"description": "resets tcp stack using netsh",
|
||||
"shell": "cmd",
|
||||
"category": "TRMM (Win):Network",
|
||||
"default_timeout": "120"
|
||||
},
|
||||
{
|
||||
"guid": "6ce5682a-49db-4c0b-9417-609cf905ac43",
|
||||
"filename": "Win_Win10_Change_Key_and_Activate.ps1",
|
||||
@@ -573,9 +639,18 @@
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Storage"
|
||||
},
|
||||
{
|
||||
"guid": "6a52f495-d43e-40f4-91a9-bbe4f578e6d1",
|
||||
"filename": "Win_User_Create.ps1",
|
||||
"submittedBy": "https://github.com/brodur",
|
||||
"name": "Create Local User",
|
||||
"description": "Create a local user. Parameters are: username, password and optional: description, fullname, group (adds to Users if not specified)",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Other"
|
||||
},
|
||||
{
|
||||
"guid": "57997ec7-b293-4fd5-9f90-a25426d0eb90",
|
||||
"filename": "Win_Get_Computer_Users.ps1",
|
||||
"filename": "Win_Users_List.ps1",
|
||||
"submittedBy": "https://github.com/tremor021",
|
||||
"name": "Get Computer Users",
|
||||
"description": "Get list of computer users and show which one is enabled",
|
||||
@@ -599,5 +674,55 @@
|
||||
"description": "Add a task to Task Scheduler, needs editing",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Other"
|
||||
},
|
||||
{
|
||||
"guid": "e371f1c6-0dd9-44de-824c-a17e1ca4c4ab",
|
||||
"filename": "Win_Outlook_SentItems_To_Delegated_Folders.ps1",
|
||||
"submittedBy": "https://github.com/dinger1986",
|
||||
"name": "Outlook - Delegated folders set for all profiles",
|
||||
"description": "Uses RunAsUser to setup sent items for the currently logged on user on delegated folders to go into the delegated folders sent for all.",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Office",
|
||||
"default_timeout": "90"
|
||||
},
|
||||
{
|
||||
"guid": "17040742-184a-4251-8f7b-4a1b0a1f02d1",
|
||||
"filename": "Win_File_Copy_Misc.ps1",
|
||||
"submittedBy": "https://github.com/tremor021",
|
||||
"name": "EXAMPLE File Copying using powershell",
|
||||
"description": "Reference Script: Will need manual tweaking, for copying files/folders from paths/websites to local",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Misc>Reference",
|
||||
"default_timeout": "1"
|
||||
},
|
||||
{
|
||||
"guid": "168037d8-78e6-4a6a-a9a9-8ec2c1dbe949",
|
||||
"filename": "Win_MSI_Install.ps1",
|
||||
"submittedBy": "https://github.com/silversword411",
|
||||
"name": "EXAMPLE Function for running MSI install via powershell",
|
||||
"description": "Reference Script: Will need manual tweaking, for running MSI from powershell",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Misc>Reference",
|
||||
"default_timeout": "1"
|
||||
},
|
||||
{
|
||||
"guid": "453c6d22-84b7-4767-8b5f-b825f233cf55",
|
||||
"filename": "Win_AD_Join_Computer.ps1",
|
||||
"submittedBy": "https://github.com/rfost52",
|
||||
"name": "AD - Join Computer to Domain",
|
||||
"description": "Join computer to a domain in Active Directory",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Active Directory",
|
||||
"default_timeout": "300"
|
||||
},
|
||||
{
|
||||
"guid": "962d3cce-49a2-4f3e-a790-36f62a6799a0",
|
||||
"filename": "Win_Collect_System_Report_And_Email.ps1",
|
||||
"submittedBy": "https://github.com/rfost52",
|
||||
"name": "Collect System Report and Email",
|
||||
"description": "Generates a system report in HTML format, then emails it",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Other",
|
||||
"default_timeout": "300"
|
||||
}
|
||||
]
|
||||
@@ -1,6 +1,6 @@
|
||||
import base64
|
||||
import re
|
||||
from typing import Any, List, Union
|
||||
from typing import List, Optional
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
@@ -8,6 +8,7 @@ from django.db import models
|
||||
from loguru import logger
|
||||
|
||||
from logs.models import BaseAuditModel
|
||||
from tacticalrmm.utils import replace_db_values
|
||||
|
||||
SCRIPT_SHELLS = [
|
||||
("powershell", "Powershell"),
|
||||
@@ -193,10 +194,7 @@ class Script(BaseAuditModel):
|
||||
return ScriptSerializer(script).data
|
||||
|
||||
@classmethod
|
||||
def parse_script_args(
|
||||
cls, agent, shell: str, args: List[str] = list()
|
||||
) -> Union[List[str], None]:
|
||||
from core.models import CustomField, GlobalKVStore
|
||||
def parse_script_args(cls, agent, shell: str, args: List[str] = list()) -> list:
|
||||
|
||||
if not args:
|
||||
return []
|
||||
@@ -211,100 +209,15 @@ class Script(BaseAuditModel):
|
||||
if match:
|
||||
# only get the match between the () in regex
|
||||
string = match.group(1)
|
||||
value = replace_db_values(string=string, agent=agent, shell=shell)
|
||||
|
||||
# split by period if exists. First should be model and second should be property
|
||||
temp = string.split(".")
|
||||
|
||||
# check for model and property
|
||||
if len(temp) != 2:
|
||||
# ignore arg since it is invalid
|
||||
continue
|
||||
|
||||
# value is in the global keystore and replace value
|
||||
if temp[0] == "global":
|
||||
if GlobalKVStore.objects.filter(name=temp[1]).exists():
|
||||
value = GlobalKVStore.objects.get(name=temp[1]).value
|
||||
temp_args.append(
|
||||
re.sub("\\{\\{.*\\}\\}", "'" + value + "'", arg)
|
||||
)
|
||||
continue
|
||||
else:
|
||||
# ignore since value doesn't exist
|
||||
continue
|
||||
|
||||
if temp[0] == "client":
|
||||
model = "client"
|
||||
obj = agent.client
|
||||
elif temp[0] == "site":
|
||||
model = "site"
|
||||
obj = agent.site
|
||||
elif temp[0] == "agent":
|
||||
model = "agent"
|
||||
obj = agent
|
||||
if value:
|
||||
temp_args.append(re.sub("\\{\\{.*\\}\\}", value, arg))
|
||||
else:
|
||||
# ignore arg since it is invalid
|
||||
continue
|
||||
|
||||
if hasattr(obj, temp[1]):
|
||||
value = getattr(obj, temp[1])
|
||||
|
||||
elif CustomField.objects.filter(model=model, name=temp[1]).exists():
|
||||
|
||||
field = CustomField.objects.get(model=model, name=temp[1])
|
||||
model_fields = getattr(field, f"{model}_fields")
|
||||
value = None
|
||||
if model_fields.filter(**{model: obj}).exists():
|
||||
value = model_fields.get(**{model: obj}).value
|
||||
|
||||
if not value and field.default_value:
|
||||
value = field.default_value
|
||||
|
||||
# check if value exists and if not use defa
|
||||
if value and field.type == "multiple":
|
||||
value = format_shell_array(shell, value)
|
||||
elif value and field.type == "checkbox":
|
||||
value = format_shell_bool(shell, value)
|
||||
|
||||
if not value:
|
||||
continue
|
||||
|
||||
else:
|
||||
# ignore arg since property is invalid
|
||||
continue
|
||||
|
||||
# replace the value in the arg and push to array
|
||||
# log any unhashable type errors
|
||||
try:
|
||||
temp_args.append(re.sub("\\{\\{.*\\}\\}", "'" + value + "'", arg)) # type: ignore
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
continue
|
||||
# pass parameter unaltered
|
||||
temp_args.append(arg)
|
||||
|
||||
else:
|
||||
temp_args.append(arg)
|
||||
|
||||
return temp_args
|
||||
|
||||
|
||||
def format_shell_array(shell: str, value: Any) -> str:
|
||||
if shell == "cmd":
|
||||
return "array args are not supported with batch"
|
||||
elif shell == "powershell":
|
||||
temp_string = ""
|
||||
for item in value:
|
||||
temp_string += item + ","
|
||||
return temp_string.strip(",")
|
||||
else: # python
|
||||
temp_string = ""
|
||||
for item in value:
|
||||
temp_string += item + ","
|
||||
return temp_string.strip(",")
|
||||
|
||||
|
||||
def format_shell_bool(shell: str, value: Any) -> str:
|
||||
if shell == "cmd":
|
||||
return "1" if value else "0"
|
||||
elif shell == "powershell":
|
||||
return "$True" if value else "$False"
|
||||
else: # python
|
||||
return "True" if value else "False"
|
||||
|
||||
11
api/tacticalrmm/scripts/permissions.py
Normal file
11
api/tacticalrmm/scripts/permissions.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from rest_framework import permissions
|
||||
|
||||
from tacticalrmm.permissions import _has_perm
|
||||
|
||||
|
||||
class ManageScriptsPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
if r.method == "GET":
|
||||
return True
|
||||
|
||||
return _has_perm(r, "can_manage_scripts")
|
||||
@@ -22,14 +22,5 @@ def handle_bulk_command_task(agentpks, cmd, shell, timeout) -> None:
|
||||
@app.task
|
||||
def handle_bulk_script_task(scriptpk, agentpks, args, timeout) -> None:
|
||||
script = Script.objects.get(pk=scriptpk)
|
||||
nats_data = {
|
||||
"func": "runscript",
|
||||
"timeout": timeout,
|
||||
"script_args": args,
|
||||
"payload": {
|
||||
"code": script.code,
|
||||
"shell": script.shell,
|
||||
},
|
||||
}
|
||||
for agent in Agent.objects.filter(pk__in=agentpks):
|
||||
asyncio.run(agent.nats_cmd(nats_data, wait=False))
|
||||
agent.run_script(scriptpk=script.pk, args=args, timeout=timeout)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import json
|
||||
import os
|
||||
from email.policy import default
|
||||
from pathlib import Path
|
||||
|
||||
from django.conf import settings
|
||||
@@ -15,6 +14,7 @@ from .serializers import ScriptSerializer, ScriptTableSerializer
|
||||
|
||||
class TestScriptViews(TacticalTestCase):
|
||||
def setUp(self):
|
||||
self.setup_coresettings()
|
||||
self.authenticate()
|
||||
|
||||
def test_get_scripts(self):
|
||||
@@ -288,3 +288,212 @@ class TestScriptViews(TacticalTestCase):
|
||||
fn: str = script["filename"]
|
||||
if " " in fn:
|
||||
raise Exception(f"{fn} must not contain spaces in filename")
|
||||
|
||||
def test_script_arg_variable_replacement(self):
|
||||
|
||||
agent = baker.make_recipe("agents.agent", public_ip="12.12.12.12")
|
||||
args = [
|
||||
"-Parameter",
|
||||
"-Another {{agent.public_ip}}",
|
||||
"-Client {{client.name}}",
|
||||
"-Site {{site.name}}",
|
||||
]
|
||||
|
||||
self.assertEqual(
|
||||
[
|
||||
"-Parameter",
|
||||
"-Another '12.12.12.12'",
|
||||
f"-Client '{agent.client.name}'",
|
||||
f"-Site '{agent.site.name}'",
|
||||
],
|
||||
Script.parse_script_args(agent=agent, shell="python", args=args),
|
||||
)
|
||||
|
||||
def test_script_arg_replacement_custom_field(self):
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
field = baker.make(
|
||||
"core.CustomField",
|
||||
name="Test Field",
|
||||
model="agent",
|
||||
type="text",
|
||||
default_value_string="DEFAULT",
|
||||
)
|
||||
|
||||
args = ["-Parameter", "-Another {{agent.Test Field}}"]
|
||||
|
||||
# test default value
|
||||
self.assertEqual(
|
||||
["-Parameter", "-Another 'DEFAULT'"],
|
||||
Script.parse_script_args(agent=agent, shell="python", args=args),
|
||||
)
|
||||
|
||||
# test with set value
|
||||
baker.make(
|
||||
"agents.AgentCustomField",
|
||||
field=field,
|
||||
agent=agent,
|
||||
string_value="CUSTOM VALUE",
|
||||
)
|
||||
self.assertEqual(
|
||||
["-Parameter", "-Another 'CUSTOM VALUE'"],
|
||||
Script.parse_script_args(agent=agent, shell="python", args=args),
|
||||
)
|
||||
|
||||
def test_script_arg_replacement_client_custom_fields(self):
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
field = baker.make(
|
||||
"core.CustomField",
|
||||
name="Test Field",
|
||||
model="client",
|
||||
type="text",
|
||||
default_value_string="DEFAULT",
|
||||
)
|
||||
|
||||
args = ["-Parameter", "-Another {{client.Test Field}}"]
|
||||
|
||||
# test default value
|
||||
self.assertEqual(
|
||||
["-Parameter", "-Another 'DEFAULT'"],
|
||||
Script.parse_script_args(agent=agent, shell="python", args=args),
|
||||
)
|
||||
|
||||
# test with set value
|
||||
baker.make(
|
||||
"clients.ClientCustomField",
|
||||
field=field,
|
||||
client=agent.client,
|
||||
string_value="CUSTOM VALUE",
|
||||
)
|
||||
self.assertEqual(
|
||||
["-Parameter", "-Another 'CUSTOM VALUE'"],
|
||||
Script.parse_script_args(agent=agent, shell="python", args=args),
|
||||
)
|
||||
|
||||
def test_script_arg_replacement_site_custom_fields(self):
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
field = baker.make(
|
||||
"core.CustomField",
|
||||
name="Test Field",
|
||||
model="site",
|
||||
type="text",
|
||||
default_value_string="DEFAULT",
|
||||
)
|
||||
|
||||
args = ["-Parameter", "-Another {{site.Test Field}}"]
|
||||
|
||||
# test default value
|
||||
self.assertEqual(
|
||||
["-Parameter", "-Another 'DEFAULT'"],
|
||||
Script.parse_script_args(agent=agent, shell="python", args=args),
|
||||
)
|
||||
|
||||
# test with set value
|
||||
value = baker.make(
|
||||
"clients.SiteCustomField",
|
||||
field=field,
|
||||
site=agent.site,
|
||||
string_value="CUSTOM VALUE",
|
||||
)
|
||||
self.assertEqual(
|
||||
["-Parameter", "-Another 'CUSTOM VALUE'"],
|
||||
Script.parse_script_args(agent=agent, shell="python", args=args),
|
||||
)
|
||||
|
||||
# test with set but empty field value
|
||||
value.string_value = "" # type: ignore
|
||||
value.save() # type: ignore
|
||||
|
||||
self.assertEqual(
|
||||
["-Parameter", "-Another 'DEFAULT'"],
|
||||
Script.parse_script_args(agent=agent, shell="python", args=args),
|
||||
)
|
||||
|
||||
# test blank default and value
|
||||
field.default_value_string = "" # type: ignore
|
||||
field.save() # type: ignore
|
||||
|
||||
self.assertEqual(
|
||||
["-Parameter", "-Another ''"],
|
||||
Script.parse_script_args(agent=agent, shell="python", args=args),
|
||||
)
|
||||
|
||||
def test_script_arg_replacement_array_fields(self):
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
field = baker.make(
|
||||
"core.CustomField",
|
||||
name="Test Field",
|
||||
model="agent",
|
||||
type="multiple",
|
||||
default_values_multiple=["this", "is", "an", "array"],
|
||||
)
|
||||
|
||||
args = ["-Parameter", "-Another {{agent.Test Field}}"]
|
||||
|
||||
# test default value
|
||||
self.assertEqual(
|
||||
["-Parameter", "-Another 'this,is,an,array'"],
|
||||
Script.parse_script_args(agent=agent, shell="python", args=args),
|
||||
)
|
||||
|
||||
# test with set value and python shell
|
||||
baker.make(
|
||||
"agents.AgentCustomField",
|
||||
field=field,
|
||||
agent=agent,
|
||||
multiple_value=["this", "is", "new"],
|
||||
)
|
||||
self.assertEqual(
|
||||
["-Parameter", "-Another 'this,is,new'"],
|
||||
Script.parse_script_args(agent=agent, shell="python", args=args),
|
||||
)
|
||||
|
||||
def test_script_arg_replacement_boolean_fields(self):
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
field = baker.make(
|
||||
"core.CustomField",
|
||||
name="Test Field",
|
||||
model="agent",
|
||||
type="checkbox",
|
||||
default_value_bool=True,
|
||||
)
|
||||
|
||||
args = ["-Parameter", "-Another {{agent.Test Field}}"]
|
||||
|
||||
# test default value with python
|
||||
self.assertEqual(
|
||||
["-Parameter", "-Another 1"],
|
||||
Script.parse_script_args(agent=agent, shell="python", args=args),
|
||||
)
|
||||
|
||||
# test with set value and python shell
|
||||
custom = baker.make(
|
||||
"agents.AgentCustomField",
|
||||
field=field,
|
||||
agent=agent,
|
||||
bool_value=False,
|
||||
)
|
||||
self.assertEqual(
|
||||
["-Parameter", "-Another 0"],
|
||||
Script.parse_script_args(agent=agent, shell="python", args=args),
|
||||
)
|
||||
|
||||
# test with set value and cmd shell
|
||||
self.assertEqual(
|
||||
["-Parameter", "-Another 0"],
|
||||
Script.parse_script_args(agent=agent, shell="cmd", args=args),
|
||||
)
|
||||
|
||||
# test with set value and powershell
|
||||
self.assertEqual(
|
||||
["-Parameter", "-Another $False"],
|
||||
Script.parse_script_args(agent=agent, shell="powershell", args=args),
|
||||
)
|
||||
|
||||
# test with True value powershell
|
||||
custom.bool_value = True # type: ignore
|
||||
custom.save() # type: ignore
|
||||
|
||||
self.assertEqual(
|
||||
["-Parameter", "-Another $True"],
|
||||
Script.parse_script_args(agent=agent, shell="powershell", args=args),
|
||||
)
|
||||
|
||||
@@ -4,20 +4,23 @@ import json
|
||||
from django.conf import settings
|
||||
from django.shortcuts import get_object_or_404
|
||||
from loguru import logger
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.decorators import api_view, permission_classes
|
||||
from rest_framework.parsers import FileUploadParser
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from tacticalrmm.utils import notify_error
|
||||
|
||||
from .models import Script
|
||||
from .permissions import ManageScriptsPerms
|
||||
from .serializers import ScriptSerializer, ScriptTableSerializer
|
||||
|
||||
logger.configure(**settings.LOG_CONFIG)
|
||||
|
||||
|
||||
class GetAddScripts(APIView):
|
||||
permission_classes = [IsAuthenticated, ManageScriptsPerms]
|
||||
parser_class = (FileUploadParser,)
|
||||
|
||||
def get(self, request):
|
||||
@@ -63,6 +66,8 @@ class GetAddScripts(APIView):
|
||||
|
||||
|
||||
class GetUpdateDeleteScript(APIView):
|
||||
permission_classes = [IsAuthenticated, ManageScriptsPerms]
|
||||
|
||||
def get(self, request, pk):
|
||||
script = get_object_or_404(Script, pk=pk)
|
||||
return Response(ScriptSerializer(script).data)
|
||||
@@ -103,6 +108,7 @@ class GetUpdateDeleteScript(APIView):
|
||||
|
||||
|
||||
@api_view()
|
||||
@permission_classes([IsAuthenticated, ManageScriptsPerms])
|
||||
def download(request, pk):
|
||||
script = get_object_or_404(Script, pk=pk)
|
||||
|
||||
|
||||
8
api/tacticalrmm/services/permissions.py
Normal file
8
api/tacticalrmm/services/permissions.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from rest_framework import permissions
|
||||
|
||||
from tacticalrmm.permissions import _has_perm
|
||||
|
||||
|
||||
class ManageWinSvcsPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
return _has_perm(r, "can_manage_winsvcs")
|
||||
@@ -3,13 +3,15 @@ import asyncio
|
||||
from django.conf import settings
|
||||
from django.shortcuts import get_object_or_404
|
||||
from loguru import logger
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.decorators import api_view, permission_classes
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
|
||||
from agents.models import Agent
|
||||
from checks.models import Check
|
||||
from tacticalrmm.utils import notify_error
|
||||
|
||||
from .permissions import ManageWinSvcsPerms
|
||||
from .serializers import ServicesSerializer
|
||||
|
||||
logger.configure(**settings.LOG_CONFIG)
|
||||
@@ -34,6 +36,7 @@ def default_services(request):
|
||||
|
||||
|
||||
@api_view(["POST"])
|
||||
@permission_classes([IsAuthenticated, ManageWinSvcsPerms])
|
||||
def service_action(request):
|
||||
agent = get_object_or_404(Agent, pk=request.data["pk"])
|
||||
action = request.data["sv_action"]
|
||||
@@ -85,6 +88,7 @@ def service_detail(request, pk, svcname):
|
||||
|
||||
|
||||
@api_view(["POST"])
|
||||
@permission_classes([IsAuthenticated, ManageWinSvcsPerms])
|
||||
def edit_service(request):
|
||||
agent = get_object_or_404(Agent, pk=request.data["pk"])
|
||||
data = {
|
||||
|
||||
11
api/tacticalrmm/software/permissions.py
Normal file
11
api/tacticalrmm/software/permissions.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from rest_framework import permissions
|
||||
|
||||
from tacticalrmm.permissions import _has_perm
|
||||
|
||||
|
||||
class ManageSoftwarePerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
if r.method == "GET":
|
||||
return True
|
||||
|
||||
return _has_perm(r, "can_manage_software")
|
||||
@@ -3,7 +3,8 @@ from typing import Any
|
||||
|
||||
from django.shortcuts import get_object_or_404
|
||||
from packaging import version as pyver
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.decorators import api_view, permission_classes
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
|
||||
from agents.models import Agent
|
||||
@@ -11,6 +12,7 @@ from logs.models import PendingAction
|
||||
from tacticalrmm.utils import filter_software, notify_error
|
||||
|
||||
from .models import ChocoSoftware, InstalledSoftware
|
||||
from .permissions import ManageSoftwarePerms
|
||||
from .serializers import InstalledSoftwareSerializer
|
||||
|
||||
|
||||
@@ -20,6 +22,7 @@ def chocos(request):
|
||||
|
||||
|
||||
@api_view(["POST"])
|
||||
@permission_classes([IsAuthenticated, ManageSoftwarePerms])
|
||||
def install(request):
|
||||
agent = get_object_or_404(Agent, pk=request.data["pk"])
|
||||
if pyver.parse(agent.version) < pyver.parse("1.4.8"):
|
||||
|
||||
@@ -41,7 +41,7 @@ app.conf.beat_schedule = {
|
||||
},
|
||||
"get-wmi": {
|
||||
"task": "agents.tasks.get_wmi_task",
|
||||
"schedule": crontab(minute="*/18"),
|
||||
"schedule": crontab(minute=18, hour="*/5"),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -54,10 +54,12 @@ def debug_task(self):
|
||||
@app.on_after_finalize.connect
|
||||
def setup_periodic_tasks(sender, **kwargs):
|
||||
|
||||
from agents.tasks import agent_outages_task
|
||||
from agents.tasks import agent_outages_task, agent_checkin_task
|
||||
from alerts.tasks import unsnooze_alerts
|
||||
from core.tasks import core_maintenance_tasks
|
||||
from core.tasks import core_maintenance_tasks, cache_db_fields_task
|
||||
|
||||
sender.add_periodic_task(45.0, agent_checkin_task.s())
|
||||
sender.add_periodic_task(60.0, agent_outages_task.s())
|
||||
sender.add_periodic_task(60.0 * 30, core_maintenance_tasks.s())
|
||||
sender.add_periodic_task(60.0 * 60, unsnooze_alerts.s())
|
||||
sender.add_periodic_task(90.0, cache_db_fields_task.s())
|
||||
|
||||
7
api/tacticalrmm/tacticalrmm/permissions.py
Normal file
7
api/tacticalrmm/tacticalrmm/permissions.py
Normal file
@@ -0,0 +1,7 @@
|
||||
def _has_perm(request, perm):
|
||||
if request.user.is_superuser or (
|
||||
request.user.role and getattr(request.user.role, "is_superuser")
|
||||
):
|
||||
return True
|
||||
|
||||
return request.user.role and getattr(request.user.role, perm)
|
||||
@@ -15,20 +15,23 @@ EXE_DIR = os.path.join(BASE_DIR, "tacticalrmm/private/exe")
|
||||
AUTH_USER_MODEL = "accounts.User"
|
||||
|
||||
# latest release
|
||||
TRMM_VERSION = "0.6.5"
|
||||
TRMM_VERSION = "0.7.2"
|
||||
|
||||
# bump this version everytime vue code is changed
|
||||
# to alert user they need to manually refresh their browser
|
||||
APP_VER = "0.0.132"
|
||||
APP_VER = "0.0.141"
|
||||
|
||||
# https://github.com/wh1te909/rmmagent
|
||||
LATEST_AGENT_VER = "1.5.2"
|
||||
LATEST_AGENT_VER = "1.5.9"
|
||||
|
||||
MESH_VER = "0.8.19"
|
||||
MESH_VER = "0.8.60"
|
||||
|
||||
# for the update script, bump when need to recreate venv or npm install
|
||||
PIP_VER = "15"
|
||||
NPM_VER = "14"
|
||||
PIP_VER = "19"
|
||||
NPM_VER = "19"
|
||||
|
||||
SETUPTOOLS_VER = "57.0.0"
|
||||
WHEEL_VER = "0.36.2"
|
||||
|
||||
DL_64 = f"https://github.com/wh1te909/rmmagent/releases/download/v{LATEST_AGENT_VER}/winagent-v{LATEST_AGENT_VER}.exe"
|
||||
DL_32 = f"https://github.com/wh1te909/rmmagent/releases/download/v{LATEST_AGENT_VER}/winagent-v{LATEST_AGENT_VER}-x86.exe"
|
||||
@@ -42,6 +45,12 @@ DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
|
||||
|
||||
ASGI_APPLICATION = "tacticalrmm.asgi.application"
|
||||
|
||||
REST_KNOX = {
|
||||
"TOKEN_TTL": timedelta(hours=5),
|
||||
"AUTO_REFRESH": True,
|
||||
"MIN_REFRESH_INTERVAL": 600,
|
||||
}
|
||||
|
||||
try:
|
||||
from .local_settings import *
|
||||
except ImportError:
|
||||
@@ -77,6 +86,15 @@ if not "AZPIPELINE" in os.environ:
|
||||
if DEBUG: # type: ignore
|
||||
INSTALLED_APPS += ("django_extensions",)
|
||||
|
||||
CHANNEL_LAYERS = {
|
||||
"default": {
|
||||
"BACKEND": "channels_redis.core.RedisChannelLayer",
|
||||
"CONFIG": {
|
||||
"hosts": [(REDIS_HOST, 6379)], # type: ignore
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if "AZPIPELINE" in os.environ:
|
||||
ADMIN_ENABLED = False
|
||||
|
||||
@@ -101,11 +119,6 @@ MIDDLEWARE = [
|
||||
if ADMIN_ENABLED: # type: ignore
|
||||
MIDDLEWARE += ("django.contrib.messages.middleware.MessageMiddleware",)
|
||||
|
||||
REST_KNOX = {
|
||||
"TOKEN_TTL": timedelta(hours=5),
|
||||
"AUTO_REFRESH": True,
|
||||
"MIN_REFRESH_INTERVAL": 600,
|
||||
}
|
||||
|
||||
ROOT_URLCONF = "tacticalrmm.urls"
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import uuid
|
||||
from django.test import TestCase, override_settings
|
||||
from model_bakery import baker
|
||||
from rest_framework.authtoken.models import Token
|
||||
@@ -10,17 +11,26 @@ from core.models import CoreSettings
|
||||
class TacticalTestCase(TestCase):
|
||||
def authenticate(self):
|
||||
self.john = User(username="john")
|
||||
self.john.is_superuser = True
|
||||
self.john.set_password("hunter2")
|
||||
self.john.save()
|
||||
self.alice = User(username="alice")
|
||||
self.alice.is_superuser = True
|
||||
self.alice.set_password("hunter2")
|
||||
self.alice.save()
|
||||
self.client_setup()
|
||||
self.client.force_authenticate(user=self.john)
|
||||
|
||||
User.objects.create_user( # type: ignore
|
||||
username=uuid.uuid4().hex,
|
||||
is_installer_user=True,
|
||||
password=User.objects.make_random_password(60), # type: ignore
|
||||
)
|
||||
|
||||
def setup_agent_auth(self, agent):
|
||||
agent_user = User.objects.create_user(
|
||||
username=agent.agent_id, password=User.objects.make_random_password(60)
|
||||
username=agent.agent_id,
|
||||
password=User.objects.make_random_password(60),
|
||||
)
|
||||
Token.objects.create(user=agent_user)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ from django.urls import include, path
|
||||
from knox import views as knox_views
|
||||
|
||||
from accounts.views import CheckCreds, LoginView
|
||||
from core import consumers
|
||||
from core.consumers import DashInfo
|
||||
|
||||
urlpatterns = [
|
||||
path("checkcreds/", CheckCreds.as_view()),
|
||||
@@ -32,5 +32,5 @@ if hasattr(settings, "ADMIN_ENABLED") and settings.ADMIN_ENABLED:
|
||||
urlpatterns += (path(settings.ADMIN_URL, admin.site.urls),)
|
||||
|
||||
ws_urlpatterns = [
|
||||
path("ws/dashinfo/", consumers.DashInfo.as_asgi()), # type: ignore
|
||||
path("ws/dashinfo/", DashInfo.as_asgi()), # type: ignore
|
||||
]
|
||||
|
||||
@@ -5,7 +5,7 @@ import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
import urllib.parse
|
||||
from typing import Union
|
||||
from typing import Optional, Union
|
||||
|
||||
import pytz
|
||||
import requests
|
||||
@@ -263,3 +263,126 @@ def run_nats_api_cmd(mode: str, ids: list[str], timeout: int = 30) -> None:
|
||||
subprocess.run(cmd, capture_output=True, timeout=timeout)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
|
||||
|
||||
def get_latest_trmm_ver() -> str:
|
||||
url = "https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/api/tacticalrmm/tacticalrmm/settings.py"
|
||||
try:
|
||||
r = requests.get(url, timeout=5)
|
||||
except:
|
||||
return "error"
|
||||
|
||||
try:
|
||||
for line in r.text.splitlines():
|
||||
if "TRMM_VERSION" in line:
|
||||
return line.split(" ")[2].strip('"')
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
|
||||
return "error"
|
||||
|
||||
|
||||
def replace_db_values(
|
||||
string: str, agent: Agent = None, shell: str = None, quotes=True
|
||||
) -> Union[str, None]:
|
||||
from core.models import CustomField, GlobalKVStore
|
||||
|
||||
# split by period if exists. First should be model and second should be property i.e {{client.name}}
|
||||
temp = string.split(".")
|
||||
|
||||
# check for model and property
|
||||
if len(temp) < 2:
|
||||
# ignore arg since it is invalid
|
||||
return None
|
||||
|
||||
# value is in the global keystore and replace value
|
||||
if temp[0] == "global":
|
||||
if GlobalKVStore.objects.filter(name=temp[1]).exists():
|
||||
value = GlobalKVStore.objects.get(name=temp[1]).value
|
||||
|
||||
return f"'{value}'" if quotes else value
|
||||
else:
|
||||
logger.error(
|
||||
f"Couldn't lookup value for: {string}. Make sure it exists in CoreSettings > Key Store"
|
||||
)
|
||||
return None
|
||||
|
||||
if not agent:
|
||||
# agent must be set if not global property
|
||||
return f"There was an error finding the agent: {agent}"
|
||||
|
||||
if temp[0] == "client":
|
||||
model = "client"
|
||||
obj = agent.client
|
||||
elif temp[0] == "site":
|
||||
model = "site"
|
||||
obj = agent.site
|
||||
elif temp[0] == "agent":
|
||||
model = "agent"
|
||||
obj = agent
|
||||
else:
|
||||
# ignore arg since it is invalid
|
||||
logger.error(
|
||||
f"Not enough information to find value for: {string}. Only agent, site, client, and global are supported."
|
||||
)
|
||||
return None
|
||||
|
||||
if hasattr(obj, temp[1]):
|
||||
value = f"'{getattr(obj, temp[1])}'" if quotes else getattr(obj, temp[1])
|
||||
|
||||
elif CustomField.objects.filter(model=model, name=temp[1]).exists():
|
||||
|
||||
field = CustomField.objects.get(model=model, name=temp[1])
|
||||
model_fields = getattr(field, f"{model}_fields")
|
||||
value = None
|
||||
if model_fields.filter(**{model: obj}).exists():
|
||||
if field.type != "checkbox" and model_fields.get(**{model: obj}).value:
|
||||
value = model_fields.get(**{model: obj}).value
|
||||
elif field.type == "checkbox":
|
||||
value = model_fields.get(**{model: obj}).value
|
||||
|
||||
# need explicit None check since a false boolean value will pass default value
|
||||
if value == None and field.default_value != None:
|
||||
value = field.default_value
|
||||
|
||||
# check if value exists and if not use default
|
||||
if value and field.type == "multiple":
|
||||
value = (
|
||||
f"'{format_shell_array(value)}'"
|
||||
if quotes
|
||||
else format_shell_array(value)
|
||||
)
|
||||
elif value != None and field.type == "checkbox":
|
||||
value = format_shell_bool(value, shell)
|
||||
else:
|
||||
value = f"'{value}'" if quotes else value
|
||||
|
||||
else:
|
||||
# ignore arg since property is invalid
|
||||
logger.error(
|
||||
f"Couldn't find property on supplied variable: {string}. Make sure it exists as a custom field or a valid agent property"
|
||||
)
|
||||
return None
|
||||
|
||||
# log any unhashable type errors
|
||||
if value != None:
|
||||
return value # type: ignore
|
||||
else:
|
||||
logger.error(
|
||||
f"Couldn't lookup value for: {string}. Make sure it exists as a custom field or a valid agent property"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def format_shell_array(value: list) -> str:
|
||||
temp_string = ""
|
||||
for item in value:
|
||||
temp_string += item + ","
|
||||
return f"{temp_string.strip(',')}"
|
||||
|
||||
|
||||
def format_shell_bool(value: bool, shell: Optional[str]) -> str:
|
||||
if shell == "powershell":
|
||||
return "$True" if value else "$False"
|
||||
else:
|
||||
return "1" if value else "0"
|
||||
|
||||
8
api/tacticalrmm/winupdate/permissions.py
Normal file
8
api/tacticalrmm/winupdate/permissions.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from rest_framework import permissions
|
||||
|
||||
from tacticalrmm.permissions import _has_perm
|
||||
|
||||
|
||||
class ManageWinUpdatePerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
return _has_perm(r, "can_manage_winupdates")
|
||||
@@ -46,7 +46,13 @@ def auto_approve_updates_task():
|
||||
def check_agent_update_schedule_task():
|
||||
# scheduled task that installs updates on agents if enabled
|
||||
agents = Agent.objects.only(
|
||||
"pk", "agent_id", "version", "last_seen", "overdue_time", "offline_time"
|
||||
"pk",
|
||||
"agent_id",
|
||||
"version",
|
||||
"last_seen",
|
||||
"overdue_time",
|
||||
"offline_time",
|
||||
"has_patches_pending",
|
||||
)
|
||||
online = [
|
||||
i
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import asyncio
|
||||
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.decorators import api_view, permission_classes
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
|
||||
from agents.models import Agent
|
||||
from tacticalrmm.utils import get_default_timezone
|
||||
|
||||
from .models import WinUpdate
|
||||
from .permissions import ManageWinUpdatePerms
|
||||
from .serializers import UpdateSerializer
|
||||
|
||||
|
||||
@@ -20,6 +22,7 @@ def get_win_updates(request, pk):
|
||||
|
||||
|
||||
@api_view()
|
||||
@permission_classes([IsAuthenticated, ManageWinUpdatePerms])
|
||||
def run_update_scan(request, pk):
|
||||
agent = get_object_or_404(Agent, pk=pk)
|
||||
agent.delete_superseded_updates()
|
||||
@@ -28,6 +31,7 @@ def run_update_scan(request, pk):
|
||||
|
||||
|
||||
@api_view()
|
||||
@permission_classes([IsAuthenticated, ManageWinUpdatePerms])
|
||||
def install_updates(request, pk):
|
||||
agent = get_object_or_404(Agent, pk=pk)
|
||||
agent.delete_superseded_updates()
|
||||
@@ -41,6 +45,7 @@ def install_updates(request, pk):
|
||||
|
||||
|
||||
@api_view(["PATCH"])
|
||||
@permission_classes([IsAuthenticated, ManageWinUpdatePerms])
|
||||
def edit_policy(request):
|
||||
patch = get_object_or_404(WinUpdate, pk=request.data["pk"])
|
||||
patch.action = request.data["policy"]
|
||||
|
||||
@@ -20,15 +20,17 @@ jobs:
|
||||
sudo -u postgres psql -c 'DROP DATABASE IF EXISTS pipeline'
|
||||
sudo -u postgres psql -c 'DROP DATABASE IF EXISTS test_pipeline'
|
||||
sudo -u postgres psql -c 'CREATE DATABASE pipeline'
|
||||
|
||||
SETTINGS_FILE="/myagent/_work/1/s/api/tacticalrmm/tacticalrmm/settings.py"
|
||||
rm -rf /myagent/_work/1/s/api/env
|
||||
cd /myagent/_work/1/s/api
|
||||
python3.9 -m venv env
|
||||
source env/bin/activate
|
||||
cd /myagent/_work/1/s/api/tacticalrmm
|
||||
pip install --no-cache-dir --upgrade pip
|
||||
pip install --no-cache-dir setuptools==54.2.0 wheel==0.36.2
|
||||
pip install --no-cache-dir -r requirements.txt -r requirements-test.txt -r requirements-dev.txt
|
||||
pip install --trusted-host pypi.org --trusted-host pypi.python.org --trusted-host files.pythonhosted.org --upgrade pip
|
||||
SETUPTOOLS_VER=$(grep "^SETUPTOOLS_VER" "$SETTINGS_FILE" | awk -F'[= "]' '{print $5}')
|
||||
WHEEL_VER=$(grep "^WHEEL_VER" "$SETTINGS_FILE" | awk -F'[= "]' '{print $5}')
|
||||
pip install --trusted-host pypi.org --trusted-host pypi.python.org --trusted-host files.pythonhosted.org setuptools==${SETUPTOOLS_VER} wheel==${WHEEL_VER}
|
||||
pip install --trusted-host pypi.org --trusted-host pypi.python.org --trusted-host files.pythonhosted.org -r requirements.txt -r requirements-test.txt -r requirements-dev.txt
|
||||
displayName: "Install Python Dependencies"
|
||||
|
||||
- script: |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
|
||||
SCRIPT_VERSION="12"
|
||||
SCRIPT_VERSION="14"
|
||||
SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/backup.sh'
|
||||
|
||||
GREEN='\033[0;32m'
|
||||
@@ -59,6 +59,7 @@ mkdir ${tmp_dir}/nginx
|
||||
mkdir ${tmp_dir}/systemd
|
||||
mkdir ${tmp_dir}/rmm
|
||||
mkdir ${tmp_dir}/confd
|
||||
mkdir ${tmp_dir}/redis
|
||||
|
||||
|
||||
pg_dump --dbname=postgresql://"${POSTGRES_USER}":"${POSTGRES_PW}"@127.0.0.1:5432/tacticalrmm | gzip -9 > ${tmp_dir}/postgres/db-${dt_now}.psql.gz
|
||||
@@ -72,6 +73,8 @@ sudo tar -czvf ${tmp_dir}/nginx/etc-nginx.tar.gz -C /etc/nginx .
|
||||
|
||||
sudo tar -czvf ${tmp_dir}/confd/etc-confd.tar.gz -C /etc/conf.d .
|
||||
|
||||
sudo gzip -9 -c /var/lib/redis/appendonly.aof > ${tmp_dir}/redis/appendonly.aof.gz
|
||||
|
||||
sudo cp ${sysd}/rmm.service ${sysd}/celery.service ${sysd}/celerybeat.service ${sysd}/meshcentral.service ${sysd}/nats.service ${tmp_dir}/systemd/
|
||||
if [ -f "${sysd}/daphne.service" ]; then
|
||||
sudo cp ${sysd}/daphne.service ${tmp_dir}/systemd/
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM nats:2.2-alpine
|
||||
FROM nats:2.2.6-alpine
|
||||
|
||||
ENV TACTICAL_DIR /opt/tactical
|
||||
ENV TACTICAL_READY_FILE ${TACTICAL_DIR}/tmp/tactical.ready
|
||||
|
||||
@@ -124,6 +124,7 @@ EOF
|
||||
python manage.py load_chocos
|
||||
python manage.py load_community_scripts
|
||||
python manage.py reload_nats
|
||||
python manage.py create_installer_user
|
||||
|
||||
# create super user
|
||||
echo "from accounts.models import User; User.objects.create_superuser('${TRMM_USER}', 'admin@example.com', '${TRMM_PASS}') if not User.objects.filter(username='${TRMM_USER}').exists() else 0;" | python manage.py shell
|
||||
|
||||
@@ -18,6 +18,7 @@ volumes:
|
||||
postgres_data:
|
||||
mongo_data:
|
||||
mesh_data:
|
||||
redis_data:
|
||||
|
||||
services:
|
||||
# postgres database for api service
|
||||
@@ -38,7 +39,10 @@ services:
|
||||
tactical-redis:
|
||||
container_name: trmm-redis
|
||||
image: redis:6.0-alpine
|
||||
command: redis-server --appendonly yes
|
||||
restart: always
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
networks:
|
||||
- redis
|
||||
|
||||
|
||||
103
docs/docs/3rdparty_screenconnect.md
Normal file
103
docs/docs/3rdparty_screenconnect.md
Normal file
@@ -0,0 +1,103 @@
|
||||
# Screenconnect / Connectwise Control
|
||||
|
||||
## Connectwise Control Integration
|
||||
|
||||
!!!info
|
||||
To make this work you will need the name of a the Service from one of your agents running a Screen Connect Guest.
|
||||
|
||||
!!!info
|
||||
You can setup a full automation policy to collect the machine GUID but this example will collect from just one agent for testing purposes.
|
||||
|
||||
From the UI go to **Settings > Global Settings > CUSTOM FIELDS > Agents**
|
||||
|
||||
Add Custom Field</br>
|
||||
**Target** = `Client`</br>
|
||||
**Name** = `ScreenConnectService`</br>
|
||||
**Field Type** = `Text` </br>
|
||||
**Default Value** = `The name of your SC Service eg. ScreenConnect Client (XXXXXXXXXXXXXXXXX)`</br>
|
||||
|
||||

|
||||
|
||||
Add Custom Field</br>
|
||||
**Target** = `Agent`</br>
|
||||
**Name** = `ScreenConnectGUID`</br>
|
||||
**Field Type** = `Text`</br>
|
||||
|
||||

|
||||
|
||||
While in Global Settings go to **URL ACTIONS**
|
||||
|
||||
Add a URL Action</br>
|
||||
**Name** = `ScreenConnect`</br>
|
||||
**Description** = `Launch Screen Connect Session`</br>
|
||||
**URL Pattern** =
|
||||
|
||||
```html
|
||||
https://<your_screenconnect_fqdn_with_port>/Host#Access/All%20Machines//{{agent.ScreenConnectGUID}}/Join
|
||||
```
|
||||
|
||||

|
||||
|
||||
Navigate to an agent with ConnectWise Service running (or apply using **Settings > Automation Manager**).</br>
|
||||
Go to Tasks.</br>
|
||||
Add Task</br>
|
||||
**Select Script** = `ScreenConnect - Get GUID for client` (this is a builtin script from script library)</br>
|
||||
**Script argument** = `-serviceName{{client.ScreenConnectService}}`</br>
|
||||
**Descriptive name of task** = `Collects the Machine GUID for ScreenConnect.`</br>
|
||||
**Collector Task** = `CHECKED`</br>
|
||||
**Custom Field to update** = `ScreenConectGUID`</br>
|
||||
|
||||

|
||||
|
||||
Click **Next**</br>
|
||||
Check **Manual**</br>
|
||||
Click **Add Task**
|
||||
|
||||
Right click on the newly created task and click **Run Task Now**.
|
||||
|
||||
Give it a second to execute then right click the agent that you are working with and go to **Run URL Action > ScreenConnect**
|
||||
|
||||
It should ask you to sign into your Connectwise Control server if you are not already logged in and launch the session.
|
||||
|
||||
*****
|
||||
|
||||
## Install Tactical RMM via Screeconnect commands window
|
||||
|
||||
1. Create a Deplopment under **Agents > Manage Deployments**
|
||||
2. Replace `<deployment URL>` below with your Deployment Download Link.
|
||||
|
||||
**x64**
|
||||
|
||||
```cmd
|
||||
#!ps
|
||||
#maxlength=500000
|
||||
#timeout=600000
|
||||
|
||||
Invoke-WebRequest "<deployment URL>" -OutFile ( New-Item -Path "C:\temp\trmminstallx64.exe" -Force )
|
||||
$proc = Start-Process c:\temp\trmminstallx64.exe -ArgumentList '-silent' -PassThru
|
||||
Wait-Process -InputObject $proc
|
||||
|
||||
if ($proc.ExitCode -ne 0) {
|
||||
Write-Warning "$_ exited with status code $($proc.ExitCode)"
|
||||
}
|
||||
Remove-Item -Path "c:\temp\trmminstallx64.exe" -Force
|
||||
```
|
||||
|
||||
**x86**
|
||||
|
||||
```cmd
|
||||
#!ps
|
||||
#maxlength=500000
|
||||
#timeout=600000
|
||||
|
||||
Invoke-WebRequest "<deployment URL>" -OutFile ( New-Item -Path "C:\temp\trmminstallx86.exe" -Force )
|
||||
$proc = Start-Process c:\temp\trmminstallx86.exe -ArgumentList '-silent' -PassThru
|
||||
Wait-Process -InputObject $proc
|
||||
|
||||
if ($proc.ExitCode -ne 0) {
|
||||
Write-Warning "$_ exited with status code $($proc.ExitCode)"
|
||||
}
|
||||
Remove-Item -Path "c:\temp\trmminstallx86.exe" -Force
|
||||
```
|
||||
|
||||
*****
|
||||
@@ -1,69 +0,0 @@
|
||||
# Alerting Overview
|
||||
|
||||
## Notification Types
|
||||
|
||||
* *Email Alerts* - Sends email
|
||||
* *SMS Alerts* - Sends text message
|
||||
* *Dashboard Alerts* - Adds a notification in the dashboard alert icon
|
||||
|
||||
|
||||
## Alert Severities
|
||||
|
||||
* Informational
|
||||
* Warning
|
||||
* Error
|
||||
|
||||
#### Agents
|
||||
Agent offline alerts always have an error severity.
|
||||
|
||||
#### Checks
|
||||
Checks can be configured to create alerts with different severities
|
||||
|
||||
* Memory and Cpuload checks can be configured with a warning and error threshold. To disable one of them put in a 0.
|
||||
* Script checks allow for information and warning return codes. Everything else, besides a 0 will result in an error severity.
|
||||
* Event Log, service, and ping checks require you to set the severity to information, warning, or error.
|
||||
|
||||
#### Automated Tasks
|
||||
For automated tasks, you set the what the alert severity should be directly on the task.
|
||||
|
||||
|
||||
## Configure Alert Templates
|
||||
Alert template allow you to setup alerting and notifications on many agents at once. Alert templates can be applied to Sites, Client, Automation Policies, and in the Global Settings.
|
||||
|
||||
To create an alert template, go to Settings > Alerts Manager. Then click New
|
||||
|
||||
In the form, give the alert template a name and make sure it is enabled.
|
||||
|
||||
Optionally setup any of the below settings:
|
||||
* *Failure Action* - Runs the selected script once on any agent. This is useful for running one-time tasks like sending an http request to an external system to create a ticket.
|
||||
* *Failure action args* - Optionally pass in arguments to the failure script.
|
||||
* *Failure action timeout* - Sets the timeout for the script.
|
||||
* *Resolved action* - Runs the selected script once on any agent if the alert is resolved. This is useful for running onetime tasks like sending an http request to an external system to close the ticket that was created.
|
||||
* *Resolved action args* - Optionally pass in arguments to the resolved script.
|
||||
* *Resolved action timeout* - Sets the timeout for the script.
|
||||
* *Email Recipients* - Overrides the default email recipients in Global Settings.
|
||||
* *From Email* - Overrides the From email address in Global Settings.
|
||||
* *SMS Recipients* - Overrides the SMS recipients in Global Settings.
|
||||
* *Include desktops* - Will apply to desktops
|
||||
#### agent/check/task settings
|
||||
* *Email on resolved* - Sends a email when the alert clears
|
||||
* *Text on resolved* - Sends a text when the alert clears
|
||||
* *Always email* - This enables the email notification setting on the agent/check/task
|
||||
* *Always sms* - This enables the text notification setting on the agent/check/task
|
||||
* *Always dashboard alert* - This enables the dashboard alert notification setting on the agent/check/task
|
||||
* *Periodic notification* - This sets up a periodic notification on for the agent/check/task alert
|
||||
* *Alert on severity* - When configured, will only send a notification through the corresponding channel if the alert is of the specified severity
|
||||
|
||||
## Applying Alert Templates
|
||||
|
||||
Alerts are applied in the following over. The agent picks the closest matching alert template.
|
||||
|
||||
* Right-click on any Client or Site and go to Assign Alert Template
|
||||
* In Automation Manager, click on Assign Alert Template for the policy you want to apply it to
|
||||
* In Global Settings, select the default alert template
|
||||
|
||||
1. Policy w/ Alert Template applied to Site
|
||||
2. Site
|
||||
3. Policy w/ Alert Template applied to Client
|
||||
4. Client
|
||||
5. Default Alert Template
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user