Compare commits
318 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2eefedadb3 | ||
|
|
e63d7a0b8a | ||
|
|
2a1b1849fa | ||
|
|
0461cb7f19 | ||
|
|
0932e0be03 | ||
|
|
4638ac9474 | ||
|
|
d8d7255029 | ||
|
|
fa05276c3f | ||
|
|
e50a5d51d8 | ||
|
|
c03ba78587 | ||
|
|
ff07c69e7d | ||
|
|
735b84b26d | ||
|
|
8dd069ad67 | ||
|
|
1857e68003 | ||
|
|
ff2508382a | ||
|
|
9cb952b116 | ||
|
|
105e8089bb | ||
|
|
730f37f247 | ||
|
|
284716751f | ||
|
|
8d0db699bf | ||
|
|
53cf1cae58 | ||
|
|
307e4719e0 | ||
|
|
5effae787a | ||
|
|
6532be0b52 | ||
|
|
fb225a5347 | ||
|
|
b83830a45e | ||
|
|
ca28288c33 | ||
|
|
b6f8d9cb25 | ||
|
|
9cad0f11e5 | ||
|
|
807be08566 | ||
|
|
67f6a985f8 | ||
|
|
f87d54ae8d | ||
|
|
d894bf7271 | ||
|
|
56e0e5cace | ||
|
|
685084e784 | ||
|
|
cbeec5a973 | ||
|
|
3fff56bcd7 | ||
|
|
c504c23eec | ||
|
|
16dae5a655 | ||
|
|
e512c5ae7d | ||
|
|
094078b928 | ||
|
|
34fc3ff919 | ||
|
|
4391f48e78 | ||
|
|
775608a3c0 | ||
|
|
b326228901 | ||
|
|
b2e98173a8 | ||
|
|
65c9b7952c | ||
|
|
b9dc9e7d62 | ||
|
|
ce178d0354 | ||
|
|
a3ff6efebc | ||
|
|
6a9bc56723 | ||
|
|
c9ac158d25 | ||
|
|
4b937a0fe8 | ||
|
|
405bf26ac5 | ||
|
|
5dcda0e0a0 | ||
|
|
83e9b60308 | ||
|
|
10b40b4730 | ||
|
|
79d6d804ef | ||
|
|
e9c7b6d8f8 | ||
|
|
4fcfbfb3f4 | ||
|
|
30cde14ed3 | ||
|
|
cf76e6f538 | ||
|
|
d0f600ec8d | ||
|
|
675f9e956f | ||
|
|
381605a6bb | ||
|
|
0fce66062b | ||
|
|
747cc9e5da | ||
|
|
25a1b464da | ||
|
|
3b6738b547 | ||
|
|
fc93e3e97f | ||
|
|
0edbb13d48 | ||
|
|
673687341c | ||
|
|
3969208942 | ||
|
|
3fa89b58df | ||
|
|
a43a9c8543 | ||
|
|
45deda4dea | ||
|
|
6ec46f02a9 | ||
|
|
d643c17ff1 | ||
|
|
e5de89c6b4 | ||
|
|
c21e7c632d | ||
|
|
6ae771682a | ||
|
|
bf2075b902 | ||
|
|
62ec8c8f76 | ||
|
|
b84d4a99b8 | ||
|
|
cce9dfe585 | ||
|
|
166be395b9 | ||
|
|
fa3f5f8d68 | ||
|
|
2926b68c32 | ||
|
|
a55f187958 | ||
|
|
c76d263375 | ||
|
|
6740d97f8f | ||
|
|
b079eebe79 | ||
|
|
363e48a1e8 | ||
|
|
f60e4e3e4f | ||
|
|
1b02974efa | ||
|
|
496abdd230 | ||
|
|
bc495d77d1 | ||
|
|
fb54d4bb64 | ||
|
|
0786163dc3 | ||
|
|
ed85611e75 | ||
|
|
86ebfce44a | ||
|
|
dae51cff51 | ||
|
|
358a2e7220 | ||
|
|
d45353e8c8 | ||
|
|
2f56e4e3a1 | ||
|
|
0e503f8273 | ||
|
|
876fe803f5 | ||
|
|
6adb9678b6 | ||
|
|
39bf7ba4a9 | ||
|
|
5da6e2ff99 | ||
|
|
44603c41a2 | ||
|
|
0feb982a73 | ||
|
|
d93cb32f2e | ||
|
|
40c47eace2 | ||
|
|
509bdd879c | ||
|
|
b98ebb6e9f | ||
|
|
924ddecff0 | ||
|
|
ca64fd218d | ||
|
|
9b12b55acd | ||
|
|
450239564a | ||
|
|
bb1cc62d2a | ||
|
|
b4875c1e2d | ||
|
|
a21440d663 | ||
|
|
eb6836b63c | ||
|
|
b39a2690c1 | ||
|
|
706902da1c | ||
|
|
d5104b5d27 | ||
|
|
a13ae5c4b1 | ||
|
|
a92d1d9958 | ||
|
|
10852a9427 | ||
|
|
b757ce1e38 | ||
|
|
91e75f3fa2 | ||
|
|
6c8e55eb2f | ||
|
|
f821f700fa | ||
|
|
d76d24408f | ||
|
|
7ad85dfe1c | ||
|
|
7d8be0a719 | ||
|
|
bac15c18e4 | ||
|
|
2f266d39e6 | ||
|
|
5726d1fc52 | ||
|
|
69aee1823e | ||
|
|
e6a0ae5f57 | ||
|
|
e5df566c7a | ||
|
|
81e173b609 | ||
|
|
d0ebcc6606 | ||
|
|
99c3fcf42a | ||
|
|
794666e7cc | ||
|
|
45abe4955d | ||
|
|
7eed421c70 | ||
|
|
69f7c397c2 | ||
|
|
d2d136e922 | ||
|
|
396e435ae0 | ||
|
|
45d8e9102a | ||
|
|
12a51deffa | ||
|
|
f2f69abec2 | ||
|
|
02b7f962e9 | ||
|
|
eb813e6b22 | ||
|
|
5ddc604341 | ||
|
|
313e672e93 | ||
|
|
ce77ad6de4 | ||
|
|
bea22690b1 | ||
|
|
c9a52bd7d0 | ||
|
|
a244a341ec | ||
|
|
2b47870032 | ||
|
|
de9e35ae6a | ||
|
|
1a6fec8ca9 | ||
|
|
094054cd99 | ||
|
|
f85b8a81f1 | ||
|
|
a44eaebf7c | ||
|
|
f37b3c063e | ||
|
|
6e5d5a3b82 | ||
|
|
bf0562d619 | ||
|
|
ecaa81be3c | ||
|
|
d98ae48935 | ||
|
|
f52a76b16c | ||
|
|
d421c27602 | ||
|
|
70e4cd4de1 | ||
|
|
29767e9265 | ||
|
|
46d4c7f96d | ||
|
|
161a6f3923 | ||
|
|
53e912341b | ||
|
|
19396ea11a | ||
|
|
1d9a5e742b | ||
|
|
e8dfdd03f7 | ||
|
|
2f5b15dac7 | ||
|
|
525e1f5136 | ||
|
|
7d63d188af | ||
|
|
87889c12ea | ||
|
|
53d023f5ee | ||
|
|
1877ab8c67 | ||
|
|
72a5a8cab7 | ||
|
|
221e49a978 | ||
|
|
1a4c67d173 | ||
|
|
42fd23ece3 | ||
|
|
3035c0712a | ||
|
|
61315f8bfd | ||
|
|
52683124d8 | ||
|
|
1f77390366 | ||
|
|
322d492540 | ||
|
|
f977d8cca9 | ||
|
|
a9aedea2bd | ||
|
|
5560bbeecb | ||
|
|
f226206703 | ||
|
|
170687226d | ||
|
|
d56d3dc271 | ||
|
|
32a202aff4 | ||
|
|
6ee75e6e60 | ||
|
|
13d74cae3b | ||
|
|
88651916b0 | ||
|
|
be12505d2f | ||
|
|
23fcf3b045 | ||
|
|
9e7459b204 | ||
|
|
4f0eb1d566 | ||
|
|
ce00481f47 | ||
|
|
f596af90ba | ||
|
|
5c74d1d021 | ||
|
|
aff659b6b6 | ||
|
|
58724d95fa | ||
|
|
8d61fcd5c9 | ||
|
|
3e1be53c36 | ||
|
|
f3754588bd | ||
|
|
c4ffffeec8 | ||
|
|
5b69f6a358 | ||
|
|
1af89a7447 | ||
|
|
90abd81035 | ||
|
|
898824b13f | ||
|
|
9d093aa7f8 | ||
|
|
1770549f6c | ||
|
|
d21be77fd2 | ||
|
|
41a1c19877 | ||
|
|
9b6571ce68 | ||
|
|
88e98e4e35 | ||
|
|
10c56ffbfa | ||
|
|
cb2c8d6f3c | ||
|
|
ca62b850ce | ||
|
|
5a75d4e140 | ||
|
|
e0972b7c24 | ||
|
|
0db497916d | ||
|
|
23a0ad3c4e | ||
|
|
2b4e1c4b67 | ||
|
|
9b1b9244cf | ||
|
|
ad570e9b16 | ||
|
|
812ba6de62 | ||
|
|
8f97124adb | ||
|
|
28289838f9 | ||
|
|
cca8a010c3 | ||
|
|
91ab296692 | ||
|
|
ee6c9c4272 | ||
|
|
21cd36fa92 | ||
|
|
b1aafe3dbc | ||
|
|
5cd832de89 | ||
|
|
24dd9d0518 | ||
|
|
aab6ab810a | ||
|
|
d1d6d5e71e | ||
|
|
e67dd68522 | ||
|
|
e25eae846d | ||
|
|
995eeaa455 | ||
|
|
240c61b967 | ||
|
|
2d8b0753b4 | ||
|
|
44eab3de7f | ||
|
|
007be5bf95 | ||
|
|
ee19c7c51f | ||
|
|
ce56afbdf9 | ||
|
|
51012695a1 | ||
|
|
0eef2d2cc5 | ||
|
|
487f9f2815 | ||
|
|
d065adcd8e | ||
|
|
0d9a1dc5eb | ||
|
|
8f9ad15108 | ||
|
|
e538e9b843 | ||
|
|
4a702b6813 | ||
|
|
1e6fd2c57a | ||
|
|
600b959d89 | ||
|
|
b96de9eb13 | ||
|
|
93be19b647 | ||
|
|
74f45f6f1d | ||
|
|
54ba3d2888 | ||
|
|
65d5149f60 | ||
|
|
917ebb3771 | ||
|
|
7e66b1f545 | ||
|
|
05837dca35 | ||
|
|
53be2ebe59 | ||
|
|
0341efcaea | ||
|
|
ec75210fd3 | ||
|
|
e6afe3e806 | ||
|
|
5aa46f068e | ||
|
|
a11a5b28bc | ||
|
|
907aa566ca | ||
|
|
5c21f099a8 | ||
|
|
b91201ae3e | ||
|
|
56d7e19968 | ||
|
|
cf91c6c90e | ||
|
|
9011148adf | ||
|
|
897d0590d2 | ||
|
|
33b33e8458 | ||
|
|
7758f5c187 | ||
|
|
83d7a03ba4 | ||
|
|
a9a0df9699 | ||
|
|
df44f8f5f8 | ||
|
|
216a9ed035 | ||
|
|
35d61b6a6c | ||
|
|
5fb72cea53 | ||
|
|
d54d021e9f | ||
|
|
06e78311df | ||
|
|
df720f95ca | ||
|
|
00faff34d3 | ||
|
|
2b5b3ea4f3 | ||
|
|
95e608d0b4 | ||
|
|
1d55bf87dd | ||
|
|
1220ce53eb | ||
|
|
2006218f87 | ||
|
|
40f427a387 | ||
|
|
445e95baed | ||
|
|
67fbc9ad33 | ||
|
|
1253e9e465 | ||
|
|
21069432e8 | ||
|
|
6facf6a324 | ||
|
|
7556197485 |
@@ -25,7 +25,6 @@ POSTGRES_PASS=postgrespass
|
||||
# DEV SETTINGS
|
||||
APP_PORT=80
|
||||
API_PORT=80
|
||||
API_PROTOCOL=https://
|
||||
HTTP_PROTOCOL=https
|
||||
DOCKER_NETWORK=172.21.0.0/24
|
||||
DOCKER_NGINX_IP=172.21.0.20
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM python:3.9.6-slim
|
||||
FROM python:3.9.9-slim
|
||||
|
||||
ENV TACTICAL_DIR /opt/tactical
|
||||
ENV TACTICAL_READY_FILE ${TACTICAL_DIR}/tmp/tactical.ready
|
||||
@@ -13,10 +13,6 @@ EXPOSE 8000 8383 8005
|
||||
RUN groupadd -g 1000 tactical && \
|
||||
useradd -u 1000 -g 1000 tactical
|
||||
|
||||
# Copy nats-api file
|
||||
COPY natsapi/bin/nats-api /usr/local/bin/
|
||||
RUN chmod +x /usr/local/bin/nats-api
|
||||
|
||||
# Copy dev python reqs
|
||||
COPY .devcontainer/requirements.txt /
|
||||
|
||||
|
||||
@@ -209,7 +209,7 @@ services:
|
||||
CERT_PRIV_KEY: ${CERT_PRIV_KEY}
|
||||
APP_PORT: ${APP_PORT}
|
||||
API_PORT: ${API_PORT}
|
||||
API_PROTOCOL: ${API_PROTOCOL}
|
||||
DEV: 1
|
||||
networks:
|
||||
dev:
|
||||
ipv4_address: ${DOCKER_NGINX_IP}
|
||||
|
||||
@@ -96,6 +96,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_natsapi_conf
|
||||
"${VIRTUAL_ENV}"/bin/python manage.py create_installer_user
|
||||
|
||||
# create super user
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -48,3 +48,6 @@ nats-rmm.conf
|
||||
.mypy_cache
|
||||
docs/site/
|
||||
reset_db.sh
|
||||
run_go_cmd.py
|
||||
nats-api.conf
|
||||
|
||||
|
||||
@@ -15,4 +15,5 @@ class Command(BaseCommand):
|
||||
username=uuid.uuid4().hex,
|
||||
is_installer_user=True,
|
||||
password=User.objects.make_random_password(60), # type: ignore
|
||||
block_dashboard_login=True,
|
||||
)
|
||||
|
||||
150
api/tacticalrmm/accounts/migrations/0028_auto_20211010_0249.py
Normal file
150
api/tacticalrmm/accounts/migrations/0028_auto_20211010_0249.py
Normal file
@@ -0,0 +1,150 @@
|
||||
# Generated by Django 3.2.6 on 2021-10-10 02:49
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('clients', '0018_auto_20211010_0249'),
|
||||
('accounts', '0027_auto_20210903_0054'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='role',
|
||||
name='can_list_accounts',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='role',
|
||||
name='can_list_agent_history',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='role',
|
||||
name='can_list_agents',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='role',
|
||||
name='can_list_alerts',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='role',
|
||||
name='can_list_api_keys',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='role',
|
||||
name='can_list_automation_policies',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='role',
|
||||
name='can_list_autotasks',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='role',
|
||||
name='can_list_checks',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='role',
|
||||
name='can_list_clients',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='role',
|
||||
name='can_list_deployments',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='role',
|
||||
name='can_list_notes',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='role',
|
||||
name='can_list_pendingactions',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='role',
|
||||
name='can_list_roles',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='role',
|
||||
name='can_list_scripts',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='role',
|
||||
name='can_list_sites',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='role',
|
||||
name='can_list_software',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='role',
|
||||
name='can_ping_agents',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='role',
|
||||
name='can_recover_agents',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='role',
|
||||
name='can_view_clients',
|
||||
field=models.ManyToManyField(blank=True, related_name='role_clients', to='clients.Client'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='role',
|
||||
name='can_view_sites',
|
||||
field=models.ManyToManyField(blank=True, related_name='role_sites', to='clients.Site'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='apikey',
|
||||
name='created_by',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='apikey',
|
||||
name='modified_by',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='role',
|
||||
name='created_by',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='role',
|
||||
name='modified_by',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='created_by',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='modified_by',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='role',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='users', to='accounts.role'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,28 @@
|
||||
# Generated by Django 3.2.6 on 2021-10-22 22:45
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0028_auto_20211010_0249'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='role',
|
||||
name='can_list_alerttemplates',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='role',
|
||||
name='can_manage_alerttemplates',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='role',
|
||||
name='can_run_urlactions',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.2.6 on 2021-11-04 02:21
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0029_auto_20211022_2245'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='role',
|
||||
name='can_manage_customfields',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='role',
|
||||
name='can_view_customfields',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -64,7 +64,7 @@ class User(AbstractUser, BaseAuditModel):
|
||||
"accounts.Role",
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="roles",
|
||||
related_name="users",
|
||||
on_delete=models.SET_NULL,
|
||||
)
|
||||
|
||||
@@ -81,6 +81,8 @@ class Role(BaseAuditModel):
|
||||
is_superuser = models.BooleanField(default=False)
|
||||
|
||||
# agents
|
||||
can_list_agents = models.BooleanField(default=False)
|
||||
can_ping_agents = 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)
|
||||
@@ -92,55 +94,82 @@ class Role(BaseAuditModel):
|
||||
can_install_agents = models.BooleanField(default=False)
|
||||
can_run_scripts = models.BooleanField(default=False)
|
||||
can_run_bulk = models.BooleanField(default=False)
|
||||
can_recover_agents = models.BooleanField(default=False)
|
||||
can_list_agent_history = models.BooleanField(default=False)
|
||||
|
||||
# core
|
||||
can_list_notes = models.BooleanField(default=False)
|
||||
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)
|
||||
can_run_urlactions = models.BooleanField(default=False)
|
||||
can_view_customfields = models.BooleanField(default=False)
|
||||
can_manage_customfields = models.BooleanField(default=False)
|
||||
|
||||
# checks
|
||||
can_list_checks = models.BooleanField(default=False)
|
||||
can_manage_checks = models.BooleanField(default=False)
|
||||
can_run_checks = models.BooleanField(default=False)
|
||||
|
||||
# clients
|
||||
can_list_clients = models.BooleanField(default=False)
|
||||
can_manage_clients = models.BooleanField(default=False)
|
||||
can_list_sites = models.BooleanField(default=False)
|
||||
can_manage_sites = models.BooleanField(default=False)
|
||||
can_list_deployments = models.BooleanField(default=False)
|
||||
can_manage_deployments = models.BooleanField(default=False)
|
||||
can_view_clients = models.ManyToManyField(
|
||||
"clients.Client", related_name="role_clients", blank=True
|
||||
)
|
||||
can_view_sites = models.ManyToManyField(
|
||||
"clients.Site", related_name="role_sites", blank=True
|
||||
)
|
||||
|
||||
# automation
|
||||
can_list_automation_policies = models.BooleanField(default=False)
|
||||
can_manage_automation_policies = models.BooleanField(default=False)
|
||||
|
||||
# automated tasks
|
||||
can_list_autotasks = models.BooleanField(default=False)
|
||||
can_manage_autotasks = models.BooleanField(default=False)
|
||||
can_run_autotasks = models.BooleanField(default=False)
|
||||
|
||||
# logs
|
||||
can_view_auditlogs = models.BooleanField(default=False)
|
||||
can_list_pendingactions = models.BooleanField(default=False)
|
||||
can_manage_pendingactions = models.BooleanField(default=False)
|
||||
can_view_debuglogs = models.BooleanField(default=False)
|
||||
|
||||
# scripts
|
||||
can_list_scripts = models.BooleanField(default=False)
|
||||
can_manage_scripts = models.BooleanField(default=False)
|
||||
|
||||
# alerts
|
||||
can_list_alerts = models.BooleanField(default=False)
|
||||
can_manage_alerts = models.BooleanField(default=False)
|
||||
can_list_alerttemplates = models.BooleanField(default=False)
|
||||
can_manage_alerttemplates = models.BooleanField(default=False)
|
||||
|
||||
# win services
|
||||
can_manage_winsvcs = models.BooleanField(default=False)
|
||||
|
||||
# software
|
||||
can_list_software = models.BooleanField(default=False)
|
||||
can_manage_software = models.BooleanField(default=False)
|
||||
|
||||
# windows updates
|
||||
can_manage_winupdates = models.BooleanField(default=False)
|
||||
|
||||
# accounts
|
||||
can_list_accounts = models.BooleanField(default=False)
|
||||
can_manage_accounts = models.BooleanField(default=False)
|
||||
can_list_roles = models.BooleanField(default=False)
|
||||
can_manage_roles = models.BooleanField(default=False)
|
||||
|
||||
# authentication
|
||||
can_list_api_keys = models.BooleanField(default=False)
|
||||
can_manage_api_keys = models.BooleanField(default=False)
|
||||
|
||||
def __str__(self):
|
||||
@@ -153,47 +182,6 @@ class Role(BaseAuditModel):
|
||||
|
||||
return RoleAuditSerializer(role).data
|
||||
|
||||
@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",
|
||||
"can_manage_api_keys",
|
||||
]
|
||||
|
||||
|
||||
class APIKey(BaseAuditModel):
|
||||
name = CharField(unique=True, max_length=25)
|
||||
|
||||
@@ -6,35 +6,38 @@ 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_list_accounts")
|
||||
else:
|
||||
|
||||
# allow users to reset their own password/2fa see issue #686
|
||||
base_path = "/accounts/users/"
|
||||
paths = ["reset/", "reset_totp/"]
|
||||
# allow users to reset their own password/2fa see issue #686
|
||||
base_path = "/accounts/users/"
|
||||
paths = ["reset/", "reset_totp/"]
|
||||
|
||||
if r.path in [base_path + i for i in paths]:
|
||||
from accounts.models import User
|
||||
if r.path in [base_path + i for i in paths]:
|
||||
from accounts.models import User
|
||||
|
||||
try:
|
||||
user = User.objects.get(pk=r.data["id"])
|
||||
except User.DoesNotExist:
|
||||
pass
|
||||
else:
|
||||
if user == r.user:
|
||||
return True
|
||||
try:
|
||||
user = User.objects.get(pk=r.data["id"])
|
||||
except User.DoesNotExist:
|
||||
pass
|
||||
else:
|
||||
if user == r.user:
|
||||
return True
|
||||
|
||||
return _has_perm(r, "can_manage_accounts")
|
||||
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")
|
||||
return _has_perm(r, "can_list_roles")
|
||||
else:
|
||||
return _has_perm(r, "can_manage_roles")
|
||||
|
||||
|
||||
class APIKeyPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
if r.method == "GET":
|
||||
return _has_perm(r, "can_list_api_keys")
|
||||
|
||||
return _has_perm(r, "can_manage_api_keys")
|
||||
|
||||
@@ -61,10 +61,15 @@ class TOTPSetupSerializer(ModelSerializer):
|
||||
|
||||
|
||||
class RoleSerializer(ModelSerializer):
|
||||
user_count = SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Role
|
||||
fields = "__all__"
|
||||
|
||||
def get_user_count(self, obj):
|
||||
return obj.users.count()
|
||||
|
||||
|
||||
class RoleAuditSerializer(ModelSerializer):
|
||||
class Meta:
|
||||
|
||||
@@ -27,12 +27,12 @@ class TestAccounts(TacticalTestCase):
|
||||
data = {"username": "bob", "password": "a3asdsa2314"}
|
||||
r = self.client.post(url, data, format="json")
|
||||
self.assertEqual(r.status_code, 400)
|
||||
self.assertEqual(r.data, "bad credentials")
|
||||
self.assertEqual(r.data, "Bad credentials")
|
||||
|
||||
data = {"username": "billy", "password": "hunter2"}
|
||||
r = self.client.post(url, data, format="json")
|
||||
self.assertEqual(r.status_code, 400)
|
||||
self.assertEqual(r.data, "bad credentials")
|
||||
self.assertEqual(r.data, "Bad credentials")
|
||||
|
||||
self.bob.totp_key = "AB5RI6YPFTZAS52G"
|
||||
self.bob.save()
|
||||
@@ -61,7 +61,7 @@ class TestAccounts(TacticalTestCase):
|
||||
mock_verify.return_value = False
|
||||
r = self.client.post(url, data, format="json")
|
||||
self.assertEqual(r.status_code, 400)
|
||||
self.assertEqual(r.data, "bad credentials")
|
||||
self.assertEqual(r.data, "Bad credentials")
|
||||
|
||||
mock_verify.return_value = True
|
||||
data = {"username": "bob", "password": "asd234234asd", "twofactor": "123456"}
|
||||
@@ -396,7 +396,7 @@ class TestAPIAuthentication(TacticalTestCase):
|
||||
self.client_setup()
|
||||
|
||||
def test_api_auth(self):
|
||||
url = "/clients/clients/"
|
||||
url = "/clients/"
|
||||
# auth should fail if no header set
|
||||
self.check_not_authenticated("get", url)
|
||||
|
||||
|
||||
@@ -9,9 +9,8 @@ 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()),
|
||||
path("roles/<int:pk>/", views.GetUpdateDeleteRole.as_view()),
|
||||
path("apikeys/", views.GetAddAPIKeys.as_view()),
|
||||
path("apikeys/<int:pk>/", views.GetUpdateDeleteAPIKey.as_view()),
|
||||
]
|
||||
|
||||
@@ -44,12 +44,12 @@ class CheckCreds(KnoxLoginView):
|
||||
AuditLog.audit_user_failed_login(
|
||||
request.data["username"], debug_info={"ip": request._client_ip}
|
||||
)
|
||||
return Response("bad credentials", status=status.HTTP_400_BAD_REQUEST)
|
||||
return notify_error("Bad credentials")
|
||||
|
||||
user = serializer.validated_data["user"]
|
||||
|
||||
if user.block_dashboard_login:
|
||||
return Response("bad credentials", status=status.HTTP_400_BAD_REQUEST)
|
||||
return notify_error("Bad credentials")
|
||||
|
||||
# if totp token not set modify response to notify frontend
|
||||
if not user.totp_key:
|
||||
@@ -72,6 +72,9 @@ class LoginView(KnoxLoginView):
|
||||
serializer.is_valid(raise_exception=True)
|
||||
user = serializer.validated_data["user"]
|
||||
|
||||
if user.block_dashboard_login:
|
||||
return notify_error("Bad credentials")
|
||||
|
||||
token = request.data["twofactor"]
|
||||
totp = pyotp.TOTP(user.totp_key)
|
||||
|
||||
@@ -96,7 +99,7 @@ class LoginView(KnoxLoginView):
|
||||
AuditLog.audit_user_failed_twofactor(
|
||||
request.data["username"], debug_info={"ip": request._client_ip}
|
||||
)
|
||||
return Response("bad credentials", status=status.HTTP_400_BAD_REQUEST)
|
||||
return notify_error("Bad credentials")
|
||||
|
||||
|
||||
class GetAddUsers(APIView):
|
||||
@@ -221,11 +224,6 @@ class UserUI(APIView):
|
||||
return Response("ok")
|
||||
|
||||
|
||||
class PermsList(APIView):
|
||||
def get(self, request):
|
||||
return Response(Role.perms())
|
||||
|
||||
|
||||
class GetAddRoles(APIView):
|
||||
permission_classes = [IsAuthenticated, RolesPerms]
|
||||
|
||||
@@ -237,7 +235,7 @@ class GetAddRoles(APIView):
|
||||
serializer = RoleSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
return Response("ok")
|
||||
return Response("Role was added")
|
||||
|
||||
|
||||
class GetUpdateDeleteRole(APIView):
|
||||
@@ -252,12 +250,12 @@ class GetUpdateDeleteRole(APIView):
|
||||
serializer = RoleSerializer(instance=role, data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
return Response("ok")
|
||||
return Response("Role was edited")
|
||||
|
||||
def delete(self, request, pk):
|
||||
role = get_object_or_404(Role, pk=pk)
|
||||
role.delete()
|
||||
return Response("ok")
|
||||
return Response("Role was removed")
|
||||
|
||||
|
||||
class GetAddAPIKeys(APIView):
|
||||
@@ -269,15 +267,9 @@ class GetAddAPIKeys(APIView):
|
||||
|
||||
def post(self, request):
|
||||
# generate a random API Key
|
||||
# https://stackoverflow.com/questions/2257441/random-string-generation-with-upper-case-letters-and-digits/23728630#23728630
|
||||
import random
|
||||
import string
|
||||
|
||||
request.data["key"] = "".join(
|
||||
random.SystemRandom().choice(string.ascii_uppercase + string.digits)
|
||||
for _ in range(32)
|
||||
)
|
||||
from django.utils.crypto import get_random_string
|
||||
|
||||
request.data["key"] = get_random_string(length=32).upper()
|
||||
serializer = APIKeySerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
obj = serializer.save()
|
||||
|
||||
@@ -30,7 +30,8 @@ agent = Recipe(
|
||||
hostname="DESKTOP-TEST123",
|
||||
version="1.3.0",
|
||||
monitoring_type=cycle(["workstation", "server"]),
|
||||
agent_id=seq("asdkj3h4234-1234hg3h4g34-234jjh34|DESKTOP-TEST123"),
|
||||
agent_id=seq(generate_agent_id("DESKTOP-TEST123")),
|
||||
last_seen=djangotime.now() - djangotime.timedelta(days=5),
|
||||
)
|
||||
|
||||
server_agent = agent.extend(
|
||||
|
||||
20
api/tacticalrmm/agents/management/commands/update_agents.py
Normal file
20
api/tacticalrmm/agents/management/commands/update_agents.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand
|
||||
from packaging import version as pyver
|
||||
|
||||
from agents.models import Agent
|
||||
from agents.tasks import send_agent_update_task
|
||||
from tacticalrmm.utils import AGENT_DEFER
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Triggers an agent update task to run"
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
q = Agent.objects.defer(*AGENT_DEFER).exclude(version=settings.LATEST_AGENT_VER)
|
||||
agent_ids: list[str] = [
|
||||
i.agent_id
|
||||
for i in q
|
||||
if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER)
|
||||
]
|
||||
send_agent_update_task.delay(agent_ids=agent_ids)
|
||||
28
api/tacticalrmm/agents/migrations/0040_auto_20211010_0249.py
Normal file
28
api/tacticalrmm/agents/migrations/0040_auto_20211010_0249.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# Generated by Django 3.2.6 on 2021-10-10 02:49
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('agents', '0039_auto_20210714_0738'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='agent',
|
||||
name='agent_id',
|
||||
field=models.CharField(max_length=200, unique=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='agent',
|
||||
name='created_by',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='agent',
|
||||
name='modified_by',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.6 on 2021-10-18 03:04
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('agents', '0040_auto_20211010_0249'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='agenthistory',
|
||||
name='username',
|
||||
field=models.CharField(default='system', max_length=255),
|
||||
),
|
||||
]
|
||||
File diff suppressed because one or more lines are too long
@@ -22,9 +22,12 @@ from packaging import version as pyver
|
||||
|
||||
from core.models import TZ_CHOICES, CoreSettings
|
||||
from logs.models import BaseAuditModel, DebugLog
|
||||
from tacticalrmm.models import PermissionQuerySet
|
||||
|
||||
|
||||
class Agent(BaseAuditModel):
|
||||
objects = PermissionQuerySet.as_manager()
|
||||
|
||||
version = models.CharField(default="0.1.0", max_length=255)
|
||||
salt_ver = models.CharField(default="1.0.3", max_length=255)
|
||||
operating_system = models.CharField(null=True, blank=True, max_length=255)
|
||||
@@ -33,7 +36,7 @@ class Agent(BaseAuditModel):
|
||||
hostname = models.CharField(max_length=255)
|
||||
salt_id = models.CharField(null=True, blank=True, max_length=255)
|
||||
local_ip = models.TextField(null=True, blank=True) # deprecated
|
||||
agent_id = models.CharField(max_length=200)
|
||||
agent_id = models.CharField(max_length=200, unique=True)
|
||||
last_seen = models.DateTimeField(null=True, blank=True)
|
||||
services = models.JSONField(null=True, blank=True)
|
||||
public_ip = models.CharField(null=True, max_length=255)
|
||||
@@ -87,6 +90,7 @@ class Agent(BaseAuditModel):
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
from automation.tasks import generate_agent_checks_task
|
||||
|
||||
# get old agent if exists
|
||||
old_agent = Agent.objects.get(pk=self.pk) if self.pk else None
|
||||
@@ -103,8 +107,7 @@ class Agent(BaseAuditModel):
|
||||
or (old_agent.monitoring_type != self.monitoring_type)
|
||||
or (old_agent.block_policy_inheritance != self.block_policy_inheritance)
|
||||
):
|
||||
self.generate_checks_from_policies()
|
||||
self.generate_tasks_from_policies()
|
||||
generate_agent_checks_task.delay(agents=[self.pk], create_tasks=True)
|
||||
|
||||
# calculate alert template for new agents
|
||||
if not old_agent:
|
||||
@@ -417,12 +420,6 @@ class Agent(BaseAuditModel):
|
||||
update.action = "approve"
|
||||
update.save(update_fields=["action"])
|
||||
|
||||
DebugLog.info(
|
||||
agent=self,
|
||||
log_type="windows_updates",
|
||||
message=f"Approving windows updates on {self.hostname}",
|
||||
)
|
||||
|
||||
# returns agent policy merged with a client or site specific policy
|
||||
def get_patch_policy(self):
|
||||
|
||||
@@ -709,7 +706,7 @@ class Agent(BaseAuditModel):
|
||||
key1 = key[0:48]
|
||||
key2 = key[48:]
|
||||
msg = '{{"a":{}, "u":"{}","time":{}}}'.format(
|
||||
action, user, int(time.time())
|
||||
action, user.lower(), int(time.time())
|
||||
)
|
||||
iv = get_random_bytes(16)
|
||||
|
||||
@@ -751,8 +748,8 @@ class Agent(BaseAuditModel):
|
||||
try:
|
||||
ret = msgpack.loads(msg.data) # type: ignore
|
||||
except Exception as e:
|
||||
DebugLog.error(agent=self, log_type="agent_issues", message=e)
|
||||
ret = str(e)
|
||||
DebugLog.error(agent=self, log_type="agent_issues", message=ret)
|
||||
|
||||
await nc.close()
|
||||
return ret
|
||||
@@ -871,6 +868,8 @@ RECOVERY_CHOICES = [
|
||||
|
||||
|
||||
class RecoveryAction(models.Model):
|
||||
objects = PermissionQuerySet.as_manager()
|
||||
|
||||
agent = models.ForeignKey(
|
||||
Agent,
|
||||
related_name="recoveryactions",
|
||||
@@ -885,6 +884,8 @@ class RecoveryAction(models.Model):
|
||||
|
||||
|
||||
class Note(models.Model):
|
||||
objects = PermissionQuerySet.as_manager()
|
||||
|
||||
agent = models.ForeignKey(
|
||||
Agent,
|
||||
related_name="notes",
|
||||
@@ -905,6 +906,8 @@ class Note(models.Model):
|
||||
|
||||
|
||||
class AgentCustomField(models.Model):
|
||||
objects = PermissionQuerySet.as_manager()
|
||||
|
||||
agent = models.ForeignKey(
|
||||
Agent,
|
||||
related_name="custom_fields",
|
||||
@@ -965,6 +968,8 @@ AGENT_HISTORY_STATUS = (("success", "Success"), ("failure", "Failure"))
|
||||
|
||||
|
||||
class AgentHistory(models.Model):
|
||||
objects = PermissionQuerySet.as_manager()
|
||||
|
||||
agent = models.ForeignKey(
|
||||
Agent,
|
||||
related_name="history",
|
||||
@@ -978,7 +983,7 @@ class AgentHistory(models.Model):
|
||||
status = models.CharField(
|
||||
max_length=50, choices=AGENT_HISTORY_STATUS, default="success"
|
||||
)
|
||||
username = models.CharField(max_length=50, default="system")
|
||||
username = models.CharField(max_length=255, default="system")
|
||||
results = models.TextField(null=True, blank=True)
|
||||
script = models.ForeignKey(
|
||||
"scripts.Script",
|
||||
|
||||
@@ -1,16 +1,42 @@
|
||||
from rest_framework import permissions
|
||||
|
||||
from tacticalrmm.permissions import _has_perm
|
||||
from tacticalrmm.permissions import _has_perm, _has_perm_on_agent
|
||||
|
||||
|
||||
class AgentPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
if r.method == "GET":
|
||||
if "agent_id" in view.kwargs.keys():
|
||||
return _has_perm(r, "can_list_agents") and _has_perm_on_agent(
|
||||
r.user, view.kwargs["agent_id"]
|
||||
)
|
||||
else:
|
||||
return _has_perm(r, "can_list_agents")
|
||||
elif r.method == "DELETE":
|
||||
return _has_perm(r, "can_uninstall_agents") and _has_perm_on_agent(
|
||||
r.user, view.kwargs["agent_id"]
|
||||
)
|
||||
else:
|
||||
if r.path == "/agents/maintenance/bulk/":
|
||||
return _has_perm(r, "can_edit_agent")
|
||||
else:
|
||||
return _has_perm(r, "can_edit_agent") and _has_perm_on_agent(
|
||||
r.user, view.kwargs["agent_id"]
|
||||
)
|
||||
|
||||
|
||||
class RecoverAgentPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
return _has_perm(r, "can_recover_agents") and _has_perm_on_agent(
|
||||
r.user, view.kwargs["agent_id"]
|
||||
)
|
||||
|
||||
|
||||
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")
|
||||
return _has_perm(r, "can_use_mesh") and _has_perm_on_agent(
|
||||
r.user, view.kwargs["agent_id"]
|
||||
)
|
||||
|
||||
|
||||
class UpdateAgentPerms(permissions.BasePermission):
|
||||
@@ -18,29 +44,39 @@ class UpdateAgentPerms(permissions.BasePermission):
|
||||
return _has_perm(r, "can_update_agents")
|
||||
|
||||
|
||||
class EditAgentPerms(permissions.BasePermission):
|
||||
class PingAgentPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
return _has_perm(r, "can_edit_agent")
|
||||
return _has_perm(r, "can_ping_agents") and _has_perm_on_agent(
|
||||
r.user, view.kwargs["agent_id"]
|
||||
)
|
||||
|
||||
|
||||
class ManageProcPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
return _has_perm(r, "can_manage_procs")
|
||||
return _has_perm(r, "can_manage_procs") and _has_perm_on_agent(
|
||||
r.user, view.kwargs["agent_id"]
|
||||
)
|
||||
|
||||
|
||||
class EvtLogPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
return _has_perm(r, "can_view_eventlogs")
|
||||
return _has_perm(r, "can_view_eventlogs") and _has_perm_on_agent(
|
||||
r.user, view.kwargs["agent_id"]
|
||||
)
|
||||
|
||||
|
||||
class SendCMDPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
return _has_perm(r, "can_send_cmd")
|
||||
return _has_perm(r, "can_send_cmd") and _has_perm_on_agent(
|
||||
r.user, view.kwargs["agent_id"]
|
||||
)
|
||||
|
||||
|
||||
class RebootAgentPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
return _has_perm(r, "can_reboot_agents")
|
||||
return _has_perm(r, "can_reboot_agents") and _has_perm_on_agent(
|
||||
r.user, view.kwargs["agent_id"]
|
||||
)
|
||||
|
||||
|
||||
class InstallAgentPerms(permissions.BasePermission):
|
||||
@@ -50,14 +86,38 @@ class InstallAgentPerms(permissions.BasePermission):
|
||||
|
||||
class RunScriptPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
return _has_perm(r, "can_run_scripts")
|
||||
return _has_perm(r, "can_run_scripts") and _has_perm_on_agent(
|
||||
r.user, view.kwargs["agent_id"]
|
||||
)
|
||||
|
||||
|
||||
class ManageNotesPerms(permissions.BasePermission):
|
||||
class AgentNotesPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
return _has_perm(r, "can_manage_notes")
|
||||
|
||||
# permissions for GET /agents/notes/ endpoint
|
||||
if r.method == "GET":
|
||||
|
||||
# permissions for /agents/<agent_id>/notes endpoint
|
||||
if "agent_id" in view.kwargs.keys():
|
||||
return _has_perm(r, "can_list_notes") and _has_perm_on_agent(
|
||||
r.user, view.kwargs["agent_id"]
|
||||
)
|
||||
else:
|
||||
return _has_perm(r, "can_list_notes")
|
||||
else:
|
||||
return _has_perm(r, "can_manage_notes")
|
||||
|
||||
|
||||
class RunBulkPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
return _has_perm(r, "can_run_bulk")
|
||||
|
||||
|
||||
class AgentHistoryPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
if "agent_id" in view.kwargs.keys():
|
||||
return _has_perm(r, "can_list_agent_history") and _has_perm_on_agent(
|
||||
r.user, view.kwargs["agent_id"]
|
||||
)
|
||||
else:
|
||||
return _has_perm(r, "can_list_agent_history")
|
||||
|
||||
@@ -1,14 +1,30 @@
|
||||
import pytz
|
||||
from clients.serializers import ClientSerializer
|
||||
from rest_framework import serializers
|
||||
from tacticalrmm.utils import get_default_timezone
|
||||
from winupdate.serializers import WinUpdatePolicySerializer
|
||||
|
||||
from .models import Agent, AgentCustomField, Note, AgentHistory
|
||||
|
||||
|
||||
class AgentCustomFieldSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = AgentCustomField
|
||||
fields = (
|
||||
"id",
|
||||
"field",
|
||||
"agent",
|
||||
"value",
|
||||
"string_value",
|
||||
"bool_value",
|
||||
"multiple_value",
|
||||
)
|
||||
extra_kwargs = {
|
||||
"string_value": {"write_only": True},
|
||||
"bool_value": {"write_only": True},
|
||||
"multiple_value": {"write_only": True},
|
||||
}
|
||||
|
||||
|
||||
class AgentSerializer(serializers.ModelSerializer):
|
||||
# for vue
|
||||
winupdatepolicy = WinUpdatePolicySerializer(many=True, read_only=True)
|
||||
status = serializers.ReadOnlyField()
|
||||
cpu_model = serializers.ReadOnlyField()
|
||||
@@ -19,28 +35,18 @@ class AgentSerializer(serializers.ModelSerializer):
|
||||
checks = serializers.ReadOnlyField()
|
||||
timezone = serializers.ReadOnlyField()
|
||||
all_timezones = serializers.SerializerMethodField()
|
||||
client_name = serializers.ReadOnlyField(source="client.name")
|
||||
client = serializers.ReadOnlyField(source="client.name")
|
||||
site_name = serializers.ReadOnlyField(source="site.name")
|
||||
custom_fields = AgentCustomFieldSerializer(many=True, read_only=True)
|
||||
patches_last_installed = serializers.ReadOnlyField()
|
||||
last_seen = serializers.ReadOnlyField()
|
||||
|
||||
def get_all_timezones(self, obj):
|
||||
return pytz.all_timezones
|
||||
|
||||
class Meta:
|
||||
model = Agent
|
||||
exclude = [
|
||||
"last_seen",
|
||||
]
|
||||
|
||||
|
||||
class AgentOverdueActionSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Agent
|
||||
fields = [
|
||||
"pk",
|
||||
"overdue_email_alert",
|
||||
"overdue_text_alert",
|
||||
"overdue_dashboard_alert",
|
||||
]
|
||||
exclude = ["id"]
|
||||
|
||||
|
||||
class AgentTableSerializer(serializers.ModelSerializer):
|
||||
@@ -88,10 +94,9 @@ class AgentTableSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Agent
|
||||
fields = [
|
||||
"id",
|
||||
"agent_id",
|
||||
"alert_template",
|
||||
"hostname",
|
||||
"agent_id",
|
||||
"site_name",
|
||||
"client_name",
|
||||
"monitoring_type",
|
||||
@@ -115,58 +120,6 @@ class AgentTableSerializer(serializers.ModelSerializer):
|
||||
depth = 2
|
||||
|
||||
|
||||
class AgentCustomFieldSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = AgentCustomField
|
||||
fields = (
|
||||
"id",
|
||||
"field",
|
||||
"agent",
|
||||
"value",
|
||||
"string_value",
|
||||
"bool_value",
|
||||
"multiple_value",
|
||||
)
|
||||
extra_kwargs = {
|
||||
"string_value": {"write_only": True},
|
||||
"bool_value": {"write_only": True},
|
||||
"multiple_value": {"write_only": True},
|
||||
}
|
||||
|
||||
|
||||
class AgentEditSerializer(serializers.ModelSerializer):
|
||||
winupdatepolicy = WinUpdatePolicySerializer(many=True, read_only=True)
|
||||
all_timezones = serializers.SerializerMethodField()
|
||||
client = ClientSerializer(read_only=True)
|
||||
custom_fields = AgentCustomFieldSerializer(many=True, read_only=True)
|
||||
|
||||
def get_all_timezones(self, obj):
|
||||
return pytz.all_timezones
|
||||
|
||||
class Meta:
|
||||
model = Agent
|
||||
fields = [
|
||||
"id",
|
||||
"hostname",
|
||||
"client",
|
||||
"site",
|
||||
"monitoring_type",
|
||||
"description",
|
||||
"time_zone",
|
||||
"timezone",
|
||||
"check_interval",
|
||||
"overdue_time",
|
||||
"offline_time",
|
||||
"overdue_text_alert",
|
||||
"overdue_email_alert",
|
||||
"overdue_dashboard_alert",
|
||||
"all_timezones",
|
||||
"winupdatepolicy",
|
||||
"policy",
|
||||
"custom_fields",
|
||||
]
|
||||
|
||||
|
||||
class WinAgentSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Agent
|
||||
@@ -180,27 +133,22 @@ class AgentHostnameSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Agent
|
||||
fields = (
|
||||
"id",
|
||||
"hostname",
|
||||
"pk",
|
||||
"agent_id",
|
||||
"client",
|
||||
"site",
|
||||
)
|
||||
|
||||
|
||||
class NoteSerializer(serializers.ModelSerializer):
|
||||
class AgentNoteSerializer(serializers.ModelSerializer):
|
||||
username = serializers.ReadOnlyField(source="user.username")
|
||||
agent_id = serializers.ReadOnlyField(source="agent.agent_id")
|
||||
|
||||
class Meta:
|
||||
model = Note
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class NotesSerializer(serializers.ModelSerializer):
|
||||
notes = NoteSerializer(many=True, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Agent
|
||||
fields = ["hostname", "pk", "notes"]
|
||||
fields = ("pk", "entry_time", "agent", "user", "note", "username", "agent_id")
|
||||
extra_kwargs = {"agent": {"write_only": True}, "user": {"write_only": True}}
|
||||
|
||||
|
||||
class AgentHistorySerializer(serializers.ModelSerializer):
|
||||
@@ -212,8 +160,8 @@ class AgentHistorySerializer(serializers.ModelSerializer):
|
||||
fields = "__all__"
|
||||
|
||||
def get_time(self, history):
|
||||
timezone = get_default_timezone()
|
||||
return history.time.astimezone(timezone).strftime("%m %d %Y %H:%M:%S")
|
||||
tz = self.context["default_tz"]
|
||||
return history.time.astimezone(tz).strftime("%m %d %Y %H:%M:%S")
|
||||
|
||||
|
||||
class AgentAuditSerializer(serializers.ModelSerializer):
|
||||
|
||||
@@ -1,27 +1,25 @@
|
||||
import asyncio
|
||||
import datetime as dt
|
||||
import random
|
||||
import urllib.parse
|
||||
from time import sleep
|
||||
from typing import Union
|
||||
|
||||
from alerts.models import Alert
|
||||
from core.models import CodeSignToken, CoreSettings
|
||||
from core.models import CoreSettings
|
||||
from django.conf import settings
|
||||
from django.utils import timezone as djangotime
|
||||
from logs.models import DebugLog, PendingAction
|
||||
from packaging import version as pyver
|
||||
from scripts.models import Script
|
||||
from tacticalrmm.celery import app
|
||||
from tacticalrmm.utils import run_nats_api_cmd
|
||||
|
||||
from agents.models import Agent
|
||||
from agents.utils import get_winagent_url
|
||||
|
||||
|
||||
def agent_update(pk: int, codesigntoken: str = None, force: bool = False) -> str:
|
||||
from agents.utils import get_exegen_url
|
||||
def agent_update(agent_id: str, force: bool = False) -> str:
|
||||
|
||||
agent = Agent.objects.get(pk=pk)
|
||||
agent = Agent.objects.get(agent_id=agent_id)
|
||||
|
||||
if pyver.parse(agent.version) <= pyver.parse("1.3.0"):
|
||||
return "not supported"
|
||||
@@ -31,19 +29,13 @@ def agent_update(pk: int, codesigntoken: str = None, force: bool = False) -> str
|
||||
DebugLog.warning(
|
||||
agent=agent,
|
||||
log_type="agent_issues",
|
||||
message=f"Unable to determine arch on {agent.hostname}({agent.pk}). Skipping agent update.",
|
||||
message=f"Unable to determine arch on {agent.hostname}({agent.agent_id}). Skipping agent update.",
|
||||
)
|
||||
return "noarch"
|
||||
|
||||
version = settings.LATEST_AGENT_VER
|
||||
inno = agent.win_inno_exe
|
||||
|
||||
if codesigntoken is not None and pyver.parse(version) >= pyver.parse("1.5.0"):
|
||||
base_url = get_exegen_url() + "/api/v1/winagents/?"
|
||||
params = {"version": version, "arch": agent.arch, "token": codesigntoken}
|
||||
url = base_url + urllib.parse.urlencode(params)
|
||||
else:
|
||||
url = agent.winagent_dl
|
||||
url = get_winagent_url(agent.arch)
|
||||
|
||||
if not force:
|
||||
if agent.pendingactions.filter(
|
||||
@@ -76,31 +68,21 @@ def agent_update(pk: int, codesigntoken: str = None, force: bool = False) -> str
|
||||
|
||||
|
||||
@app.task
|
||||
def force_code_sign(pks: list[int]) -> None:
|
||||
try:
|
||||
token = CodeSignToken.objects.first().token # type:ignore
|
||||
except:
|
||||
return
|
||||
|
||||
chunks = (pks[i : i + 50] for i in range(0, len(pks), 50))
|
||||
def force_code_sign(agent_ids: list[str]) -> None:
|
||||
chunks = (agent_ids[i : i + 50] for i in range(0, len(agent_ids), 50))
|
||||
for chunk in chunks:
|
||||
for pk in chunk:
|
||||
agent_update(pk=pk, codesigntoken=token, force=True)
|
||||
for agent_id in chunk:
|
||||
agent_update(agent_id=agent_id, force=True)
|
||||
sleep(0.05)
|
||||
sleep(4)
|
||||
|
||||
|
||||
@app.task
|
||||
def send_agent_update_task(pks: list[int]) -> None:
|
||||
try:
|
||||
codesigntoken = CodeSignToken.objects.first().token # type:ignore
|
||||
except:
|
||||
codesigntoken = None
|
||||
|
||||
chunks = (pks[i : i + 30] for i in range(0, len(pks), 30))
|
||||
def send_agent_update_task(agent_ids: list[str]) -> None:
|
||||
chunks = (agent_ids[i : i + 50] for i in range(0, len(agent_ids), 50))
|
||||
for chunk in chunks:
|
||||
for pk in chunk:
|
||||
agent_update(pk, codesigntoken)
|
||||
for agent_id in chunk:
|
||||
agent_update(agent_id)
|
||||
sleep(0.05)
|
||||
sleep(4)
|
||||
|
||||
@@ -111,22 +93,17 @@ def auto_self_agent_update_task() -> None:
|
||||
if not core.agent_auto_update: # type:ignore
|
||||
return
|
||||
|
||||
try:
|
||||
codesigntoken = CodeSignToken.objects.first().token # type:ignore
|
||||
except:
|
||||
codesigntoken = None
|
||||
|
||||
q = Agent.objects.only("pk", "version")
|
||||
pks: list[int] = [
|
||||
i.pk
|
||||
q = Agent.objects.only("agent_id", "version")
|
||||
agent_ids: list[str] = [
|
||||
i.agent_id
|
||||
for i in q
|
||||
if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER)
|
||||
]
|
||||
|
||||
chunks = (pks[i : i + 30] for i in range(0, len(pks), 30))
|
||||
chunks = (agent_ids[i : i + 30] for i in range(0, len(agent_ids), 30))
|
||||
for chunk in chunks:
|
||||
for pk in chunk:
|
||||
agent_update(pk, codesigntoken)
|
||||
for agent_id in chunk:
|
||||
agent_update(agent_id)
|
||||
sleep(0.05)
|
||||
sleep(4)
|
||||
|
||||
@@ -290,7 +267,7 @@ def run_script_email_results_task(
|
||||
server.send_message(msg)
|
||||
server.quit()
|
||||
except Exception as e:
|
||||
DebugLog.error(message=e)
|
||||
DebugLog.error(message=str(e))
|
||||
|
||||
|
||||
@app.task
|
||||
@@ -321,25 +298,6 @@ def clear_faults_task(older_than_days: int) -> None:
|
||||
)
|
||||
|
||||
|
||||
@app.task
|
||||
def get_wmi_task() -> None:
|
||||
agents = Agent.objects.only(
|
||||
"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, timeout=45)
|
||||
|
||||
|
||||
@app.task
|
||||
def agent_checkin_task() -> None:
|
||||
run_nats_api_cmd("checkin", timeout=30)
|
||||
|
||||
|
||||
@app.task
|
||||
def agent_getinfo_task() -> None:
|
||||
run_nats_api_cmd("agentinfo", timeout=30)
|
||||
|
||||
|
||||
@app.task
|
||||
def prune_agent_history(older_than_days: int) -> str:
|
||||
from .models import AgentHistory
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,33 +1,44 @@
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
from checks.views import GetAddChecks
|
||||
from autotasks.views import GetAddAutoTasks
|
||||
from logs.views import PendingActions
|
||||
|
||||
urlpatterns = [
|
||||
path("listagents/", views.AgentsTableList.as_view()),
|
||||
path("listagentsnodetail/", views.list_agents_no_detail),
|
||||
path("<int:pk>/agenteditdetails/", views.agent_edit_details),
|
||||
path("overdueaction/", views.overdue_action),
|
||||
path("sendrawcmd/", views.send_raw_cmd),
|
||||
path("<pk>/agentdetail/", views.agent_detail),
|
||||
path("<int:pk>/meshcentral/", views.meshcentral),
|
||||
# agent views
|
||||
path("", views.GetAgents.as_view()),
|
||||
path("<agent:agent_id>/", views.GetUpdateDeleteAgent.as_view()),
|
||||
path("<agent:agent_id>/cmd/", views.send_raw_cmd),
|
||||
path("<agent:agent_id>/runscript/", views.run_script),
|
||||
path("<agent:agent_id>/wmi/", views.WMI.as_view()),
|
||||
path("<agent:agent_id>/recover/", views.recover),
|
||||
path("<agent:agent_id>/reboot/", views.Reboot.as_view()),
|
||||
path("<agent:agent_id>/ping/", views.ping),
|
||||
# alias for checks get view
|
||||
path("<agent:agent_id>/checks/", GetAddChecks.as_view()),
|
||||
# alias for autotasks get view
|
||||
path("<agent:agent_id>/tasks/", GetAddAutoTasks.as_view()),
|
||||
# alias for pending actions get view
|
||||
path("<agent:agent_id>/pendingactions/", PendingActions.as_view()),
|
||||
# agent remote background
|
||||
path("<agent:agent_id>/meshcentral/", views.AgentMeshCentral.as_view()),
|
||||
path("<agent:agent_id>/meshcentral/recover/", views.AgentMeshCentral.as_view()),
|
||||
path("<agent:agent_id>/processes/", views.AgentProcesses.as_view()),
|
||||
path("<agent:agent_id>/processes/<int:pid>/", views.AgentProcesses.as_view()),
|
||||
path("<agent:agent_id>/eventlog/<str:logtype>/<int:days>/", views.get_event_log),
|
||||
# agent history
|
||||
path("history/", views.AgentHistoryView.as_view()),
|
||||
path("<agent:agent_id>/history/", views.AgentHistoryView.as_view()),
|
||||
# agent notes
|
||||
path("notes/", views.GetAddNotes.as_view()),
|
||||
path("notes/<int:pk>/", views.GetEditDeleteNote.as_view()),
|
||||
path("<agent:agent_id>/notes/", views.GetAddNotes.as_view()),
|
||||
# bulk actions
|
||||
path("maintenance/bulk/", views.agent_maintenance),
|
||||
path("actions/bulk/", views.bulk),
|
||||
path("versions/", views.get_agent_versions),
|
||||
path("update/", views.update_agents),
|
||||
path("installer/", views.install_agent),
|
||||
path("<str:arch>/getmeshexe/", views.get_mesh_exe),
|
||||
path("uninstall/", views.uninstall),
|
||||
path("editagent/", views.edit_agent),
|
||||
path("<pk>/geteventlog/<logtype>/<days>/", views.get_event_log),
|
||||
path("getagentversions/", views.get_agent_versions),
|
||||
path("updateagents/", views.update_agents),
|
||||
path("<pk>/getprocs/", views.get_processes),
|
||||
path("<pk>/<pid>/killproc/", views.kill_proc),
|
||||
path("reboot/", views.Reboot.as_view()),
|
||||
path("installagent/", views.install_agent),
|
||||
path("<int:pk>/ping/", views.ping),
|
||||
path("recover/", views.recover),
|
||||
path("runscript/", views.run_script),
|
||||
path("<int:pk>/recovermesh/", views.recover_mesh),
|
||||
path("<int:pk>/notes/", views.GetAddNotes.as_view()),
|
||||
path("<int:pk>/note/", views.GetEditDeleteNote.as_view()),
|
||||
path("bulk/", views.bulk),
|
||||
path("maintenance/", views.agent_maintenance),
|
||||
path("<int:pk>/wmi/", views.WMI.as_view()),
|
||||
path("history/<int:pk>/", views.AgentHistoryView.as_view()),
|
||||
]
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import random
|
||||
import urllib.parse
|
||||
|
||||
import requests
|
||||
|
||||
from django.conf import settings
|
||||
from core.models import CodeSignToken
|
||||
|
||||
|
||||
def get_exegen_url() -> str:
|
||||
@@ -20,18 +21,20 @@ def get_exegen_url() -> str:
|
||||
|
||||
|
||||
def get_winagent_url(arch: str) -> str:
|
||||
from core.models import CodeSignToken
|
||||
|
||||
dl_url = settings.DL_32 if arch == "32" else settings.DL_64
|
||||
|
||||
try:
|
||||
codetoken = CodeSignToken.objects.first().token
|
||||
base_url = get_exegen_url() + "/api/v1/winagents/?"
|
||||
params = {
|
||||
"version": settings.LATEST_AGENT_VER,
|
||||
"arch": arch,
|
||||
"token": codetoken,
|
||||
}
|
||||
dl_url = base_url + urllib.parse.urlencode(params)
|
||||
t: CodeSignToken = CodeSignToken.objects.first() # type: ignore
|
||||
if t.is_valid:
|
||||
base_url = get_exegen_url() + "/api/v1/winagents/?"
|
||||
params = {
|
||||
"version": settings.LATEST_AGENT_VER,
|
||||
"arch": arch,
|
||||
"token": t.token,
|
||||
}
|
||||
dl_url = base_url + urllib.parse.urlencode(params)
|
||||
except:
|
||||
dl_url = settings.DL_64 if arch == "64" else settings.DL_32
|
||||
pass
|
||||
|
||||
return dl_url
|
||||
|
||||
@@ -8,53 +8,242 @@ import time
|
||||
from django.conf import settings
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.db.models import Q
|
||||
from packaging import version as pyver
|
||||
from rest_framework import status
|
||||
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 rest_framework.exceptions import PermissionDenied
|
||||
|
||||
from core.models import CoreSettings
|
||||
from logs.models import AuditLog, DebugLog, PendingAction
|
||||
from scripts.models import Script
|
||||
from scripts.tasks import handle_bulk_command_task, handle_bulk_script_task
|
||||
from tacticalrmm.utils import get_default_timezone, notify_error, reload_nats
|
||||
from tacticalrmm.utils import (
|
||||
get_default_timezone,
|
||||
notify_error,
|
||||
reload_nats,
|
||||
AGENT_DEFER,
|
||||
)
|
||||
from winupdate.serializers import WinUpdatePolicySerializer
|
||||
from winupdate.tasks import bulk_check_for_updates_task, bulk_install_updates_task
|
||||
from tacticalrmm.permissions import (
|
||||
_has_perm_on_agent,
|
||||
_has_perm_on_client,
|
||||
_has_perm_on_site,
|
||||
)
|
||||
|
||||
from .models import Agent, AgentCustomField, Note, RecoveryAction, AgentHistory
|
||||
from .permissions import (
|
||||
EditAgentPerms,
|
||||
AgentHistoryPerms,
|
||||
AgentPerms,
|
||||
EvtLogPerms,
|
||||
InstallAgentPerms,
|
||||
ManageNotesPerms,
|
||||
RecoverAgentPerms,
|
||||
AgentNotesPerms,
|
||||
ManageProcPerms,
|
||||
MeshPerms,
|
||||
RebootAgentPerms,
|
||||
RunBulkPerms,
|
||||
RunScriptPerms,
|
||||
SendCMDPerms,
|
||||
UninstallPerms,
|
||||
PingAgentPerms,
|
||||
UpdateAgentPerms,
|
||||
)
|
||||
from .serializers import (
|
||||
AgentCustomFieldSerializer,
|
||||
AgentEditSerializer,
|
||||
AgentHistorySerializer,
|
||||
AgentHostnameSerializer,
|
||||
AgentOverdueActionSerializer,
|
||||
AgentSerializer,
|
||||
AgentTableSerializer,
|
||||
NoteSerializer,
|
||||
NotesSerializer,
|
||||
AgentNoteSerializer,
|
||||
)
|
||||
from .tasks import run_script_email_results_task, send_agent_update_task
|
||||
|
||||
|
||||
@api_view()
|
||||
class GetAgents(APIView):
|
||||
permission_classes = [IsAuthenticated, AgentPerms]
|
||||
|
||||
def get(self, request):
|
||||
if "site" in request.query_params.keys():
|
||||
filter = Q(site_id=request.query_params["site"])
|
||||
elif "client" in request.query_params.keys():
|
||||
filter = Q(site__client_id=request.query_params["client"])
|
||||
else:
|
||||
filter = Q()
|
||||
|
||||
# by default detail=true
|
||||
if (
|
||||
"detail" not in request.query_params.keys()
|
||||
or "detail" in request.query_params.keys()
|
||||
and request.query_params["detail"] == "true"
|
||||
):
|
||||
|
||||
agents = (
|
||||
Agent.objects.filter_by_role(request.user) # type: ignore
|
||||
.select_related("site", "policy", "alert_template")
|
||||
.prefetch_related("agentchecks")
|
||||
.filter(filter)
|
||||
.defer(*AGENT_DEFER)
|
||||
)
|
||||
ctx = {"default_tz": get_default_timezone()}
|
||||
serializer = AgentTableSerializer(agents, many=True, context=ctx)
|
||||
|
||||
# if detail=false
|
||||
else:
|
||||
agents = (
|
||||
Agent.objects.filter_by_role(request.user) # type: ignore
|
||||
.select_related("site")
|
||||
.filter(filter)
|
||||
.only("agent_id", "hostname", "site")
|
||||
)
|
||||
serializer = AgentHostnameSerializer(agents, many=True)
|
||||
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class GetUpdateDeleteAgent(APIView):
|
||||
permission_classes = [IsAuthenticated, AgentPerms]
|
||||
|
||||
# get agent details
|
||||
def get(self, request, agent_id):
|
||||
agent = get_object_or_404(Agent, agent_id=agent_id)
|
||||
return Response(AgentSerializer(agent).data)
|
||||
|
||||
# edit agent
|
||||
def put(self, request, agent_id):
|
||||
agent = get_object_or_404(Agent, agent_id=agent_id)
|
||||
|
||||
a_serializer = AgentSerializer(instance=agent, data=request.data, partial=True)
|
||||
a_serializer.is_valid(raise_exception=True)
|
||||
a_serializer.save()
|
||||
|
||||
if "winupdatepolicy" in request.data.keys():
|
||||
policy = agent.winupdatepolicy.get() # type: ignore
|
||||
p_serializer = WinUpdatePolicySerializer(
|
||||
instance=policy, data=request.data["winupdatepolicy"][0]
|
||||
)
|
||||
p_serializer.is_valid(raise_exception=True)
|
||||
p_serializer.save()
|
||||
|
||||
if "custom_fields" in request.data.keys():
|
||||
|
||||
for field in request.data["custom_fields"]:
|
||||
|
||||
custom_field = field
|
||||
custom_field["agent"] = agent.id # type: ignore
|
||||
|
||||
if AgentCustomField.objects.filter(
|
||||
field=field["field"], agent=agent.id # type: ignore
|
||||
):
|
||||
value = AgentCustomField.objects.get(
|
||||
field=field["field"], agent=agent.id # type: ignore
|
||||
)
|
||||
serializer = AgentCustomFieldSerializer(
|
||||
instance=value, data=custom_field
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
else:
|
||||
serializer = AgentCustomFieldSerializer(data=custom_field)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
|
||||
return Response("The agent was updated successfully")
|
||||
|
||||
# uninstall agent
|
||||
def delete(self, request, agent_id):
|
||||
agent = get_object_or_404(Agent, agent_id=agent_id)
|
||||
asyncio.run(agent.nats_cmd({"func": "uninstall"}, wait=False))
|
||||
name = agent.hostname
|
||||
agent.delete()
|
||||
reload_nats()
|
||||
return Response(f"{name} will now be uninstalled.")
|
||||
|
||||
|
||||
class AgentProcesses(APIView):
|
||||
permission_classes = [IsAuthenticated, ManageProcPerms]
|
||||
|
||||
# list agent processes
|
||||
def get(self, request, agent_id):
|
||||
agent = get_object_or_404(Agent, agent_id=agent_id)
|
||||
r = asyncio.run(agent.nats_cmd(data={"func": "procs"}, timeout=5))
|
||||
if r == "timeout" or r == "natsdown":
|
||||
return notify_error("Unable to contact the agent")
|
||||
return Response(r)
|
||||
|
||||
# kill agent process
|
||||
def delete(self, request, agent_id, pid):
|
||||
agent = get_object_or_404(Agent, agent_id=agent_id)
|
||||
r = asyncio.run(
|
||||
agent.nats_cmd({"func": "killproc", "procpid": int(pid)}, timeout=15)
|
||||
)
|
||||
|
||||
if r == "timeout" or r == "natsdown":
|
||||
return notify_error("Unable to contact the agent")
|
||||
elif r != "ok":
|
||||
return notify_error(r)
|
||||
|
||||
return Response(f"Process with PID: {pid} was ended successfully")
|
||||
|
||||
|
||||
class AgentMeshCentral(APIView):
|
||||
permission_classes = [IsAuthenticated, MeshPerms]
|
||||
|
||||
# get mesh urls
|
||||
def get(self, request, agent_id):
|
||||
agent = get_object_or_404(Agent, agent_id=agent_id)
|
||||
core = CoreSettings.objects.first()
|
||||
|
||||
token = agent.get_login_token(
|
||||
key=core.mesh_token,
|
||||
user=f"user//{core.mesh_username.lower()}", # type:ignore
|
||||
)
|
||||
|
||||
if token == "err":
|
||||
return notify_error("Invalid mesh token")
|
||||
|
||||
control = f"{core.mesh_site}/?login={token}&gotonode={agent.mesh_node_id}&viewmode=11&hide=31" # type:ignore
|
||||
terminal = f"{core.mesh_site}/?login={token}&gotonode={agent.mesh_node_id}&viewmode=12&hide=31" # type:ignore
|
||||
file = f"{core.mesh_site}/?login={token}&gotonode={agent.mesh_node_id}&viewmode=13&hide=31" # type:ignore
|
||||
|
||||
AuditLog.audit_mesh_session(
|
||||
username=request.user.username,
|
||||
agent=agent,
|
||||
debug_info={"ip": request._client_ip},
|
||||
)
|
||||
|
||||
ret = {
|
||||
"hostname": agent.hostname,
|
||||
"control": control,
|
||||
"terminal": terminal,
|
||||
"file": file,
|
||||
"status": agent.status,
|
||||
"client": agent.client.name,
|
||||
"site": agent.site.name,
|
||||
}
|
||||
return Response(ret)
|
||||
|
||||
# start mesh recovery
|
||||
def post(self, request, agent_id):
|
||||
agent = get_object_or_404(Agent, agent_id=agent_id)
|
||||
data = {"func": "recover", "payload": {"mode": "mesh"}}
|
||||
r = asyncio.run(agent.nats_cmd(data, timeout=90))
|
||||
if r != "ok":
|
||||
return notify_error("Unable to contact the agent")
|
||||
|
||||
return Response(f"Repaired mesh agent on {agent.hostname}")
|
||||
|
||||
|
||||
@api_view(["GET"])
|
||||
@permission_classes([IsAuthenticated, AgentPerms])
|
||||
def get_agent_versions(request):
|
||||
agents = Agent.objects.prefetch_related("site").only("pk", "hostname")
|
||||
agents = (
|
||||
Agent.objects.filter_by_role(request.user)
|
||||
.prefetch_related("site")
|
||||
.only("pk", "hostname")
|
||||
)
|
||||
return Response(
|
||||
{
|
||||
"versions": [settings.LATEST_AGENT_VER],
|
||||
@@ -66,20 +255,24 @@ 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] = [
|
||||
i.pk
|
||||
q = (
|
||||
Agent.objects.filter_by_role(request.user)
|
||||
.filter(agent_id__in=request.data["agent_ids"])
|
||||
.only("agent_id", "version")
|
||||
)
|
||||
agent_ids: list[str] = [
|
||||
i.agent_id
|
||||
for i in q
|
||||
if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER)
|
||||
]
|
||||
send_agent_update_task.delay(pks=pks)
|
||||
send_agent_update_task.delay(agent_ids=agent_ids)
|
||||
return Response("ok")
|
||||
|
||||
|
||||
@api_view()
|
||||
@permission_classes([IsAuthenticated, UninstallPerms])
|
||||
def ping(request, pk):
|
||||
agent = get_object_or_404(Agent, pk=pk)
|
||||
@api_view(["GET"])
|
||||
@permission_classes([IsAuthenticated, PingAgentPerms])
|
||||
def ping(request, agent_id):
|
||||
agent = get_object_or_404(Agent, agent_id=agent_id)
|
||||
status = "offline"
|
||||
attempts = 0
|
||||
while 1:
|
||||
@@ -97,131 +290,12 @@ def ping(request, pk):
|
||||
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()
|
||||
return Response(f"{name} will now be uninstalled.")
|
||||
|
||||
|
||||
@api_view(["PATCH", "PUT"])
|
||||
@permission_classes([IsAuthenticated, EditAgentPerms])
|
||||
def edit_agent(request):
|
||||
agent = get_object_or_404(Agent, pk=request.data["id"])
|
||||
|
||||
a_serializer = AgentEditSerializer(instance=agent, data=request.data, partial=True)
|
||||
a_serializer.is_valid(raise_exception=True)
|
||||
a_serializer.save()
|
||||
|
||||
if "winupdatepolicy" in request.data.keys():
|
||||
policy = agent.winupdatepolicy.get() # type: ignore
|
||||
p_serializer = WinUpdatePolicySerializer(
|
||||
instance=policy, data=request.data["winupdatepolicy"][0]
|
||||
)
|
||||
p_serializer.is_valid(raise_exception=True)
|
||||
p_serializer.save()
|
||||
|
||||
if "custom_fields" in request.data.keys():
|
||||
|
||||
for field in request.data["custom_fields"]:
|
||||
|
||||
custom_field = field
|
||||
custom_field["agent"] = agent.id # type: ignore
|
||||
|
||||
if AgentCustomField.objects.filter(
|
||||
field=field["field"], agent=agent.id # type: ignore
|
||||
):
|
||||
value = AgentCustomField.objects.get(
|
||||
field=field["field"], agent=agent.id # type: ignore
|
||||
)
|
||||
serializer = AgentCustomFieldSerializer(
|
||||
instance=value, data=custom_field
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
else:
|
||||
serializer = AgentCustomFieldSerializer(data=custom_field)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
|
||||
return Response("ok")
|
||||
|
||||
|
||||
@api_view()
|
||||
@permission_classes([IsAuthenticated, MeshPerms])
|
||||
def meshcentral(request, pk):
|
||||
agent = get_object_or_404(Agent, pk=pk)
|
||||
core = CoreSettings.objects.first()
|
||||
|
||||
token = agent.get_login_token(
|
||||
key=core.mesh_token, user=f"user//{core.mesh_username}" # type:ignore
|
||||
)
|
||||
|
||||
if token == "err":
|
||||
return notify_error("Invalid mesh token")
|
||||
|
||||
control = f"{core.mesh_site}/?login={token}&gotonode={agent.mesh_node_id}&viewmode=11&hide=31" # type:ignore
|
||||
terminal = f"{core.mesh_site}/?login={token}&gotonode={agent.mesh_node_id}&viewmode=12&hide=31" # type:ignore
|
||||
file = f"{core.mesh_site}/?login={token}&gotonode={agent.mesh_node_id}&viewmode=13&hide=31" # type:ignore
|
||||
|
||||
AuditLog.audit_mesh_session(
|
||||
username=request.user.username,
|
||||
agent=agent,
|
||||
debug_info={"ip": request._client_ip},
|
||||
)
|
||||
|
||||
ret = {
|
||||
"hostname": agent.hostname,
|
||||
"control": control,
|
||||
"terminal": terminal,
|
||||
"file": file,
|
||||
"status": agent.status,
|
||||
"client": agent.client.name,
|
||||
"site": agent.site.name,
|
||||
}
|
||||
return Response(ret)
|
||||
|
||||
|
||||
@api_view()
|
||||
def agent_detail(request, pk):
|
||||
agent = get_object_or_404(Agent, pk=pk)
|
||||
return Response(AgentSerializer(agent).data)
|
||||
|
||||
|
||||
@api_view()
|
||||
def get_processes(request, pk):
|
||||
agent = get_object_or_404(Agent, pk=pk)
|
||||
r = asyncio.run(agent.nats_cmd(data={"func": "procs"}, timeout=5))
|
||||
if r == "timeout":
|
||||
return notify_error("Unable to contact the agent")
|
||||
return Response(r)
|
||||
|
||||
|
||||
@api_view()
|
||||
@permission_classes([IsAuthenticated, ManageProcPerms])
|
||||
def kill_proc(request, pk, pid):
|
||||
agent = get_object_or_404(Agent, pk=pk)
|
||||
r = asyncio.run(
|
||||
agent.nats_cmd({"func": "killproc", "procpid": int(pid)}, timeout=15)
|
||||
)
|
||||
|
||||
if r == "timeout":
|
||||
return notify_error("Unable to contact the agent")
|
||||
elif r != "ok":
|
||||
return notify_error(r)
|
||||
|
||||
return Response("ok")
|
||||
|
||||
|
||||
@api_view()
|
||||
@api_view(["GET"])
|
||||
@permission_classes([IsAuthenticated, EvtLogPerms])
|
||||
def get_event_log(request, pk, logtype, days):
|
||||
agent = get_object_or_404(Agent, pk=pk)
|
||||
def get_event_log(request, agent_id, logtype, days):
|
||||
agent = get_object_or_404(Agent, agent_id=agent_id)
|
||||
timeout = 180 if logtype == "Security" else 30
|
||||
|
||||
data = {
|
||||
"func": "eventlog",
|
||||
"timeout": timeout,
|
||||
@@ -231,7 +305,7 @@ def get_event_log(request, pk, logtype, days):
|
||||
},
|
||||
}
|
||||
r = asyncio.run(agent.nats_cmd(data, timeout=timeout + 2))
|
||||
if r == "timeout":
|
||||
if r == "timeout" or r == "natsdown":
|
||||
return notify_error("Unable to contact the agent")
|
||||
|
||||
return Response(r)
|
||||
@@ -239,8 +313,8 @@ 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"])
|
||||
def send_raw_cmd(request, agent_id):
|
||||
agent = get_object_or_404(Agent, agent_id=agent_id)
|
||||
timeout = int(request.data["timeout"])
|
||||
data = {
|
||||
"func": "rawcmd",
|
||||
@@ -276,81 +350,11 @@ def send_raw_cmd(request):
|
||||
return Response(r)
|
||||
|
||||
|
||||
class AgentsTableList(APIView):
|
||||
def patch(self, request):
|
||||
if "sitePK" in request.data.keys():
|
||||
queryset = (
|
||||
Agent.objects.select_related("site", "policy", "alert_template")
|
||||
.prefetch_related("agentchecks")
|
||||
.filter(site_id=request.data["sitePK"])
|
||||
)
|
||||
elif "clientPK" in request.data.keys():
|
||||
queryset = (
|
||||
Agent.objects.select_related("site", "policy", "alert_template")
|
||||
.prefetch_related("agentchecks")
|
||||
.filter(site__client_id=request.data["clientPK"])
|
||||
)
|
||||
else:
|
||||
queryset = Agent.objects.select_related(
|
||||
"site", "policy", "alert_template"
|
||||
).prefetch_related("agentchecks")
|
||||
|
||||
queryset = queryset.only(
|
||||
"pk",
|
||||
"hostname",
|
||||
"agent_id",
|
||||
"site",
|
||||
"policy",
|
||||
"alert_template",
|
||||
"monitoring_type",
|
||||
"description",
|
||||
"needs_reboot",
|
||||
"overdue_text_alert",
|
||||
"overdue_email_alert",
|
||||
"overdue_time",
|
||||
"offline_time",
|
||||
"last_seen",
|
||||
"boot_time",
|
||||
"logged_in_username",
|
||||
"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)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
@api_view()
|
||||
def list_agents_no_detail(request):
|
||||
agents = Agent.objects.select_related("site").only("pk", "hostname", "site")
|
||||
return Response(AgentHostnameSerializer(agents, many=True).data)
|
||||
|
||||
|
||||
@api_view()
|
||||
def agent_edit_details(request, pk):
|
||||
agent = get_object_or_404(Agent, pk=pk)
|
||||
return Response(AgentEditSerializer(agent).data)
|
||||
|
||||
|
||||
@api_view(["POST"])
|
||||
def overdue_action(request):
|
||||
agent = get_object_or_404(Agent, pk=request.data["pk"])
|
||||
serializer = AgentOverdueActionSerializer(
|
||||
instance=agent, data=request.data, partial=True
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
return Response(agent.hostname)
|
||||
|
||||
|
||||
class Reboot(APIView):
|
||||
permission_classes = [IsAuthenticated, RebootAgentPerms]
|
||||
# reboot now
|
||||
def post(self, request):
|
||||
agent = get_object_or_404(Agent, pk=request.data["pk"])
|
||||
def post(self, request, agent_id):
|
||||
agent = get_object_or_404(Agent, agent_id=agent_id)
|
||||
r = asyncio.run(agent.nats_cmd({"func": "rebootnow"}, timeout=10))
|
||||
if r != "ok":
|
||||
return notify_error("Unable to contact the agent")
|
||||
@@ -358,8 +362,8 @@ class Reboot(APIView):
|
||||
return Response("ok")
|
||||
|
||||
# reboot later
|
||||
def patch(self, request):
|
||||
agent = get_object_or_404(Agent, pk=request.data["pk"])
|
||||
def patch(self, request, agent_id):
|
||||
agent = get_object_or_404(Agent, agent_id=agent_id)
|
||||
|
||||
try:
|
||||
obj = dt.datetime.strptime(request.data["datetime"], "%Y-%m-%d %H:%M")
|
||||
@@ -412,17 +416,24 @@ def install_agent(request):
|
||||
version = settings.LATEST_AGENT_VER
|
||||
arch = request.data["arch"]
|
||||
|
||||
if not _has_perm_on_site(request.user, site_id):
|
||||
raise PermissionDenied()
|
||||
|
||||
# response type is blob so we have to use
|
||||
# status codes and render error message on the frontend
|
||||
if arch == "64" and not os.path.exists(
|
||||
os.path.join(settings.EXE_DIR, "meshagent.exe")
|
||||
):
|
||||
return Response(status=status.HTTP_406_NOT_ACCEPTABLE)
|
||||
return notify_error(
|
||||
"Missing 64 bit meshagent.exe. Upload it from Settings > Global Settings > MeshCentral"
|
||||
)
|
||||
|
||||
if arch == "32" and not os.path.exists(
|
||||
os.path.join(settings.EXE_DIR, "meshagent-x86.exe")
|
||||
):
|
||||
return Response(status=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE)
|
||||
return notify_error(
|
||||
"Missing 32 bit meshagent.exe. Upload it from Settings > Global Settings > MeshCentral"
|
||||
)
|
||||
|
||||
inno = (
|
||||
f"winagent-v{version}.exe" if arch == "64" else f"winagent-v{version}-x86.exe"
|
||||
@@ -539,8 +550,9 @@ def install_agent(request):
|
||||
|
||||
|
||||
@api_view(["POST"])
|
||||
def recover(request):
|
||||
agent = get_object_or_404(Agent, pk=request.data["pk"])
|
||||
@permission_classes([IsAuthenticated, RecoverAgentPerms])
|
||||
def recover(request, agent_id):
|
||||
agent = get_object_or_404(Agent, agent_id=agent_id)
|
||||
mode = request.data["mode"]
|
||||
|
||||
# attempt a realtime recovery, otherwise fall back to old recovery method
|
||||
@@ -577,8 +589,8 @@ def recover(request):
|
||||
|
||||
@api_view(["POST"])
|
||||
@permission_classes([IsAuthenticated, RunScriptPerms])
|
||||
def run_script(request):
|
||||
agent = get_object_or_404(Agent, pk=request.data["pk"])
|
||||
def run_script(request, agent_id):
|
||||
agent = get_object_or_404(Agent, agent_id=agent_id)
|
||||
script = get_object_or_404(Script, pk=request.data["script"])
|
||||
output = request.data["output"]
|
||||
args = request.data["args"]
|
||||
@@ -671,17 +683,6 @@ def run_script(request):
|
||||
return Response(f"{script.name} will now be run on {agent.hostname}")
|
||||
|
||||
|
||||
@api_view()
|
||||
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=90))
|
||||
if r != "ok":
|
||||
return notify_error("Unable to contact the agent")
|
||||
|
||||
return Response(f"Repaired mesh agent on {agent.hostname}")
|
||||
|
||||
|
||||
@api_view(["POST"])
|
||||
def get_mesh_exe(request, arch):
|
||||
filename = "meshagent.exe" if arch == "64" else "meshagent-x86.exe"
|
||||
@@ -704,34 +705,62 @@ def get_mesh_exe(request, arch):
|
||||
|
||||
|
||||
class GetAddNotes(APIView):
|
||||
def get(self, request, pk):
|
||||
agent = get_object_or_404(Agent, pk=pk)
|
||||
return Response(NotesSerializer(agent).data)
|
||||
permission_classes = [IsAuthenticated, AgentNotesPerms]
|
||||
|
||||
def post(self, request, pk):
|
||||
agent = get_object_or_404(Agent, pk=pk)
|
||||
serializer = NoteSerializer(data=request.data, partial=True)
|
||||
def get(self, request, agent_id=None):
|
||||
if agent_id:
|
||||
agent = get_object_or_404(Agent, agent_id=agent_id)
|
||||
notes = Note.objects.filter(agent=agent)
|
||||
else:
|
||||
notes = Note.objects.filter_by_role(request.user)
|
||||
|
||||
return Response(AgentNoteSerializer(notes, many=True).data)
|
||||
|
||||
def post(self, request):
|
||||
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
|
||||
if not _has_perm_on_agent(request.user, agent.agent_id):
|
||||
raise PermissionDenied()
|
||||
|
||||
data = {
|
||||
"note": request.data["note"],
|
||||
"agent": agent.pk,
|
||||
"user": request.user.pk,
|
||||
}
|
||||
|
||||
serializer = AgentNoteSerializer(data=data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save(agent=agent, user=request.user)
|
||||
serializer.save()
|
||||
return Response("Note added!")
|
||||
|
||||
|
||||
class GetEditDeleteNote(APIView):
|
||||
permission_classes = [IsAuthenticated, ManageNotesPerms]
|
||||
permission_classes = [IsAuthenticated, AgentNotesPerms]
|
||||
|
||||
def get(self, request, pk):
|
||||
note = get_object_or_404(Note, pk=pk)
|
||||
return Response(NoteSerializer(note).data)
|
||||
|
||||
def patch(self, request, pk):
|
||||
if not _has_perm_on_agent(request.user, note.agent.agent_id):
|
||||
raise PermissionDenied()
|
||||
|
||||
return Response(AgentNoteSerializer(note).data)
|
||||
|
||||
def put(self, request, pk):
|
||||
note = get_object_or_404(Note, pk=pk)
|
||||
serializer = NoteSerializer(instance=note, data=request.data, partial=True)
|
||||
|
||||
if not _has_perm_on_agent(request.user, note.agent.agent_id):
|
||||
raise PermissionDenied()
|
||||
|
||||
serializer = AgentNoteSerializer(instance=note, data=request.data, partial=True)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
return Response("Note edited!")
|
||||
|
||||
def delete(self, request, pk):
|
||||
note = get_object_or_404(Note, pk=pk)
|
||||
|
||||
if not _has_perm_on_agent(request.user, note.agent.agent_id):
|
||||
raise PermissionDenied()
|
||||
|
||||
note.delete()
|
||||
return Response("Note was deleted!")
|
||||
|
||||
@@ -743,13 +772,27 @@ def bulk(request):
|
||||
return notify_error("Must select at least 1 agent")
|
||||
|
||||
if request.data["target"] == "client":
|
||||
q = Agent.objects.filter(site__client_id=request.data["client"])
|
||||
if not _has_perm_on_client(request.user, request.data["client"]):
|
||||
raise PermissionDenied()
|
||||
q = Agent.objects.filter_by_role(request.user).filter(
|
||||
site__client_id=request.data["client"]
|
||||
)
|
||||
|
||||
elif request.data["target"] == "site":
|
||||
q = Agent.objects.filter(site_id=request.data["site"])
|
||||
if not _has_perm_on_site(request.user, request.data["site"]):
|
||||
raise PermissionDenied()
|
||||
q = Agent.objects.filter_by_role(request.user).filter(
|
||||
site_id=request.data["site"]
|
||||
)
|
||||
|
||||
elif request.data["target"] == "agents":
|
||||
q = Agent.objects.filter(pk__in=request.data["agents"])
|
||||
q = Agent.objects.filter_by_role(request.user).filter(
|
||||
agent_id__in=request.data["agents"]
|
||||
)
|
||||
|
||||
elif request.data["target"] == "all":
|
||||
q = Agent.objects.only("pk", "monitoring_type")
|
||||
q = Agent.objects.filter_by_role(request.user).only("pk", "monitoring_type")
|
||||
|
||||
else:
|
||||
return notify_error("Something went wrong")
|
||||
|
||||
@@ -807,40 +850,60 @@ def bulk(request):
|
||||
|
||||
|
||||
@api_view(["POST"])
|
||||
@permission_classes([IsAuthenticated, AgentPerms])
|
||||
def agent_maintenance(request):
|
||||
|
||||
if request.data["type"] == "Client":
|
||||
Agent.objects.filter(site__client_id=request.data["id"]).update(
|
||||
maintenance_mode=request.data["action"]
|
||||
if not _has_perm_on_client(request.user, request.data["id"]):
|
||||
raise PermissionDenied()
|
||||
|
||||
count = (
|
||||
Agent.objects.filter_by_role(request.user)
|
||||
.filter(site__client_id=request.data["id"])
|
||||
.update(maintenance_mode=request.data["action"])
|
||||
)
|
||||
|
||||
elif request.data["type"] == "Site":
|
||||
Agent.objects.filter(site_id=request.data["id"]).update(
|
||||
maintenance_mode=request.data["action"]
|
||||
)
|
||||
if not _has_perm_on_site(request.user, request.data["id"]):
|
||||
raise PermissionDenied()
|
||||
|
||||
elif request.data["type"] == "Agent":
|
||||
agent = Agent.objects.get(pk=request.data["id"])
|
||||
agent.maintenance_mode = request.data["action"]
|
||||
agent.save(update_fields=["maintenance_mode"])
|
||||
count = (
|
||||
Agent.objects.filter_by_role(request.user)
|
||||
.filter(site_id=request.data["id"])
|
||||
.update(maintenance_mode=request.data["action"])
|
||||
)
|
||||
|
||||
else:
|
||||
return notify_error("Invalid data")
|
||||
|
||||
return Response("ok")
|
||||
if count:
|
||||
action = "disabled" if not request.data["action"] else "enabled"
|
||||
return Response(f"Maintenance mode has been {action} on {count} agents")
|
||||
else:
|
||||
return Response(
|
||||
f"No agents have been put in maintenance mode. You might not have permissions to the resources."
|
||||
)
|
||||
|
||||
|
||||
class WMI(APIView):
|
||||
def get(self, request, pk):
|
||||
agent = get_object_or_404(Agent, pk=pk)
|
||||
permission_classes = [IsAuthenticated, AgentPerms]
|
||||
|
||||
def post(self, request, agent_id):
|
||||
agent = get_object_or_404(Agent, agent_id=agent_id)
|
||||
r = asyncio.run(agent.nats_cmd({"func": "sysinfo"}, timeout=20))
|
||||
if r != "ok":
|
||||
return notify_error("Unable to contact the agent")
|
||||
return Response("ok")
|
||||
return Response("Agent WMI data refreshed successfully")
|
||||
|
||||
|
||||
class AgentHistoryView(APIView):
|
||||
def get(self, request, pk):
|
||||
agent = get_object_or_404(Agent, pk=pk)
|
||||
history = AgentHistory.objects.filter(agent=agent)
|
||||
permission_classes = [IsAuthenticated, AgentHistoryPerms]
|
||||
|
||||
return Response(AgentHistorySerializer(history, many=True).data)
|
||||
def get(self, request, agent_id=None):
|
||||
if agent_id:
|
||||
agent = get_object_or_404(Agent, agent_id=agent_id)
|
||||
history = AgentHistory.objects.filter(agent=agent)
|
||||
else:
|
||||
history = AgentHistory.objects.filter_by_role(request.user)
|
||||
ctx = {"default_tz": get_default_timezone()}
|
||||
return Response(AgentHistorySerializer(history, many=True, context=ctx).data)
|
||||
|
||||
23
api/tacticalrmm/alerts/migrations/0010_auto_20210917_1954.py
Normal file
23
api/tacticalrmm/alerts/migrations/0010_auto_20210917_1954.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.2.6 on 2021-09-17 19:54
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("alerts", "0009_auto_20210721_1810"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="alerttemplate",
|
||||
name="created_by",
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="alerttemplate",
|
||||
name="modified_by",
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
]
|
||||
@@ -9,6 +9,7 @@ from django.db.models.fields import BooleanField, PositiveIntegerField
|
||||
from django.utils import timezone as djangotime
|
||||
|
||||
from logs.models import BaseAuditModel, DebugLog
|
||||
from tacticalrmm.models import PermissionQuerySet
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from agents.models import Agent
|
||||
@@ -31,6 +32,8 @@ ALERT_TYPE_CHOICES = [
|
||||
|
||||
|
||||
class Alert(models.Model):
|
||||
objects = PermissionQuerySet.as_manager()
|
||||
|
||||
agent = models.ForeignKey(
|
||||
"agents.Agent",
|
||||
related_name="agent",
|
||||
@@ -461,7 +464,7 @@ class Alert(models.Model):
|
||||
try:
|
||||
temp_args.append(re.sub("\\{\\{.*\\}\\}", value, arg)) # type: ignore
|
||||
except Exception as e:
|
||||
DebugLog.error(log_type="scripting", message=e)
|
||||
DebugLog.error(log_type="scripting", message=str(e))
|
||||
continue
|
||||
|
||||
else:
|
||||
|
||||
@@ -1,11 +1,55 @@
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework import permissions
|
||||
|
||||
from tacticalrmm.permissions import _has_perm
|
||||
from tacticalrmm.permissions import _has_perm, _has_perm_on_agent
|
||||
|
||||
|
||||
class ManageAlertsPerms(permissions.BasePermission):
|
||||
def _has_perm_on_alert(user, id: int):
|
||||
from alerts.models import Alert
|
||||
|
||||
role = user.role
|
||||
if user.is_superuser or (role and getattr(role, "is_superuser")):
|
||||
return True
|
||||
|
||||
# make sure non-superusers with empty roles aren't permitted
|
||||
elif not role:
|
||||
return False
|
||||
|
||||
alert = get_object_or_404(Alert, id=id)
|
||||
|
||||
if alert.agent:
|
||||
agent_id = alert.agent.agent_id
|
||||
elif alert.assigned_check:
|
||||
agent_id = alert.assigned_check.agent.agent_id
|
||||
elif alert.assigned_task:
|
||||
agent_id = alert.assigned_task.agent.agent_id
|
||||
else:
|
||||
return True
|
||||
|
||||
return _has_perm_on_agent(user, agent_id)
|
||||
|
||||
|
||||
class AlertPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
if r.method == "GET" or r.method == "PATCH":
|
||||
return True
|
||||
if "pk" in view.kwargs.keys():
|
||||
return _has_perm(r, "can_list_alerts") and _has_perm_on_alert(
|
||||
r.user, view.kwargs["pk"]
|
||||
)
|
||||
else:
|
||||
return _has_perm(r, "can_list_alerts")
|
||||
else:
|
||||
if "pk" in view.kwargs.keys():
|
||||
return _has_perm(r, "can_manage_alerts") and _has_perm_on_alert(
|
||||
r.user, view.kwargs["pk"]
|
||||
)
|
||||
else:
|
||||
return _has_perm(r, "can_manage_alerts")
|
||||
|
||||
return _has_perm(r, "can_manage_alerts")
|
||||
|
||||
class AlertTemplatePerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
if r.method == "GET":
|
||||
return _has_perm(r, "can_list_alerttemplates")
|
||||
else:
|
||||
return _has_perm(r, "can_manage_alerttemplates")
|
||||
|
||||
@@ -2,7 +2,7 @@ from rest_framework.fields import SerializerMethodField
|
||||
from rest_framework.serializers import ModelSerializer, ReadOnlyField
|
||||
|
||||
from automation.serializers import PolicySerializer
|
||||
from clients.serializers import ClientSerializer, SiteSerializer
|
||||
from clients.serializers import ClientMinimumSerializer, SiteMinimumSerializer
|
||||
from tacticalrmm.utils import get_default_timezone
|
||||
|
||||
from .models import Alert, AlertTemplate
|
||||
@@ -113,8 +113,8 @@ class AlertTemplateSerializer(ModelSerializer):
|
||||
|
||||
class AlertTemplateRelationSerializer(ModelSerializer):
|
||||
policies = PolicySerializer(read_only=True, many=True)
|
||||
clients = ClientSerializer(read_only=True, many=True)
|
||||
sites = SiteSerializer(read_only=True, many=True)
|
||||
clients = ClientMinimumSerializer(read_only=True, many=True)
|
||||
sites = SiteMinimumSerializer(read_only=True, many=True)
|
||||
|
||||
class Meta:
|
||||
model = AlertTemplate
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from datetime import datetime, timedelta
|
||||
from unittest.mock import patch
|
||||
from itertools import cycle
|
||||
|
||||
from core.models import CoreSettings
|
||||
from django.conf import settings
|
||||
@@ -8,6 +9,7 @@ from model_bakery import baker, seq
|
||||
from tacticalrmm.test import TacticalTestCase
|
||||
|
||||
from alerts.tasks import cache_agents_alert_template
|
||||
from agents.tasks import handle_agents_task
|
||||
|
||||
from .models import Alert, AlertTemplate
|
||||
from .serializers import (
|
||||
@@ -16,6 +18,8 @@ from .serializers import (
|
||||
AlertTemplateSerializer,
|
||||
)
|
||||
|
||||
base_url = "/alerts"
|
||||
|
||||
|
||||
class TestAlertsViews(TacticalTestCase):
|
||||
def setUp(self):
|
||||
@@ -23,7 +27,7 @@ class TestAlertsViews(TacticalTestCase):
|
||||
self.setup_coresettings()
|
||||
|
||||
def test_get_alerts(self):
|
||||
url = "/alerts/alerts/"
|
||||
url = "/alerts/"
|
||||
|
||||
# create check, task, and agent to test each serializer function
|
||||
check = baker.make_recipe("checks.diskspace_check")
|
||||
@@ -116,7 +120,7 @@ class TestAlertsViews(TacticalTestCase):
|
||||
self.check_not_authenticated("patch", url)
|
||||
|
||||
def test_add_alert(self):
|
||||
url = "/alerts/alerts/"
|
||||
url = "/alerts/"
|
||||
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
data = {
|
||||
@@ -133,11 +137,11 @@ class TestAlertsViews(TacticalTestCase):
|
||||
|
||||
def test_get_alert(self):
|
||||
# returns 404 for invalid alert pk
|
||||
resp = self.client.get("/alerts/alerts/500/", format="json")
|
||||
resp = self.client.get("/alerts/500/", format="json")
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
alert = baker.make("alerts.Alert")
|
||||
url = f"/alerts/alerts/{alert.pk}/" # type: ignore
|
||||
url = f"/alerts/{alert.pk}/" # type: ignore
|
||||
|
||||
resp = self.client.get(url, format="json")
|
||||
serializer = AlertSerializer(alert)
|
||||
@@ -149,16 +153,15 @@ class TestAlertsViews(TacticalTestCase):
|
||||
|
||||
def test_update_alert(self):
|
||||
# returns 404 for invalid alert pk
|
||||
resp = self.client.put("/alerts/alerts/500/", format="json")
|
||||
resp = self.client.put("/alerts/500/", format="json")
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
alert = baker.make("alerts.Alert", resolved=False, snoozed=False)
|
||||
|
||||
url = f"/alerts/alerts/{alert.pk}/" # type: ignore
|
||||
url = f"/alerts/{alert.pk}/" # type: ignore
|
||||
|
||||
# test resolving alert
|
||||
data = {
|
||||
"id": alert.pk, # type: ignore
|
||||
"type": "resolve",
|
||||
}
|
||||
resp = self.client.put(url, data, format="json")
|
||||
@@ -167,26 +170,26 @@ class TestAlertsViews(TacticalTestCase):
|
||||
self.assertTrue(Alert.objects.get(pk=alert.pk).resolved_on) # type: ignore
|
||||
|
||||
# test snoozing alert
|
||||
data = {"id": alert.pk, "type": "snooze", "snooze_days": "30"} # type: ignore
|
||||
data = {"type": "snooze", "snooze_days": "30"} # type: ignore
|
||||
resp = self.client.put(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertTrue(Alert.objects.get(pk=alert.pk).snoozed) # type: ignore
|
||||
self.assertTrue(Alert.objects.get(pk=alert.pk).snooze_until) # type: ignore
|
||||
|
||||
# test snoozing alert without snooze_days
|
||||
data = {"id": alert.pk, "type": "snooze"} # type: ignore
|
||||
data = {"type": "snooze"} # type: ignore
|
||||
resp = self.client.put(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
|
||||
# test unsnoozing alert
|
||||
data = {"id": alert.pk, "type": "unsnooze"} # type: ignore
|
||||
data = {"type": "unsnooze"} # type: ignore
|
||||
resp = self.client.put(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertFalse(Alert.objects.get(pk=alert.pk).snoozed) # type: ignore
|
||||
self.assertFalse(Alert.objects.get(pk=alert.pk).snooze_until) # type: ignore
|
||||
|
||||
# test invalid type
|
||||
data = {"id": alert.pk, "type": "invalid"} # type: ignore
|
||||
data = {"type": "invalid"} # type: ignore
|
||||
resp = self.client.put(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
|
||||
@@ -194,13 +197,13 @@ class TestAlertsViews(TacticalTestCase):
|
||||
|
||||
def test_delete_alert(self):
|
||||
# returns 404 for invalid alert pk
|
||||
resp = self.client.put("/alerts/alerts/500/", format="json")
|
||||
resp = self.client.put("/alerts/500/", format="json")
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
alert = baker.make("alerts.Alert")
|
||||
|
||||
# test delete alert
|
||||
url = f"/alerts/alerts/{alert.pk}/" # type: ignore
|
||||
url = f"/alerts/{alert.pk}/" # type: ignore
|
||||
resp = self.client.delete(url, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
@@ -242,7 +245,7 @@ class TestAlertsViews(TacticalTestCase):
|
||||
self.assertTrue(Alert.objects.filter(snoozed=False).exists())
|
||||
|
||||
def test_get_alert_templates(self):
|
||||
url = "/alerts/alerttemplates/"
|
||||
url = "/alerts/templates/"
|
||||
|
||||
alert_templates = baker.make("alerts.AlertTemplate", _quantity=3)
|
||||
resp = self.client.get(url, format="json")
|
||||
@@ -254,7 +257,7 @@ class TestAlertsViews(TacticalTestCase):
|
||||
self.check_not_authenticated("get", url)
|
||||
|
||||
def test_add_alert_template(self):
|
||||
url = "/alerts/alerttemplates/"
|
||||
url = "/alerts/templates/"
|
||||
|
||||
data = {
|
||||
"name": "Test Template",
|
||||
@@ -267,11 +270,11 @@ class TestAlertsViews(TacticalTestCase):
|
||||
|
||||
def test_get_alert_template(self):
|
||||
# returns 404 for invalid alert template pk
|
||||
resp = self.client.get("/alerts/alerttemplates/500/", format="json")
|
||||
resp = self.client.get("/alerts/templates/500/", format="json")
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
alert_template = baker.make("alerts.AlertTemplate")
|
||||
url = f"/alerts/alerttemplates/{alert_template.pk}/" # type: ignore
|
||||
url = f"/alerts/templates/{alert_template.pk}/" # type: ignore
|
||||
|
||||
resp = self.client.get(url, format="json")
|
||||
serializer = AlertTemplateSerializer(alert_template)
|
||||
@@ -283,16 +286,15 @@ class TestAlertsViews(TacticalTestCase):
|
||||
|
||||
def test_update_alert_template(self):
|
||||
# returns 404 for invalid alert pk
|
||||
resp = self.client.put("/alerts/alerttemplates/500/", format="json")
|
||||
resp = self.client.put("/alerts/templates/500/", format="json")
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
alert_template = baker.make("alerts.AlertTemplate")
|
||||
|
||||
url = f"/alerts/alerttemplates/{alert_template.pk}/" # type: ignore
|
||||
url = f"/alerts/templates/{alert_template.pk}/" # type: ignore
|
||||
|
||||
# test data
|
||||
data = {
|
||||
"id": alert_template.pk, # type: ignore
|
||||
"agent_email_on_resolved": True,
|
||||
"agent_text_on_resolved": True,
|
||||
"agent_include_desktops": True,
|
||||
@@ -308,13 +310,13 @@ class TestAlertsViews(TacticalTestCase):
|
||||
|
||||
def test_delete_alert_template(self):
|
||||
# returns 404 for invalid alert pk
|
||||
resp = self.client.put("/alerts/alerttemplates/500/", format="json")
|
||||
resp = self.client.put("/alerts/templates/500/", format="json")
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
alert_template = baker.make("alerts.AlertTemplate")
|
||||
|
||||
# test delete alert
|
||||
url = f"/alerts/alerttemplates/{alert_template.pk}/" # type: ignore
|
||||
url = f"/alerts/templates/{alert_template.pk}/" # type: ignore
|
||||
resp = self.client.delete(url, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
@@ -332,7 +334,7 @@ class TestAlertsViews(TacticalTestCase):
|
||||
core.alert_template = alert_template # type: ignore
|
||||
core.save() # type: ignore
|
||||
|
||||
url = f"/alerts/alerttemplates/{alert_template.pk}/related/" # type: ignore
|
||||
url = f"/alerts/templates/{alert_template.pk}/related/" # type: ignore
|
||||
|
||||
resp = self.client.get(url, format="json")
|
||||
serializer = AlertTemplateRelationSerializer(alert_template)
|
||||
@@ -675,25 +677,14 @@ class TestAlertTasks(TacticalTestCase):
|
||||
url = "/api/v3/checkin/"
|
||||
|
||||
agent_template_text.version = settings.LATEST_AGENT_VER
|
||||
agent_template_text.last_seen = djangotime.now()
|
||||
agent_template_text.save()
|
||||
|
||||
agent_template_email.version = settings.LATEST_AGENT_VER
|
||||
agent_template_email.last_seen = djangotime.now()
|
||||
agent_template_email.save()
|
||||
|
||||
data = {
|
||||
"agent_id": agent_template_text.agent_id,
|
||||
"version": settings.LATEST_AGENT_VER,
|
||||
}
|
||||
|
||||
resp = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
data = {
|
||||
"agent_id": agent_template_email.agent_id,
|
||||
"version": settings.LATEST_AGENT_VER,
|
||||
}
|
||||
|
||||
resp = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
handle_agents_task()
|
||||
|
||||
recovery_sms.assert_called_with(
|
||||
pk=Alert.objects.get(agent=agent_template_text).pk
|
||||
@@ -1364,15 +1355,7 @@ class TestAlertTasks(TacticalTestCase):
|
||||
agent.last_seen = djangotime.now()
|
||||
agent.save()
|
||||
|
||||
url = "/api/v3/checkin/"
|
||||
|
||||
data = {
|
||||
"agent_id": agent.agent_id,
|
||||
"version": settings.LATEST_AGENT_VER,
|
||||
}
|
||||
|
||||
resp = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
handle_agents_task()
|
||||
|
||||
# this is what data should be
|
||||
data = {
|
||||
@@ -1434,3 +1417,155 @@ class TestAlertTasks(TacticalTestCase):
|
||||
prune_resolved_alerts(30)
|
||||
|
||||
self.assertEqual(Alert.objects.count(), 31)
|
||||
|
||||
|
||||
class TestAlertPermissions(TacticalTestCase):
|
||||
def setUp(self):
|
||||
self.setup_coresettings()
|
||||
self.client_setup()
|
||||
|
||||
def test_get_alerts_permissions(self):
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
agent1 = baker.make_recipe("agents.agent")
|
||||
agent2 = baker.make_recipe("agents.agent")
|
||||
agents = [agent, agent1, agent2]
|
||||
checks = baker.make("checks.Check", agent=cycle(agents), _quantity=3)
|
||||
tasks = baker.make("autotasks.AutomatedTask", agent=cycle(agents), _quantity=3)
|
||||
baker.make(
|
||||
"alerts.Alert", alert_type="task", assigned_task=cycle(tasks), _quantity=3
|
||||
)
|
||||
baker.make(
|
||||
"alerts.Alert",
|
||||
alert_type="check",
|
||||
assigned_check=cycle(checks),
|
||||
_quantity=3,
|
||||
)
|
||||
baker.make(
|
||||
"alerts.Alert", alert_type="availability", agent=cycle(agents), _quantity=3
|
||||
)
|
||||
baker.make("alerts.Alert", alert_type="custom", _quantity=4)
|
||||
|
||||
# test super user access
|
||||
r = self.check_authorized_superuser("patch", f"{base_url}/")
|
||||
self.assertEqual(len(r.data), 13) # type: ignore
|
||||
|
||||
user = self.create_user_with_roles([])
|
||||
self.client.force_authenticate(user=user) # type: ignore
|
||||
|
||||
self.check_not_authorized("patch", f"{base_url}/")
|
||||
|
||||
# add list software role to user
|
||||
user.role.can_list_alerts = True
|
||||
user.role.save()
|
||||
|
||||
r = self.check_authorized("patch", f"{base_url}/")
|
||||
self.assertEqual(len(r.data), 13) # type: ignore
|
||||
|
||||
# test limiting to client
|
||||
user.role.can_view_clients.set([agent.client])
|
||||
r = self.check_authorized("patch", f"{base_url}/")
|
||||
self.assertEqual(len(r.data), 7) # type: ignore
|
||||
|
||||
# test limiting to site
|
||||
user.role.can_view_clients.clear()
|
||||
user.role.can_view_sites.set([agent1.site])
|
||||
r = self.client.patch(f"{base_url}/")
|
||||
self.assertEqual(len(r.data), 7) # type: ignore
|
||||
|
||||
# test limiting to site and client
|
||||
user.role.can_view_clients.set([agent2.client])
|
||||
r = self.client.patch(f"{base_url}/")
|
||||
self.assertEqual(len(r.data), 10) # type: ignore
|
||||
|
||||
@patch("alerts.models.Alert.delete", return_value=1)
|
||||
def test_edit_delete_get_alert_permissions(self, delete):
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
agent1 = baker.make_recipe("agents.agent")
|
||||
agent2 = baker.make_recipe("agents.agent")
|
||||
agents = [agent, agent1, agent2]
|
||||
checks = baker.make("checks.Check", agent=cycle(agents), _quantity=3)
|
||||
tasks = baker.make("autotasks.AutomatedTask", agent=cycle(agents), _quantity=3)
|
||||
alert_tasks = baker.make(
|
||||
"alerts.Alert", alert_type="task", assigned_task=cycle(tasks), _quantity=3
|
||||
)
|
||||
alert_checks = baker.make(
|
||||
"alerts.Alert",
|
||||
alert_type="check",
|
||||
assigned_check=cycle(checks),
|
||||
_quantity=3,
|
||||
)
|
||||
alert_agents = baker.make(
|
||||
"alerts.Alert", alert_type="availability", agent=cycle(agents), _quantity=3
|
||||
)
|
||||
alert_custom = baker.make("alerts.Alert", alert_type="custom", _quantity=4)
|
||||
|
||||
# alert task url
|
||||
task_url = f"{base_url}/{alert_tasks[0].id}/" # for agent
|
||||
unauthorized_task_url = f"{base_url}/{alert_tasks[1].id}/" # for agent1
|
||||
# alert check url
|
||||
check_url = f"{base_url}/{alert_checks[0].id}/" # for agent
|
||||
unauthorized_check_url = f"{base_url}/{alert_checks[1].id}/" # for agent1
|
||||
# alert agent url
|
||||
agent_url = f"{base_url}/{alert_agents[0].id}/" # for agent
|
||||
unauthorized_agent_url = f"{base_url}/{alert_agents[1].id}/" # for agent1
|
||||
# custom alert url
|
||||
custom_url = f"{base_url}/{alert_custom[0].id}/" # no agent associated
|
||||
|
||||
authorized_urls = [task_url, check_url, agent_url, custom_url]
|
||||
unauthorized_urls = [
|
||||
unauthorized_agent_url,
|
||||
unauthorized_check_url,
|
||||
unauthorized_task_url,
|
||||
]
|
||||
|
||||
for method in ["get", "put", "delete"]:
|
||||
|
||||
# test superuser access
|
||||
for url in authorized_urls:
|
||||
self.check_authorized_superuser(method, url)
|
||||
|
||||
for url in unauthorized_urls:
|
||||
self.check_authorized_superuser(method, url)
|
||||
|
||||
user = self.create_user_with_roles([])
|
||||
self.client.force_authenticate(user=user) # type: ignore
|
||||
|
||||
# test user without role
|
||||
for url in authorized_urls:
|
||||
self.check_not_authorized(method, url)
|
||||
|
||||
for url in unauthorized_urls:
|
||||
self.check_not_authorized(method, url)
|
||||
|
||||
# add user to role and test
|
||||
setattr(
|
||||
user.role,
|
||||
"can_list_alerts" if method == "get" else "can_manage_alerts",
|
||||
True,
|
||||
)
|
||||
user.role.save()
|
||||
|
||||
# test user with role
|
||||
for url in authorized_urls:
|
||||
self.check_authorized(method, url)
|
||||
|
||||
for url in unauthorized_urls:
|
||||
self.check_authorized(method, url)
|
||||
|
||||
# limit user to client if agent check
|
||||
user.role.can_view_clients.set([agent.client])
|
||||
|
||||
for url in authorized_urls:
|
||||
self.check_authorized(method, url)
|
||||
|
||||
for url in unauthorized_urls:
|
||||
self.check_not_authorized(method, url)
|
||||
|
||||
# limit user to client if agent check
|
||||
user.role.can_view_sites.set([agent1.site])
|
||||
|
||||
for url in authorized_urls:
|
||||
self.check_authorized(method, url)
|
||||
|
||||
for url in unauthorized_urls:
|
||||
self.check_authorized(method, url)
|
||||
|
||||
@@ -3,10 +3,10 @@ from django.urls import path
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path("alerts/", views.GetAddAlerts.as_view()),
|
||||
path("", views.GetAddAlerts.as_view()),
|
||||
path("<int:pk>/", views.GetUpdateDeleteAlert.as_view()),
|
||||
path("bulk/", views.BulkAlerts.as_view()),
|
||||
path("alerts/<int:pk>/", views.GetUpdateDeleteAlert.as_view()),
|
||||
path("alerttemplates/", views.GetAddAlertTemplates.as_view()),
|
||||
path("alerttemplates/<int:pk>/", views.GetUpdateDeleteAlertTemplate.as_view()),
|
||||
path("alerttemplates/<int:pk>/related/", views.RelatedAlertTemplate.as_view()),
|
||||
path("templates/", views.GetAddAlertTemplates.as_view()),
|
||||
path("templates/<int:pk>/", views.GetUpdateDeleteAlertTemplate.as_view()),
|
||||
path("templates/<int:pk>/related/", views.RelatedAlertTemplate.as_view()),
|
||||
]
|
||||
|
||||
@@ -10,7 +10,7 @@ from rest_framework.views import APIView
|
||||
from tacticalrmm.utils import notify_error
|
||||
|
||||
from .models import Alert, AlertTemplate
|
||||
from .permissions import ManageAlertsPerms
|
||||
from .permissions import AlertPerms, AlertTemplatePerms
|
||||
from .serializers import (
|
||||
AlertSerializer,
|
||||
AlertTemplateRelationSerializer,
|
||||
@@ -20,7 +20,7 @@ from .tasks import cache_agents_alert_template
|
||||
|
||||
|
||||
class GetAddAlerts(APIView):
|
||||
permission_classes = [IsAuthenticated, ManageAlertsPerms]
|
||||
permission_classes = [IsAuthenticated, AlertPerms]
|
||||
|
||||
def patch(self, request):
|
||||
|
||||
@@ -92,7 +92,8 @@ class GetAddAlerts(APIView):
|
||||
)
|
||||
|
||||
alerts = (
|
||||
Alert.objects.filter(clientFilter)
|
||||
Alert.objects.filter_by_role(request.user)
|
||||
.filter(clientFilter)
|
||||
.filter(severityFilter)
|
||||
.filter(resolvedFilter)
|
||||
.filter(snoozedFilter)
|
||||
@@ -101,7 +102,7 @@ class GetAddAlerts(APIView):
|
||||
return Response(AlertSerializer(alerts, many=True).data)
|
||||
|
||||
else:
|
||||
alerts = Alert.objects.all()
|
||||
alerts = Alert.objects.filter_by_role(request.user)
|
||||
return Response(AlertSerializer(alerts, many=True).data)
|
||||
|
||||
def post(self, request):
|
||||
@@ -113,11 +114,10 @@ class GetAddAlerts(APIView):
|
||||
|
||||
|
||||
class GetUpdateDeleteAlert(APIView):
|
||||
permission_classes = [IsAuthenticated, ManageAlertsPerms]
|
||||
permission_classes = [IsAuthenticated, AlertPerms]
|
||||
|
||||
def get(self, request, pk):
|
||||
alert = get_object_or_404(Alert, pk=pk)
|
||||
|
||||
return Response(AlertSerializer(alert).data)
|
||||
|
||||
def put(self, request, pk):
|
||||
@@ -169,7 +169,7 @@ class GetUpdateDeleteAlert(APIView):
|
||||
|
||||
|
||||
class BulkAlerts(APIView):
|
||||
permission_classes = [IsAuthenticated, ManageAlertsPerms]
|
||||
permission_classes = [IsAuthenticated, AlertPerms]
|
||||
|
||||
def post(self, request):
|
||||
if request.data["bulk_action"] == "resolve":
|
||||
@@ -193,11 +193,10 @@ class BulkAlerts(APIView):
|
||||
|
||||
|
||||
class GetAddAlertTemplates(APIView):
|
||||
permission_classes = [IsAuthenticated, ManageAlertsPerms]
|
||||
permission_classes = [IsAuthenticated, AlertTemplatePerms]
|
||||
|
||||
def get(self, request):
|
||||
alert_templates = AlertTemplate.objects.all()
|
||||
|
||||
return Response(AlertTemplateSerializer(alert_templates, many=True).data)
|
||||
|
||||
def post(self, request):
|
||||
@@ -212,7 +211,7 @@ class GetAddAlertTemplates(APIView):
|
||||
|
||||
|
||||
class GetUpdateDeleteAlertTemplate(APIView):
|
||||
permission_classes = [IsAuthenticated, ManageAlertsPerms]
|
||||
permission_classes = [IsAuthenticated, AlertTemplatePerms]
|
||||
|
||||
def get(self, request, pk):
|
||||
alert_template = get_object_or_404(AlertTemplate, pk=pk)
|
||||
@@ -243,6 +242,8 @@ class GetUpdateDeleteAlertTemplate(APIView):
|
||||
|
||||
|
||||
class RelatedAlertTemplate(APIView):
|
||||
permission_classes = [IsAuthenticated, AlertTemplatePerms]
|
||||
|
||||
def get(self, request, pk):
|
||||
alert_template = get_object_or_404(AlertTemplate, pk=pk)
|
||||
return Response(AlertTemplateRelationSerializer(alert_template).data)
|
||||
|
||||
@@ -130,42 +130,6 @@ class TestAPIv3(TacticalTestCase):
|
||||
self.assertIsInstance(r.json()["check_interval"], int)
|
||||
self.assertEqual(len(r.json()["checks"]), 15)
|
||||
|
||||
def test_checkin_patch(self):
|
||||
from logs.models import PendingAction
|
||||
|
||||
url = "/api/v3/checkin/"
|
||||
agent_updated = baker.make_recipe("agents.agent", version="1.3.0")
|
||||
PendingAction.objects.create(
|
||||
agent=agent_updated,
|
||||
action_type="agentupdate",
|
||||
details={
|
||||
"url": agent_updated.winagent_dl,
|
||||
"version": agent_updated.version,
|
||||
"inno": agent_updated.win_inno_exe,
|
||||
},
|
||||
)
|
||||
action = agent_updated.pendingactions.filter(action_type="agentupdate").first()
|
||||
self.assertEqual(action.status, "pending")
|
||||
|
||||
# test agent failed to update and still on same version
|
||||
payload = {
|
||||
"func": "hello",
|
||||
"agent_id": agent_updated.agent_id,
|
||||
"version": "1.3.0",
|
||||
}
|
||||
r = self.client.patch(url, payload, format="json")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
action = agent_updated.pendingactions.filter(action_type="agentupdate").first()
|
||||
self.assertEqual(action.status, "pending")
|
||||
|
||||
# test agent successful update
|
||||
payload["version"] = settings.LATEST_AGENT_VER
|
||||
r = self.client.patch(url, payload, format="json")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
action = agent_updated.pendingactions.filter(action_type="agentupdate").first()
|
||||
self.assertEqual(action.status, "completed")
|
||||
action.delete()
|
||||
|
||||
@patch("apiv3.views.reload_nats")
|
||||
def test_agent_recovery(self, reload_nats):
|
||||
reload_nats.return_value = "ok"
|
||||
|
||||
@@ -23,7 +23,7 @@ from checks.serializers import CheckRunnerGetSerializer
|
||||
from checks.utils import bytes2human
|
||||
from logs.models import PendingAction, DebugLog
|
||||
from software.models import InstalledSoftware
|
||||
from tacticalrmm.utils import SoftwareList, filter_software, notify_error, reload_nats
|
||||
from tacticalrmm.utils import notify_error, reload_nats
|
||||
from winupdate.models import WinUpdate, WinUpdatePolicy
|
||||
|
||||
|
||||
@@ -32,55 +32,11 @@ class CheckIn(APIView):
|
||||
authentication_classes = [TokenAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def patch(self, request):
|
||||
def put(self, request):
|
||||
"""
|
||||
!!! DEPRECATED AS OF AGENT 1.6.0 !!!
|
||||
!!! DEPRECATED AS OF AGENT 1.7.0 !!!
|
||||
Endpoint be removed in a future release
|
||||
"""
|
||||
from alerts.models import Alert
|
||||
|
||||
updated = False
|
||||
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
|
||||
if pyver.parse(request.data["version"]) > pyver.parse(
|
||||
agent.version
|
||||
) or pyver.parse(request.data["version"]) == pyver.parse(
|
||||
settings.LATEST_AGENT_VER
|
||||
):
|
||||
updated = True
|
||||
agent.version = request.data["version"]
|
||||
agent.last_seen = djangotime.now()
|
||||
agent.save(update_fields=["version", "last_seen"])
|
||||
|
||||
# change agent update pending status to completed if agent has just updated
|
||||
if (
|
||||
updated
|
||||
and agent.pendingactions.filter( # type: ignore
|
||||
action_type="agentupdate", status="pending"
|
||||
).exists()
|
||||
):
|
||||
agent.pendingactions.filter( # type: ignore
|
||||
action_type="agentupdate", status="pending"
|
||||
).update(status="completed")
|
||||
|
||||
# handles any alerting actions
|
||||
if Alert.objects.filter(agent=agent, resolved=False).exists():
|
||||
Alert.handle_alert_resolve(agent)
|
||||
|
||||
# sync scheduled tasks
|
||||
if agent.autotasks.exclude(sync_status="synced").exists(): # type: ignore
|
||||
tasks = agent.autotasks.exclude(sync_status="synced") # type: ignore
|
||||
|
||||
for task in tasks:
|
||||
if task.sync_status == "pendingdeletion":
|
||||
task.delete_task_on_agent()
|
||||
elif task.sync_status == "initial":
|
||||
task.modify_task_on_agent()
|
||||
elif task.sync_status == "notsynced":
|
||||
task.create_task_on_agent()
|
||||
|
||||
return Response("ok")
|
||||
|
||||
def put(self, request):
|
||||
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
|
||||
serializer = WinAgentSerializer(instance=agent, data=request.data, partial=True)
|
||||
|
||||
@@ -109,11 +65,8 @@ class CheckIn(APIView):
|
||||
return Response("ok")
|
||||
|
||||
if request.data["func"] == "software":
|
||||
raw: SoftwareList = request.data["software"]
|
||||
if not isinstance(raw, list):
|
||||
return notify_error("err")
|
||||
sw = request.data["software"]
|
||||
|
||||
sw = filter_software(raw)
|
||||
if not InstalledSoftware.objects.filter(agent=agent).exists():
|
||||
InstalledSoftware(agent=agent, software=sw).save()
|
||||
else:
|
||||
@@ -371,6 +324,13 @@ class TaskRunner(APIView):
|
||||
serializer.is_valid(raise_exception=True)
|
||||
new_task = serializer.save(last_run=djangotime.now())
|
||||
|
||||
AgentHistory.objects.create(
|
||||
agent=agent,
|
||||
type="task_run",
|
||||
script=task.script,
|
||||
script_results=request.data,
|
||||
)
|
||||
|
||||
# check if task is a collector and update the custom field
|
||||
if task.custom_field:
|
||||
if not task.stderr:
|
||||
@@ -500,11 +460,7 @@ class Software(APIView):
|
||||
|
||||
def post(self, request):
|
||||
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
|
||||
raw: SoftwareList = request.data["software"]
|
||||
if not isinstance(raw, list):
|
||||
return notify_error("err")
|
||||
|
||||
sw = filter_software(raw)
|
||||
sw = request.data["software"]
|
||||
if not InstalledSoftware.objects.filter(agent=agent).exists():
|
||||
InstalledSoftware(agent=agent, software=sw).save()
|
||||
else:
|
||||
@@ -570,7 +526,18 @@ class AgentRecovery(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request, agentid):
|
||||
agent = get_object_or_404(Agent, agent_id=agentid)
|
||||
agent = get_object_or_404(
|
||||
Agent.objects.prefetch_related("recoveryactions").only(
|
||||
"pk", "agent_id", "last_seen"
|
||||
),
|
||||
agent_id=agentid,
|
||||
)
|
||||
|
||||
# TODO remove these 2 lines after agent v1.7.0 has been out for a while
|
||||
# this is handled now by nats-api service
|
||||
agent.last_seen = djangotime.now()
|
||||
agent.save(update_fields=["last_seen"])
|
||||
|
||||
recovery = agent.recoveryactions.filter(last_run=None).last() # type: ignore
|
||||
ret = {"mode": "pass", "shellcmd": ""}
|
||||
if recovery is None:
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.2.6 on 2021-09-17 19:54
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("automation", "0008_auto_20210302_0415"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="policy",
|
||||
name="created_by",
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="policy",
|
||||
name="modified_by",
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
]
|
||||
@@ -6,6 +6,6 @@ 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")
|
||||
return _has_perm(r, "can_list_automation_policies")
|
||||
else:
|
||||
return _has_perm(r, "can_manage_automation_policies")
|
||||
|
||||
@@ -8,7 +8,7 @@ from agents.serializers import AgentHostnameSerializer
|
||||
from autotasks.models import AutomatedTask
|
||||
from checks.models import Check
|
||||
from clients.models import Client
|
||||
from clients.serializers import ClientSerializer, SiteSerializer
|
||||
from clients.serializers import ClientMinimumSerializer, SiteMinimumSerializer
|
||||
from winupdate.serializers import WinUpdatePolicySerializer
|
||||
|
||||
from .models import Policy
|
||||
@@ -21,25 +21,70 @@ class PolicySerializer(ModelSerializer):
|
||||
|
||||
|
||||
class PolicyTableSerializer(ModelSerializer):
|
||||
|
||||
default_server_policy = ReadOnlyField(source="is_default_server_policy")
|
||||
default_workstation_policy = ReadOnlyField(source="is_default_workstation_policy")
|
||||
agents_count = SerializerMethodField(read_only=True)
|
||||
winupdatepolicy = WinUpdatePolicySerializer(many=True, read_only=True)
|
||||
alert_template = ReadOnlyField(source="alert_template.id")
|
||||
excluded_clients = ClientSerializer(many=True)
|
||||
excluded_sites = SiteSerializer(many=True)
|
||||
excluded_agents = AgentHostnameSerializer(many=True)
|
||||
|
||||
class Meta:
|
||||
model = Policy
|
||||
fields = "__all__"
|
||||
depth = 1
|
||||
|
||||
def get_agents_count(self, policy):
|
||||
return policy.related_agents().count()
|
||||
|
||||
|
||||
class PolicyRelatedSerializer(ModelSerializer):
|
||||
workstation_clients = SerializerMethodField()
|
||||
server_clients = SerializerMethodField()
|
||||
workstation_sites = SerializerMethodField()
|
||||
server_sites = SerializerMethodField()
|
||||
agents = SerializerMethodField()
|
||||
|
||||
def get_agents(self, policy):
|
||||
return AgentHostnameSerializer(
|
||||
policy.agents.filter_by_role(self.context["user"]).only(
|
||||
"agent_id", "hostname"
|
||||
),
|
||||
many=True,
|
||||
).data
|
||||
|
||||
def get_workstation_clients(self, policy):
|
||||
return ClientMinimumSerializer(
|
||||
policy.workstation_clients.filter_by_role(self.context["user"]), many=True
|
||||
).data
|
||||
|
||||
def get_server_clients(self, policy):
|
||||
return ClientMinimumSerializer(
|
||||
policy.server_clients.filter_by_role(self.context["user"]), many=True
|
||||
).data
|
||||
|
||||
def get_workstation_sites(self, policy):
|
||||
return SiteMinimumSerializer(
|
||||
policy.workstation_sites.filter_by_role(self.context["user"]), many=True
|
||||
).data
|
||||
|
||||
def get_server_sites(self, policy):
|
||||
return SiteMinimumSerializer(
|
||||
policy.server_sites.filter_by_role(self.context["user"]), many=True
|
||||
).data
|
||||
|
||||
class Meta:
|
||||
model = Policy
|
||||
fields = (
|
||||
"pk",
|
||||
"name",
|
||||
"workstation_clients",
|
||||
"workstation_sites",
|
||||
"server_clients",
|
||||
"server_sites",
|
||||
"agents",
|
||||
"is_default_server_policy",
|
||||
"is_default_workstation_policy",
|
||||
)
|
||||
|
||||
|
||||
class PolicyOverviewSerializer(ModelSerializer):
|
||||
class Meta:
|
||||
model = Client
|
||||
@@ -48,7 +93,6 @@ class PolicyOverviewSerializer(ModelSerializer):
|
||||
|
||||
|
||||
class PolicyCheckStatusSerializer(ModelSerializer):
|
||||
|
||||
hostname = ReadOnlyField(source="agent.hostname")
|
||||
|
||||
class Meta:
|
||||
@@ -57,7 +101,6 @@ class PolicyCheckStatusSerializer(ModelSerializer):
|
||||
|
||||
|
||||
class PolicyTaskStatusSerializer(ModelSerializer):
|
||||
|
||||
hostname = ReadOnlyField(source="agent.hostname")
|
||||
|
||||
class Meta:
|
||||
@@ -65,32 +108,6 @@ class PolicyTaskStatusSerializer(ModelSerializer):
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class PolicyCheckSerializer(ModelSerializer):
|
||||
class Meta:
|
||||
model = Check
|
||||
fields = (
|
||||
"id",
|
||||
"check_type",
|
||||
"readable_desc",
|
||||
"assignedtask",
|
||||
"text_alert",
|
||||
"email_alert",
|
||||
"dashboard_alert",
|
||||
)
|
||||
depth = 1
|
||||
|
||||
|
||||
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
|
||||
fields = "__all__"
|
||||
depth = 1
|
||||
|
||||
|
||||
class PolicyAuditSerializer(ModelSerializer):
|
||||
class Meta:
|
||||
model = Policy
|
||||
|
||||
@@ -8,12 +8,9 @@ from tacticalrmm.test import TacticalTestCase
|
||||
from winupdate.models import WinUpdatePolicy
|
||||
|
||||
from .serializers import (
|
||||
AutoTasksFieldSerializer,
|
||||
PolicyCheckSerializer,
|
||||
PolicyCheckStatusSerializer,
|
||||
PolicyOverviewSerializer,
|
||||
PolicySerializer,
|
||||
PolicyTableSerializer,
|
||||
PolicyTaskStatusSerializer,
|
||||
)
|
||||
|
||||
@@ -26,12 +23,10 @@ class TestPolicyViews(TacticalTestCase):
|
||||
def test_get_all_policies(self):
|
||||
url = "/automation/policies/"
|
||||
|
||||
policies = baker.make("automation.Policy", _quantity=3)
|
||||
baker.make("automation.Policy", _quantity=3)
|
||||
resp = self.client.get(url, format="json")
|
||||
serializer = PolicyTableSerializer(policies, many=True)
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(resp.data, serializer.data) # type: ignore
|
||||
self.assertEqual(len(resp.data), 3)
|
||||
|
||||
self.check_not_authenticated("get", url)
|
||||
|
||||
@@ -181,38 +176,6 @@ class TestPolicyViews(TacticalTestCase):
|
||||
|
||||
self.check_not_authenticated("delete", url)
|
||||
|
||||
def test_get_all_policy_tasks(self):
|
||||
# create policy with tasks
|
||||
policy = baker.make("automation.Policy")
|
||||
tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3)
|
||||
url = f"/automation/{policy.pk}/policyautomatedtasks/" # type: ignore
|
||||
|
||||
resp = self.client.get(url, format="json")
|
||||
serializer = AutoTasksFieldSerializer(tasks, many=True)
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(resp.data, serializer.data) # type: ignore
|
||||
self.assertEqual(len(resp.data), 3) # type: ignore
|
||||
|
||||
self.check_not_authenticated("get", url)
|
||||
|
||||
def test_get_all_policy_checks(self):
|
||||
|
||||
# setup data
|
||||
policy = baker.make("automation.Policy")
|
||||
checks = self.create_checks(policy=policy)
|
||||
|
||||
url = f"/automation/{policy.pk}/policychecks/" # type: ignore
|
||||
|
||||
resp = self.client.get(url, format="json")
|
||||
serializer = PolicyCheckSerializer(checks, many=True)
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(resp.data, serializer.data) # type: ignore
|
||||
self.assertEqual(len(resp.data), 7) # type: ignore
|
||||
|
||||
self.check_not_authenticated("get", url)
|
||||
|
||||
def test_get_policy_check_status(self):
|
||||
# setup data
|
||||
site = baker.make("clients.Site")
|
||||
@@ -225,14 +188,14 @@ class TestPolicyViews(TacticalTestCase):
|
||||
managed_by_policy=True,
|
||||
parent_check=policy_diskcheck.pk,
|
||||
)
|
||||
url = f"/automation/policycheckstatus/{policy_diskcheck.pk}/check/"
|
||||
url = f"/automation/checks/{policy_diskcheck.pk}/status/"
|
||||
|
||||
resp = self.client.patch(url, format="json")
|
||||
resp = self.client.get(url, format="json")
|
||||
serializer = PolicyCheckStatusSerializer([managed_check], many=True)
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(resp.data, serializer.data) # type: ignore
|
||||
self.check_not_authenticated("patch", url)
|
||||
self.check_not_authenticated("get", url)
|
||||
|
||||
def test_policy_overview(self):
|
||||
from clients.models import Client
|
||||
@@ -292,15 +255,15 @@ class TestPolicyViews(TacticalTestCase):
|
||||
"autotasks.AutomatedTask", parent_task=task.id, _quantity=5 # type: ignore
|
||||
)
|
||||
|
||||
url = f"/automation/policyautomatedtaskstatus/{task.id}/task/" # type: ignore
|
||||
url = f"/automation/tasks/{task.id}/status/" # type: ignore
|
||||
|
||||
serializer = PolicyTaskStatusSerializer(policy_tasks, many=True)
|
||||
resp = self.client.patch(url, format="json")
|
||||
resp = self.client.get(url, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(resp.data, serializer.data) # type: ignore
|
||||
self.assertEqual(len(resp.data), 5) # type: ignore
|
||||
|
||||
self.check_not_authenticated("patch", url)
|
||||
self.check_not_authenticated("get", url)
|
||||
|
||||
@patch("automation.tasks.run_win_policy_autotasks_task.delay")
|
||||
def test_run_win_task(self, mock_task):
|
||||
@@ -313,16 +276,16 @@ class TestPolicyViews(TacticalTestCase):
|
||||
_quantity=6,
|
||||
)
|
||||
|
||||
url = "/automation/runwintask/1/"
|
||||
resp = self.client.put(url, format="json")
|
||||
url = "/automation/tasks/1/run/"
|
||||
resp = self.client.post(url, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
mock_task.assert_called() # type: ignore
|
||||
|
||||
self.check_not_authenticated("put", url)
|
||||
self.check_not_authenticated("post", url)
|
||||
|
||||
def test_create_new_patch_policy(self):
|
||||
url = "/automation/winupdatepolicy/"
|
||||
url = "/automation/patchpolicy/"
|
||||
|
||||
# test policy doesn't exist
|
||||
data = {"policy": 500}
|
||||
@@ -353,15 +316,14 @@ class TestPolicyViews(TacticalTestCase):
|
||||
def test_update_patch_policy(self):
|
||||
|
||||
# test policy doesn't exist
|
||||
resp = self.client.put("/automation/winupdatepolicy/500/", format="json")
|
||||
resp = self.client.put("/automation/patchpolicy/500/", format="json")
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
policy = baker.make("automation.Policy")
|
||||
patch_policy = baker.make("winupdate.WinUpdatePolicy", policy=policy)
|
||||
url = f"/automation/winupdatepolicy/{patch_policy.pk}/" # type: ignore
|
||||
url = f"/automation/patchpolicy/{patch_policy.pk}/" # type: ignore
|
||||
|
||||
data = {
|
||||
"id": patch_policy.pk, # type: ignore
|
||||
"policy": policy.pk, # type: ignore
|
||||
"critical": "approve",
|
||||
"important": "approve",
|
||||
@@ -377,7 +339,7 @@ class TestPolicyViews(TacticalTestCase):
|
||||
self.check_not_authenticated("put", url)
|
||||
|
||||
def test_reset_patch_policy(self):
|
||||
url = "/automation/winupdatepolicy/reset/"
|
||||
url = "/automation/patchpolicy/reset/"
|
||||
|
||||
inherit_fields = {
|
||||
"critical": "inherit",
|
||||
@@ -406,7 +368,7 @@ class TestPolicyViews(TacticalTestCase):
|
||||
# test reset agents in site
|
||||
data = {"site": sites[0].id} # type: ignore
|
||||
|
||||
resp = self.client.patch(url, data, format="json")
|
||||
resp = self.client.post(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
agents = Agent.objects.filter(site=sites[0]) # type: ignore
|
||||
@@ -418,7 +380,7 @@ class TestPolicyViews(TacticalTestCase):
|
||||
# test reset agents in client
|
||||
data = {"client": clients[1].id} # type: ignore
|
||||
|
||||
resp = self.client.patch(url, data, format="json")
|
||||
resp = self.client.post(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
agents = Agent.objects.filter(site__client=clients[1]) # type: ignore
|
||||
@@ -430,7 +392,7 @@ class TestPolicyViews(TacticalTestCase):
|
||||
# test reset all agents
|
||||
data = {}
|
||||
|
||||
resp = self.client.patch(url, data, format="json")
|
||||
resp = self.client.post(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
agents = Agent.objects.all()
|
||||
@@ -438,17 +400,17 @@ class TestPolicyViews(TacticalTestCase):
|
||||
for k, v in inherit_fields.items():
|
||||
self.assertEqual(getattr(agent.winupdatepolicy.get(), k), v)
|
||||
|
||||
self.check_not_authenticated("patch", url)
|
||||
self.check_not_authenticated("post", url)
|
||||
|
||||
def test_delete_patch_policy(self):
|
||||
# test patch policy doesn't exist
|
||||
resp = self.client.delete("/automation/winupdatepolicy/500/", format="json")
|
||||
resp = self.client.delete("/automation/patchpolicy/500/", format="json")
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
winupdate_policy = baker.make_recipe(
|
||||
"winupdate.winupdate_policy", policy__name="Test Policy"
|
||||
)
|
||||
url = f"/automation/winupdatepolicy/{winupdate_policy.pk}/"
|
||||
url = f"/automation/patchpolicy/{winupdate_policy.pk}/"
|
||||
|
||||
resp = self.client.delete(url, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
@@ -503,7 +465,7 @@ class TestPolicyTasks(TacticalTestCase):
|
||||
|
||||
# Add Client to Policy
|
||||
policy.server_clients.add(server_agents[13].client) # type: ignore
|
||||
policy.workstation_clients.add(workstation_agents[15].client) # type: ignore
|
||||
policy.workstation_clients.add(workstation_agents[13].client) # type: ignore
|
||||
|
||||
resp = self.client.get(
|
||||
f"/automation/policies/{policy.pk}/related/", format="json" # type: ignore
|
||||
@@ -511,22 +473,28 @@ class TestPolicyTasks(TacticalTestCase):
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEquals(len(resp.data["server_clients"]), 1) # type: ignore
|
||||
self.assertEquals(len(resp.data["server_sites"]), 5) # type: ignore
|
||||
self.assertEquals(len(resp.data["server_sites"]), 0) # type: ignore
|
||||
self.assertEquals(len(resp.data["workstation_clients"]), 1) # type: ignore
|
||||
self.assertEquals(len(resp.data["workstation_sites"]), 5) # type: ignore
|
||||
self.assertEquals(len(resp.data["agents"]), 10) # type: ignore
|
||||
self.assertEquals(len(resp.data["workstation_sites"]), 0) # type: ignore
|
||||
self.assertEquals(len(resp.data["agents"]), 0) # type: ignore
|
||||
|
||||
# Add Site to Policy and the agents and sites length shouldn't change
|
||||
policy.server_sites.add(server_agents[13].site) # type: ignore
|
||||
policy.workstation_sites.add(workstation_agents[15].site) # type: ignore
|
||||
self.assertEquals(len(resp.data["server_sites"]), 5) # type: ignore
|
||||
self.assertEquals(len(resp.data["workstation_sites"]), 5) # type: ignore
|
||||
self.assertEquals(len(resp.data["agents"]), 10) # type: ignore
|
||||
# Add Site to Policy
|
||||
policy.server_sites.add(server_agents[10].site) # type: ignore
|
||||
policy.workstation_sites.add(workstation_agents[10].site) # type: ignore
|
||||
resp = self.client.get(
|
||||
f"/automation/policies/{policy.pk}/related/", format="json" # type: ignore
|
||||
)
|
||||
self.assertEquals(len(resp.data["server_sites"]), 1) # type: ignore
|
||||
self.assertEquals(len(resp.data["workstation_sites"]), 1) # type: ignore
|
||||
self.assertEquals(len(resp.data["agents"]), 0) # type: ignore
|
||||
|
||||
# Add Agent to Policy and the agents length shouldn't change
|
||||
policy.agents.add(server_agents[13]) # type: ignore
|
||||
policy.agents.add(workstation_agents[15]) # type: ignore
|
||||
self.assertEquals(len(resp.data["agents"]), 10) # type: ignore
|
||||
# Add Agent to Policy
|
||||
policy.agents.add(server_agents[2]) # type: ignore
|
||||
policy.agents.add(workstation_agents[2]) # type: ignore
|
||||
resp = self.client.get(
|
||||
f"/automation/policies/{policy.pk}/related/", format="json" # type: ignore
|
||||
)
|
||||
self.assertEquals(len(resp.data["agents"]), 2) # type: ignore
|
||||
|
||||
def test_generating_agent_policy_checks(self):
|
||||
from .tasks import generate_agent_checks_task
|
||||
@@ -918,11 +886,13 @@ class TestPolicyTasks(TacticalTestCase):
|
||||
@patch("autotasks.models.AutomatedTask.create_task_on_agent")
|
||||
@patch("autotasks.models.AutomatedTask.delete_task_on_agent")
|
||||
def test_delete_policy_tasks(self, delete_task_on_agent, create_task):
|
||||
from .tasks import delete_policy_autotasks_task
|
||||
from .tasks import delete_policy_autotasks_task, generate_agent_checks_task
|
||||
|
||||
policy = baker.make("automation.Policy", active=True)
|
||||
tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3)
|
||||
baker.make_recipe("agents.server_agent", policy=policy)
|
||||
agent = baker.make_recipe("agents.server_agent", policy=policy)
|
||||
|
||||
generate_agent_checks_task(agents=[agent.pk], create_tasks=True)
|
||||
|
||||
delete_policy_autotasks_task(task=tasks[0].id) # type: ignore
|
||||
|
||||
@@ -931,11 +901,13 @@ class TestPolicyTasks(TacticalTestCase):
|
||||
@patch("autotasks.models.AutomatedTask.create_task_on_agent")
|
||||
@patch("autotasks.models.AutomatedTask.run_win_task")
|
||||
def test_run_policy_task(self, run_win_task, create_task):
|
||||
from .tasks import run_win_policy_autotasks_task
|
||||
from .tasks import run_win_policy_autotasks_task, generate_agent_checks_task
|
||||
|
||||
policy = baker.make("automation.Policy", active=True)
|
||||
tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3)
|
||||
baker.make_recipe("agents.server_agent", policy=policy)
|
||||
agent = baker.make_recipe("agents.server_agent", policy=policy)
|
||||
|
||||
generate_agent_checks_task(agents=[agent.pk], create_tasks=True)
|
||||
|
||||
run_win_policy_autotasks_task(task=tasks[0].id) # type: ignore
|
||||
|
||||
@@ -944,7 +916,10 @@ class TestPolicyTasks(TacticalTestCase):
|
||||
@patch("autotasks.models.AutomatedTask.create_task_on_agent")
|
||||
@patch("autotasks.models.AutomatedTask.modify_task_on_agent")
|
||||
def test_update_policy_tasks(self, modify_task_on_agent, create_task):
|
||||
from .tasks import update_policy_autotasks_fields_task
|
||||
from .tasks import (
|
||||
update_policy_autotasks_fields_task,
|
||||
generate_agent_checks_task,
|
||||
)
|
||||
|
||||
# setup data
|
||||
policy = baker.make("automation.Policy", active=True)
|
||||
@@ -956,6 +931,8 @@ class TestPolicyTasks(TacticalTestCase):
|
||||
)
|
||||
agent = baker.make_recipe("agents.server_agent", policy=policy)
|
||||
|
||||
generate_agent_checks_task(agents=[agent.pk], create_tasks=True)
|
||||
|
||||
tasks[0].enabled = False # type: ignore
|
||||
tasks[0].save() # type: ignore
|
||||
|
||||
@@ -995,6 +972,8 @@ class TestPolicyTasks(TacticalTestCase):
|
||||
|
||||
@patch("autotasks.models.AutomatedTask.create_task_on_agent")
|
||||
def test_policy_exclusions(self, create_task):
|
||||
from .tasks import generate_agent_checks_task
|
||||
|
||||
# setup data
|
||||
policy = baker.make("automation.Policy", active=True)
|
||||
baker.make_recipe("checks.memory_check", policy=policy)
|
||||
@@ -1003,6 +982,8 @@ class TestPolicyTasks(TacticalTestCase):
|
||||
"agents.agent", policy=policy, monitoring_type="server"
|
||||
)
|
||||
|
||||
generate_agent_checks_task(agents=[agent.pk], create_tasks=True)
|
||||
|
||||
# make sure related agents on policy returns correctly
|
||||
self.assertEqual(policy.related_agents().count(), 1) # type: ignore
|
||||
self.assertEqual(agent.agentchecks.count(), 1) # type: ignore
|
||||
@@ -1164,3 +1145,9 @@ class TestPolicyTasks(TacticalTestCase):
|
||||
# should get policies from agent policy
|
||||
self.assertTrue(agent.autotasks.all())
|
||||
self.assertTrue(agent.agentchecks.all())
|
||||
|
||||
|
||||
class TestAutomationPermission(TacticalTestCase):
|
||||
def setUp(self):
|
||||
self.client_setup()
|
||||
self.setup_coresettings()
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
from checks.views import GetAddChecks
|
||||
from autotasks.views import GetAddAutoTasks
|
||||
|
||||
urlpatterns = [
|
||||
path("policies/", views.GetAddPolicies.as_view()),
|
||||
@@ -8,12 +10,14 @@ urlpatterns = [
|
||||
path("policies/overview/", views.OverviewPolicy.as_view()),
|
||||
path("policies/<int:pk>/", views.GetUpdateDeletePolicy.as_view()),
|
||||
path("sync/", views.PolicySync.as_view()),
|
||||
path("<int:pk>/policychecks/", views.PolicyCheck.as_view()),
|
||||
path("<int:pk>/policyautomatedtasks/", views.PolicyAutoTask.as_view()),
|
||||
path("policycheckstatus/<int:check>/check/", views.PolicyCheck.as_view()),
|
||||
path("policyautomatedtaskstatus/<int:task>/task/", views.PolicyAutoTask.as_view()),
|
||||
path("runwintask/<int:task>/", views.PolicyAutoTask.as_view()),
|
||||
path("winupdatepolicy/", views.UpdatePatchPolicy.as_view()),
|
||||
path("winupdatepolicy/<int:patchpolicy>/", views.UpdatePatchPolicy.as_view()),
|
||||
path("winupdatepolicy/reset/", views.UpdatePatchPolicy.as_view()),
|
||||
# alias to get policy checks
|
||||
path("policies/<int:policy>/checks/", GetAddChecks.as_view()),
|
||||
# alias to get policy tasks
|
||||
path("policies/<int:policy>/tasks/", GetAddAutoTasks.as_view()),
|
||||
path("checks/<int:check>/status/", views.PolicyCheck.as_view()),
|
||||
path("tasks/<int:task>/status/", views.PolicyAutoTask.as_view()),
|
||||
path("tasks/<int:task>/run/", views.PolicyAutoTask.as_view()),
|
||||
path("patchpolicy/", views.UpdatePatchPolicy.as_view()),
|
||||
path("patchpolicy/<int:pk>/", views.UpdatePatchPolicy.as_view()),
|
||||
path("patchpolicy/reset/", views.ResetPatchPolicy.as_view()),
|
||||
]
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
from agents.models import Agent
|
||||
from agents.serializers import AgentHostnameSerializer
|
||||
from autotasks.models import AutomatedTask
|
||||
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 rest_framework.exceptions import PermissionDenied
|
||||
from tacticalrmm.utils import notify_error
|
||||
from tacticalrmm.permissions import _has_perm_on_client, _has_perm_on_site
|
||||
from winupdate.models import WinUpdatePolicy
|
||||
from winupdate.serializers import WinUpdatePolicySerializer
|
||||
|
||||
from .models import Policy
|
||||
from .permissions import AutomationPolicyPerms
|
||||
from .serializers import (
|
||||
AutoTasksFieldSerializer,
|
||||
PolicyCheckSerializer,
|
||||
PolicyCheckStatusSerializer,
|
||||
PolicyRelatedSerializer,
|
||||
PolicyOverviewSerializer,
|
||||
PolicySerializer,
|
||||
PolicyTableSerializer,
|
||||
@@ -31,7 +30,11 @@ class GetAddPolicies(APIView):
|
||||
def get(self, request):
|
||||
policies = Policy.objects.all()
|
||||
|
||||
return Response(PolicyTableSerializer(policies, many=True).data)
|
||||
return Response(
|
||||
PolicyTableSerializer(
|
||||
policies, context={"user": request.user}, many=True
|
||||
).data
|
||||
)
|
||||
|
||||
def post(self, request):
|
||||
serializer = PolicySerializer(data=request.data, partial=True)
|
||||
@@ -102,19 +105,14 @@ 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)
|
||||
return Response(AutoTasksFieldSerializer(tasks, many=True).data)
|
||||
|
||||
# get status of all tasks
|
||||
def patch(self, request, task):
|
||||
def get(self, request, task):
|
||||
tasks = AutomatedTask.objects.filter(parent_task=task)
|
||||
return Response(PolicyTaskStatusSerializer(tasks, many=True).data)
|
||||
|
||||
# bulk run win tasks associated with policy
|
||||
def put(self, request, task):
|
||||
def post(self, request, task):
|
||||
from .tasks import run_win_policy_autotasks_task
|
||||
|
||||
run_win_policy_autotasks_task.delay(task=task)
|
||||
@@ -124,11 +122,7 @@ 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)
|
||||
|
||||
def patch(self, request, check):
|
||||
def get(self, request, check):
|
||||
checks = Check.objects.filter(parent_check=check)
|
||||
return Response(PolicyCheckStatusSerializer(checks, many=True).data)
|
||||
|
||||
@@ -143,8 +137,6 @@ class OverviewPolicy(APIView):
|
||||
class GetRelated(APIView):
|
||||
def get(self, request, pk):
|
||||
|
||||
response = {}
|
||||
|
||||
policy = (
|
||||
Policy.objects.filter(pk=pk)
|
||||
.prefetch_related(
|
||||
@@ -156,43 +148,9 @@ class GetRelated(APIView):
|
||||
.first()
|
||||
)
|
||||
|
||||
response["default_server_policy"] = policy.is_default_server_policy
|
||||
response["default_workstation_policy"] = policy.is_default_workstation_policy
|
||||
|
||||
response["server_clients"] = ClientSerializer(
|
||||
policy.server_clients.all(), many=True
|
||||
).data
|
||||
response["workstation_clients"] = ClientSerializer(
|
||||
policy.workstation_clients.all(), many=True
|
||||
).data
|
||||
|
||||
filtered_server_sites = list()
|
||||
filtered_workstation_sites = list()
|
||||
|
||||
for client in policy.server_clients.all():
|
||||
for site in client.sites.all():
|
||||
if site not in policy.server_sites.all():
|
||||
filtered_server_sites.append(site)
|
||||
|
||||
response["server_sites"] = SiteSerializer(
|
||||
filtered_server_sites + list(policy.server_sites.all()), many=True
|
||||
).data
|
||||
|
||||
for client in policy.workstation_clients.all():
|
||||
for site in client.sites.all():
|
||||
if site not in policy.workstation_sites.all():
|
||||
filtered_workstation_sites.append(site)
|
||||
|
||||
response["workstation_sites"] = SiteSerializer(
|
||||
filtered_workstation_sites + list(policy.workstation_sites.all()), many=True
|
||||
).data
|
||||
|
||||
response["agents"] = AgentHostnameSerializer(
|
||||
policy.related_agents().only("pk", "hostname"),
|
||||
many=True,
|
||||
).data
|
||||
|
||||
return Response(response)
|
||||
return Response(
|
||||
PolicyRelatedSerializer(policy, context={"user": request.user}).data
|
||||
)
|
||||
|
||||
|
||||
class UpdatePatchPolicy(APIView):
|
||||
@@ -209,8 +167,8 @@ class UpdatePatchPolicy(APIView):
|
||||
return Response("ok")
|
||||
|
||||
# update patch policy
|
||||
def put(self, request, patchpolicy):
|
||||
policy = get_object_or_404(WinUpdatePolicy, pk=patchpolicy)
|
||||
def put(self, request, pk):
|
||||
policy = get_object_or_404(WinUpdatePolicy, pk=pk)
|
||||
|
||||
serializer = WinUpdatePolicySerializer(
|
||||
instance=policy, data=request.data, partial=True
|
||||
@@ -220,20 +178,41 @@ class UpdatePatchPolicy(APIView):
|
||||
|
||||
return Response("ok")
|
||||
|
||||
# bulk reset agent patch policy
|
||||
def patch(self, request):
|
||||
# delete patch policy
|
||||
def delete(self, request, pk):
|
||||
get_object_or_404(WinUpdatePolicy, pk=pk).delete()
|
||||
|
||||
return Response("ok")
|
||||
|
||||
|
||||
class ResetPatchPolicy(APIView):
|
||||
# bulk reset agent patch policy
|
||||
def post(self, request):
|
||||
|
||||
agents = None
|
||||
if "client" in request.data:
|
||||
agents = Agent.objects.prefetch_related("winupdatepolicy").filter(
|
||||
site__client_id=request.data["client"]
|
||||
if not _has_perm_on_client(request.user, request.data["client"]):
|
||||
raise PermissionDenied()
|
||||
|
||||
agents = (
|
||||
Agent.objects.filter_by_role(request.user)
|
||||
.prefetch_related("winupdatepolicy")
|
||||
.filter(site__client_id=request.data["client"])
|
||||
)
|
||||
elif "site" in request.data:
|
||||
agents = Agent.objects.prefetch_related("winupdatepolicy").filter(
|
||||
site_id=request.data["site"]
|
||||
if not _has_perm_on_site(request.user, request.data["site"]):
|
||||
raise PermissionDenied()
|
||||
|
||||
agents = (
|
||||
Agent.objects.filter_by_role(request.user)
|
||||
.prefetch_related("winupdatepolicy")
|
||||
.filter(site_id=request.data["site"])
|
||||
)
|
||||
else:
|
||||
agents = Agent.objects.prefetch_related("winupdatepolicy").only("pk")
|
||||
agents = (
|
||||
Agent.objects.filter_by_role(request.user)
|
||||
.prefetch_related("winupdatepolicy")
|
||||
.only("pk")
|
||||
)
|
||||
|
||||
for agent in agents:
|
||||
winupdatepolicy = agent.winupdatepolicy.get()
|
||||
@@ -258,10 +237,4 @@ class UpdatePatchPolicy(APIView):
|
||||
]
|
||||
)
|
||||
|
||||
return Response("ok")
|
||||
|
||||
# delete patch policy
|
||||
def delete(self, request, patchpolicy):
|
||||
get_object_or_404(WinUpdatePolicy, pk=patchpolicy).delete()
|
||||
|
||||
return Response("ok")
|
||||
return Response("The patch policy on the affected agents has been reset.")
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.2.6 on 2021-09-17 19:54
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("autotasks", "0022_automatedtask_collector_all_output"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="automatedtask",
|
||||
name="created_by",
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="automatedtask",
|
||||
name="modified_by",
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
]
|
||||
@@ -12,6 +12,7 @@ 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, DebugLog
|
||||
from tacticalrmm.models import PermissionQuerySet
|
||||
from packaging import version as pyver
|
||||
from tacticalrmm.utils import bitdays_to_string
|
||||
|
||||
@@ -47,6 +48,8 @@ TASK_STATUS_CHOICES = [
|
||||
|
||||
|
||||
class AutomatedTask(BaseAuditModel):
|
||||
objects = PermissionQuerySet.as_manager()
|
||||
|
||||
agent = models.ForeignKey(
|
||||
"agents.Agent",
|
||||
related_name="autotasks",
|
||||
@@ -132,6 +135,31 @@ class AutomatedTask(BaseAuditModel):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
from autotasks.tasks import enable_or_disable_win_task
|
||||
from automation.tasks import update_policy_autotasks_fields_task
|
||||
|
||||
# get old agent if exists
|
||||
old_task = AutomatedTask.objects.get(pk=self.pk) if self.pk else None
|
||||
super(AutomatedTask, self).save(old_model=old_task, *args, **kwargs)
|
||||
|
||||
# check if automated task was enabled/disabled and send celery task
|
||||
if old_task and old_task.enabled != self.enabled:
|
||||
if self.agent:
|
||||
enable_or_disable_win_task.delay(pk=self.pk)
|
||||
|
||||
# check if automated task was enabled/disabled and send celery task
|
||||
elif old_task.policy:
|
||||
update_policy_autotasks_fields_task.delay(
|
||||
task=self.pk, update_agent=True
|
||||
)
|
||||
# check if policy task was edited and then check if it was a field worth copying to rest of agent tasks
|
||||
elif old_task and old_task.policy:
|
||||
for field in self.policy_fields_to_copy:
|
||||
if getattr(self, field) != getattr(old_task, field):
|
||||
update_policy_autotasks_fields_task.delay(task=self.pk)
|
||||
break
|
||||
|
||||
@property
|
||||
def schedule(self):
|
||||
if self.task_type == "manual":
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
from rest_framework import permissions
|
||||
|
||||
from tacticalrmm.permissions import _has_perm
|
||||
from tacticalrmm.permissions import _has_perm, _has_perm_on_agent
|
||||
|
||||
|
||||
class ManageAutoTaskPerms(permissions.BasePermission):
|
||||
class AutoTaskPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
if r.method == "GET":
|
||||
return True
|
||||
|
||||
return _has_perm(r, "can_manage_autotasks")
|
||||
if "agent_id" in view.kwargs.keys():
|
||||
return _has_perm(r, "can_list_autotasks") and _has_perm_on_agent(
|
||||
r.user, view.kwargs["agent_id"]
|
||||
)
|
||||
else:
|
||||
return _has_perm(r, "can_list_autotasks")
|
||||
else:
|
||||
return _has_perm(r, "can_manage_autotasks")
|
||||
|
||||
|
||||
class RunAutoTaskPerms(permissions.BasePermission):
|
||||
|
||||
@@ -10,7 +10,7 @@ from .models import AutomatedTask
|
||||
|
||||
class TaskSerializer(serializers.ModelSerializer):
|
||||
|
||||
assigned_check = CheckSerializer(read_only=True)
|
||||
check_name = serializers.ReadOnlyField(source="assigned_check.readable_desc")
|
||||
schedule = serializers.ReadOnlyField()
|
||||
last_run = serializers.ReadOnlyField(source="last_run_as_timezone")
|
||||
alert_template = serializers.SerializerMethodField()
|
||||
@@ -37,19 +37,6 @@ class TaskSerializer(serializers.ModelSerializer):
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class AutoTaskSerializer(serializers.ModelSerializer):
|
||||
|
||||
autotasks = TaskSerializer(many=True, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Agent
|
||||
fields = (
|
||||
"pk",
|
||||
"hostname",
|
||||
"autotasks",
|
||||
)
|
||||
|
||||
|
||||
# below is for the windows agent
|
||||
class TaskRunnerScriptField(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
|
||||
@@ -7,21 +7,49 @@ from model_bakery import baker
|
||||
from tacticalrmm.test import TacticalTestCase
|
||||
|
||||
from .models import AutomatedTask
|
||||
from .serializers import AutoTaskSerializer
|
||||
from .serializers import TaskSerializer
|
||||
from .tasks import create_win_task_schedule, remove_orphaned_win_tasks, run_win_task
|
||||
|
||||
base_url = "/tasks"
|
||||
|
||||
|
||||
class TestAutotaskViews(TacticalTestCase):
|
||||
def setUp(self):
|
||||
self.authenticate()
|
||||
self.setup_coresettings()
|
||||
|
||||
def test_get_autotasks(self):
|
||||
# setup data
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
baker.make("autotasks.AutomatedTask", agent=agent, _quantity=3)
|
||||
policy = baker.make("automation.Policy")
|
||||
baker.make("autotasks.AutomatedTask", policy=policy, _quantity=4)
|
||||
baker.make("autotasks.AutomatedTask", _quantity=7)
|
||||
|
||||
# test returning all tasks
|
||||
url = f"{base_url}/"
|
||||
resp = self.client.get(url, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(len(resp.data), 14)
|
||||
|
||||
# test returning tasks for a specific agent
|
||||
url = f"/agents/{agent.agent_id}/tasks/"
|
||||
resp = self.client.get(url, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(len(resp.data), 3)
|
||||
|
||||
# test returning tasks for a specific policy
|
||||
url = f"/automation/policies/{policy.id}/tasks/"
|
||||
resp = self.client.get(url, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(len(resp.data), 4)
|
||||
|
||||
@patch("automation.tasks.generate_agent_autotasks_task.delay")
|
||||
@patch("autotasks.tasks.create_win_task_schedule.delay")
|
||||
def test_add_autotask(
|
||||
self, create_win_task_schedule, generate_agent_autotasks_task
|
||||
):
|
||||
url = "/tasks/automatedtasks/"
|
||||
url = f"{base_url}/"
|
||||
|
||||
# setup data
|
||||
script = baker.make_recipe("scripts.script")
|
||||
@@ -29,22 +57,9 @@ class TestAutotaskViews(TacticalTestCase):
|
||||
policy = baker.make("automation.Policy")
|
||||
check = baker.make_recipe("checks.diskspace_check", agent=agent)
|
||||
|
||||
# test script set to invalid pk
|
||||
data = {"autotask": {"script": 500}}
|
||||
|
||||
resp = self.client.post(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
# test invalid policy
|
||||
data = {"autotask": {"script": script.id}, "policy": 500}
|
||||
|
||||
resp = self.client.post(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
# test invalid agent
|
||||
data = {
|
||||
"autotask": {"script": script.id},
|
||||
"agent": 500,
|
||||
"agent": "13kfs89as9d89asd8f98df8df8dfhdf",
|
||||
}
|
||||
|
||||
resp = self.client.post(url, data, format="json")
|
||||
@@ -52,18 +67,16 @@ class TestAutotaskViews(TacticalTestCase):
|
||||
|
||||
# test add task to agent
|
||||
data = {
|
||||
"autotask": {
|
||||
"name": "Test Task Scheduled with Assigned Check",
|
||||
"run_time_days": ["Sunday", "Monday", "Friday"],
|
||||
"run_time_minute": "10:00",
|
||||
"timeout": 120,
|
||||
"enabled": True,
|
||||
"script": script.id,
|
||||
"script_args": None,
|
||||
"task_type": "scheduled",
|
||||
"assigned_check": check.id,
|
||||
},
|
||||
"agent": agent.id,
|
||||
"agent": agent.agent_id,
|
||||
"name": "Test Task Scheduled with Assigned Check",
|
||||
"run_time_days": ["Sunday", "Monday", "Friday"],
|
||||
"run_time_minute": "10:00",
|
||||
"timeout": 120,
|
||||
"enabled": True,
|
||||
"script": script.id,
|
||||
"script_args": None,
|
||||
"task_type": "scheduled",
|
||||
"assigned_check": check.id,
|
||||
}
|
||||
|
||||
resp = self.client.post(url, data, format="json")
|
||||
@@ -73,17 +86,15 @@ class TestAutotaskViews(TacticalTestCase):
|
||||
|
||||
# test add task to policy
|
||||
data = {
|
||||
"autotask": {
|
||||
"name": "Test Task Manual",
|
||||
"run_time_days": [],
|
||||
"timeout": 120,
|
||||
"enabled": True,
|
||||
"script": script.id,
|
||||
"script_args": None,
|
||||
"task_type": "manual",
|
||||
"assigned_check": None,
|
||||
},
|
||||
"policy": policy.id, # type: ignore
|
||||
"name": "Test Task Manual",
|
||||
"run_time_days": [],
|
||||
"timeout": 120,
|
||||
"enabled": True,
|
||||
"script": script.id,
|
||||
"script_args": None,
|
||||
"task_type": "manual",
|
||||
"assigned_check": None,
|
||||
}
|
||||
|
||||
resp = self.client.post(url, data, format="json")
|
||||
@@ -97,12 +108,12 @@ class TestAutotaskViews(TacticalTestCase):
|
||||
|
||||
# setup data
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
baker.make("autotasks.AutomatedTask", agent=agent, _quantity=3)
|
||||
task = baker.make("autotasks.AutomatedTask", agent=agent)
|
||||
|
||||
url = f"/tasks/{agent.id}/automatedtasks/"
|
||||
url = f"{base_url}/{task.id}/"
|
||||
|
||||
resp = self.client.get(url, format="json")
|
||||
serializer = AutoTaskSerializer(agent)
|
||||
serializer = TaskSerializer(task)
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(resp.data, serializer.data) # type: ignore
|
||||
@@ -118,33 +129,48 @@ class TestAutotaskViews(TacticalTestCase):
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
agent_task = baker.make("autotasks.AutomatedTask", agent=agent)
|
||||
policy = baker.make("automation.Policy")
|
||||
policy_task = baker.make("autotasks.AutomatedTask", policy=policy)
|
||||
policy_task = baker.make("autotasks.AutomatedTask", enabled=True, policy=policy)
|
||||
|
||||
# test invalid url
|
||||
resp = self.client.patch("/tasks/500/automatedtasks/", format="json")
|
||||
resp = self.client.put(f"{base_url}/500/", format="json")
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
url = f"/tasks/{agent_task.id}/automatedtasks/" # type: ignore
|
||||
url = f"{base_url}/{agent_task.id}/" # type: ignore
|
||||
|
||||
# test editing agent task
|
||||
data = {"enableordisable": False}
|
||||
# test editing task with no task called
|
||||
data = {"name": "New Name"}
|
||||
|
||||
resp = self.client.patch(url, data, format="json")
|
||||
resp = self.client.put(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
enable_or_disable_win_task.not_called() # type: ignore
|
||||
|
||||
# test editing task
|
||||
data = {"enabled": False}
|
||||
|
||||
resp = self.client.put(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
enable_or_disable_win_task.assert_called_with(pk=agent_task.id) # type: ignore
|
||||
|
||||
url = f"/tasks/{policy_task.id}/automatedtasks/" # type: ignore
|
||||
url = f"{base_url}/{policy_task.id}/" # type: ignore
|
||||
|
||||
# test editing policy task
|
||||
data = {"enableordisable": True}
|
||||
data = {"enabled": False}
|
||||
|
||||
resp = self.client.patch(url, data, format="json")
|
||||
resp = self.client.put(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
update_policy_autotasks_fields_task.assert_called_with(
|
||||
task=policy_task.id, update_agent=True # type: ignore
|
||||
)
|
||||
update_policy_autotasks_fields_task.reset_mock()
|
||||
|
||||
self.check_not_authenticated("patch", url)
|
||||
# test editing policy task with no agent update
|
||||
data = {"name": "New Name"}
|
||||
|
||||
resp = self.client.put(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
update_policy_autotasks_fields_task.assert_called_with(task=policy_task.id)
|
||||
|
||||
self.check_not_authenticated("put", url)
|
||||
|
||||
@patch("autotasks.tasks.delete_win_task_schedule.delay")
|
||||
@patch("automation.tasks.delete_policy_autotasks_task.delay")
|
||||
@@ -158,17 +184,17 @@ class TestAutotaskViews(TacticalTestCase):
|
||||
policy_task = baker.make("autotasks.AutomatedTask", policy=policy)
|
||||
|
||||
# test invalid url
|
||||
resp = self.client.delete("/tasks/500/automatedtasks/", format="json")
|
||||
resp = self.client.delete(f"{base_url}/500/", format="json")
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
# test delete agent task
|
||||
url = f"/tasks/{agent_task.id}/automatedtasks/" # type: ignore
|
||||
url = f"{base_url}/{agent_task.id}/" # type: ignore
|
||||
resp = self.client.delete(url, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
delete_win_task_schedule.assert_called_with(pk=agent_task.id) # type: ignore
|
||||
|
||||
# test delete policy task
|
||||
url = f"/tasks/{policy_task.id}/automatedtasks/" # type: ignore
|
||||
url = f"{base_url}/{policy_task.id}/" # 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
|
||||
@@ -183,16 +209,16 @@ class TestAutotaskViews(TacticalTestCase):
|
||||
task = baker.make("autotasks.AutomatedTask", agent=agent)
|
||||
|
||||
# test invalid url
|
||||
resp = self.client.get("/tasks/runwintask/500/", format="json")
|
||||
resp = self.client.post(f"{base_url}/500/run/", format="json")
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
# test run agent task
|
||||
url = f"/tasks/runwintask/{task.id}/" # type: ignore
|
||||
resp = self.client.get(url, format="json")
|
||||
url = f"{base_url}/{task.id}/run/" # type: ignore
|
||||
resp = self.client.post(url, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
run_win_task.assert_called()
|
||||
|
||||
self.check_not_authenticated("get", url)
|
||||
self.check_not_authenticated("post", url)
|
||||
|
||||
|
||||
class TestAutoTaskCeleryTasks(TacticalTestCase):
|
||||
@@ -410,3 +436,227 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
|
||||
timeout=5,
|
||||
)
|
||||
self.assertEqual(ret.status, "SUCCESS")
|
||||
|
||||
|
||||
class TestTaskPermissions(TacticalTestCase):
|
||||
def setUp(self):
|
||||
self.setup_coresettings()
|
||||
self.client_setup()
|
||||
|
||||
def test_get_tasks_permissions(self):
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
policy = baker.make("automation.Policy")
|
||||
unauthorized_agent = baker.make_recipe("agents.agent")
|
||||
task = baker.make("autotasks.AutomatedTask", agent=agent, _quantity=5)
|
||||
unauthorized_task = baker.make(
|
||||
"autotasks.AutomatedTask", agent=unauthorized_agent, _quantity=7
|
||||
)
|
||||
|
||||
policy_tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=2)
|
||||
|
||||
# test super user access
|
||||
self.check_authorized_superuser("get", f"{base_url}/")
|
||||
self.check_authorized_superuser("get", f"/agents/{agent.agent_id}/tasks/")
|
||||
self.check_authorized_superuser(
|
||||
"get", f"/agents/{unauthorized_agent.agent_id}/tasks/"
|
||||
)
|
||||
self.check_authorized_superuser(
|
||||
"get", f"/automation/policies/{policy.id}/tasks/"
|
||||
)
|
||||
|
||||
user = self.create_user_with_roles([])
|
||||
self.client.force_authenticate(user=user)
|
||||
|
||||
self.check_not_authorized("get", f"{base_url}/")
|
||||
self.check_not_authorized("get", f"/agents/{agent.agent_id}/tasks/")
|
||||
self.check_not_authorized(
|
||||
"get", f"/agents/{unauthorized_agent.agent_id}/tasks/"
|
||||
)
|
||||
self.check_not_authorized("get", f"/automation/policies/{policy.id}/tasks/")
|
||||
|
||||
# add list software role to user
|
||||
user.role.can_list_autotasks = True
|
||||
user.role.save()
|
||||
|
||||
r = self.check_authorized("get", f"{base_url}/")
|
||||
self.assertEqual(len(r.data), 14)
|
||||
r = self.check_authorized("get", f"/agents/{agent.agent_id}/tasks/")
|
||||
self.assertEqual(len(r.data), 5)
|
||||
r = self.check_authorized(
|
||||
"get", f"/agents/{unauthorized_agent.agent_id}/tasks/"
|
||||
)
|
||||
self.assertEqual(len(r.data), 7)
|
||||
r = self.check_authorized("get", f"/automation/policies/{policy.id}/tasks/")
|
||||
self.assertEqual(len(r.data), 2)
|
||||
|
||||
# test limiting to client
|
||||
user.role.can_view_clients.set([agent.client])
|
||||
self.check_not_authorized(
|
||||
"get", f"/agents/{unauthorized_agent.agent_id}/tasks/"
|
||||
)
|
||||
self.check_authorized("get", f"/agents/{agent.agent_id}/tasks/")
|
||||
self.check_authorized("get", f"/automation/policies/{policy.id}/tasks/")
|
||||
|
||||
# make sure queryset is limited too
|
||||
r = self.client.get(f"{base_url}/")
|
||||
self.assertEqual(len(r.data), 7)
|
||||
|
||||
def test_add_task_permissions(self):
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
unauthorized_agent = baker.make_recipe("agents.agent")
|
||||
policy = baker.make("automation.Policy")
|
||||
script = baker.make("scripts.Script")
|
||||
|
||||
policy_data = {
|
||||
"policy": policy.id, # type: ignore
|
||||
"name": "Test Task Manual",
|
||||
"run_time_days": [],
|
||||
"timeout": 120,
|
||||
"enabled": True,
|
||||
"script": script.id,
|
||||
"script_args": [],
|
||||
"task_type": "manual",
|
||||
"assigned_check": None,
|
||||
}
|
||||
|
||||
agent_data = {
|
||||
"agent": agent.agent_id,
|
||||
"name": "Test Task Manual",
|
||||
"run_time_days": [],
|
||||
"timeout": 120,
|
||||
"enabled": True,
|
||||
"script": script.id,
|
||||
"script_args": [],
|
||||
"task_type": "manual",
|
||||
"assigned_check": None,
|
||||
}
|
||||
|
||||
unauthorized_agent_data = {
|
||||
"agent": unauthorized_agent.agent_id,
|
||||
"name": "Test Task Manual",
|
||||
"run_time_days": [],
|
||||
"timeout": 120,
|
||||
"enabled": True,
|
||||
"script": script.id,
|
||||
"script_args": [],
|
||||
"task_type": "manual",
|
||||
"assigned_check": None,
|
||||
}
|
||||
|
||||
url = f"{base_url}/"
|
||||
|
||||
for data in [policy_data, agent_data]:
|
||||
# test superuser access
|
||||
self.check_authorized_superuser("post", url, data)
|
||||
|
||||
user = self.create_user_with_roles([])
|
||||
self.client.force_authenticate(user=user)
|
||||
|
||||
# test user without role
|
||||
self.check_not_authorized("post", url, data)
|
||||
|
||||
# add user to role and test
|
||||
setattr(user.role, "can_manage_autotasks", True)
|
||||
user.role.save()
|
||||
|
||||
self.check_authorized("post", url, data)
|
||||
|
||||
# limit user to client
|
||||
user.role.can_view_clients.set([agent.client])
|
||||
if "agent" in data.keys():
|
||||
self.check_authorized("post", url, data)
|
||||
self.check_not_authorized("post", url, unauthorized_agent_data)
|
||||
else:
|
||||
self.check_authorized("post", url, data)
|
||||
|
||||
# mock the task delete method so it actually isn't deleted
|
||||
@patch("autotasks.models.AutomatedTask.delete")
|
||||
def test_task_get_edit_delete_permissions(self, delete_task):
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
unauthorized_agent = baker.make_recipe("agents.agent")
|
||||
policy = baker.make("automation.Policy")
|
||||
task = baker.make("autotasks.AutomatedTask", agent=agent)
|
||||
unauthorized_task = baker.make(
|
||||
"autotasks.AutomatedTask", agent=unauthorized_agent
|
||||
)
|
||||
policy_task = baker.make("autotasks.AutomatedTask", policy=policy)
|
||||
|
||||
for method in ["get", "put", "delete"]:
|
||||
|
||||
url = f"{base_url}/{task.id}/"
|
||||
unauthorized_url = f"{base_url}/{unauthorized_task.id}/"
|
||||
policy_url = f"{base_url}/{policy_task.id}/"
|
||||
|
||||
# test superuser access
|
||||
self.check_authorized_superuser(method, url)
|
||||
self.check_authorized_superuser(method, unauthorized_url)
|
||||
self.check_authorized_superuser(method, policy_url)
|
||||
|
||||
user = self.create_user_with_roles([])
|
||||
self.client.force_authenticate(user=user)
|
||||
|
||||
# test user without role
|
||||
self.check_not_authorized(method, url)
|
||||
self.check_not_authorized(method, unauthorized_url)
|
||||
self.check_not_authorized(method, policy_url)
|
||||
|
||||
# add user to role and test
|
||||
setattr(
|
||||
user.role,
|
||||
"can_list_autotasks" if method == "get" else "can_manage_autotasks",
|
||||
True,
|
||||
)
|
||||
user.role.save()
|
||||
|
||||
self.check_authorized(method, url)
|
||||
self.check_authorized(method, unauthorized_url)
|
||||
self.check_authorized(method, policy_url)
|
||||
|
||||
# limit user to client if agent task
|
||||
user.role.can_view_clients.set([agent.client])
|
||||
|
||||
self.check_authorized(method, url)
|
||||
self.check_not_authorized(method, unauthorized_url)
|
||||
self.check_authorized(method, policy_url)
|
||||
|
||||
def test_task_action_permissions(self):
|
||||
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
unauthorized_agent = baker.make_recipe("agents.agent")
|
||||
task = baker.make("autotasks.AutomatedTask", agent=agent)
|
||||
unauthorized_task = baker.make(
|
||||
"autotasks.AutomatedTask", agent=unauthorized_agent
|
||||
)
|
||||
|
||||
url = f"{base_url}/{task.id}/run/"
|
||||
unauthorized_url = f"{base_url}/{unauthorized_task.id}/run/"
|
||||
|
||||
# test superuser access
|
||||
self.check_authorized_superuser("post", url)
|
||||
self.check_authorized_superuser("post", unauthorized_url)
|
||||
|
||||
user = self.create_user_with_roles([])
|
||||
self.client.force_authenticate(user=user)
|
||||
|
||||
# test user without role
|
||||
self.check_not_authorized("post", url)
|
||||
self.check_not_authorized("post", unauthorized_url)
|
||||
|
||||
# add user to role and test
|
||||
user.role.can_run_autotasks = True
|
||||
user.role.save()
|
||||
|
||||
self.check_authorized("post", url)
|
||||
self.check_authorized("post", unauthorized_url)
|
||||
|
||||
# limit user to client if agent task
|
||||
user.role.can_view_sites.set([agent.site])
|
||||
|
||||
self.check_authorized("post", url)
|
||||
self.check_not_authorized("post", unauthorized_url)
|
||||
|
||||
def test_policy_fields_to_copy_exists(self):
|
||||
fields = [i.name for i in AutomatedTask._meta.get_fields()]
|
||||
task = baker.make("autotasks.AutomatedTask")
|
||||
for i in task.policy_fields_to_copy: # type: ignore
|
||||
self.assertIn(i, fields)
|
||||
|
||||
@@ -3,7 +3,7 @@ from django.urls import path
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path("<int:pk>/automatedtasks/", views.AutoTask.as_view()),
|
||||
path("automatedtasks/", views.AddAutoTask.as_view()),
|
||||
path("runwintask/<int:pk>/", views.run_task),
|
||||
path("", views.GetAddAutoTasks.as_view()),
|
||||
path("<int:pk>/", views.GetEditDeleteAutoTask.as_view()),
|
||||
path("<int:pk>/run/", views.RunAutoTask.as_view()),
|
||||
]
|
||||
|
||||
@@ -1,55 +1,59 @@
|
||||
from django.shortcuts import get_object_or_404
|
||||
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 rest_framework.exceptions import PermissionDenied
|
||||
|
||||
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 automation.models import Policy
|
||||
from tacticalrmm.utils import get_bit_days
|
||||
from tacticalrmm.permissions import _has_perm_on_agent
|
||||
|
||||
from .models import AutomatedTask
|
||||
from .permissions import ManageAutoTaskPerms, RunAutoTaskPerms
|
||||
from .serializers import AutoTaskSerializer, TaskSerializer
|
||||
from .permissions import AutoTaskPerms, RunAutoTaskPerms
|
||||
from .serializers import TaskSerializer
|
||||
|
||||
|
||||
class AddAutoTask(APIView):
|
||||
permission_classes = [IsAuthenticated, ManageAutoTaskPerms]
|
||||
class GetAddAutoTasks(APIView):
|
||||
permission_classes = [IsAuthenticated, AutoTaskPerms]
|
||||
|
||||
def get(self, request, agent_id=None, policy=None):
|
||||
|
||||
if agent_id:
|
||||
agent = get_object_or_404(Agent, agent_id=agent_id)
|
||||
tasks = AutomatedTask.objects.filter(agent=agent)
|
||||
elif policy:
|
||||
policy = get_object_or_404(Policy, id=policy)
|
||||
tasks = AutomatedTask.objects.filter(policy=policy)
|
||||
else:
|
||||
tasks = AutomatedTask.objects.filter_by_role(request.user)
|
||||
return Response(TaskSerializer(tasks, many=True).data)
|
||||
|
||||
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
|
||||
script = get_object_or_404(Script, pk=data["autotask"]["script"])
|
||||
data = request.data.copy()
|
||||
|
||||
# Determine if adding check to Policy or Agent
|
||||
if "policy" in data:
|
||||
policy = get_object_or_404(Policy, id=data["policy"])
|
||||
# Object used for filter and save
|
||||
parent = {"policy": policy}
|
||||
else:
|
||||
agent = get_object_or_404(Agent, pk=data["agent"])
|
||||
parent = {"agent": agent}
|
||||
# Determine if adding to an agent and replace agent_id with pk
|
||||
if "agent" in data.keys():
|
||||
agent = get_object_or_404(Agent, agent_id=data["agent"])
|
||||
|
||||
check = None
|
||||
if data["autotask"]["assigned_check"]:
|
||||
check = get_object_or_404(Check, pk=data["autotask"]["assigned_check"])
|
||||
if not _has_perm_on_agent(request.user, agent.agent_id):
|
||||
raise PermissionDenied()
|
||||
|
||||
data["agent"] = agent.pk
|
||||
|
||||
bit_weekdays = None
|
||||
if data["autotask"]["run_time_days"]:
|
||||
bit_weekdays = get_bit_days(data["autotask"]["run_time_days"])
|
||||
if "run_time_days" in data.keys():
|
||||
if data["run_time_days"]:
|
||||
bit_weekdays = get_bit_days(data["run_time_days"])
|
||||
data.pop("run_time_days")
|
||||
|
||||
del data["autotask"]["run_time_days"]
|
||||
serializer = TaskSerializer(data=data["autotask"], partial=True, context=parent)
|
||||
serializer = TaskSerializer(data=data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
task = serializer.save(
|
||||
**parent,
|
||||
script=script,
|
||||
win_task_name=AutomatedTask.generate_task_name(),
|
||||
assigned_check=check,
|
||||
run_time_bit_weekdays=bit_weekdays,
|
||||
)
|
||||
|
||||
@@ -59,58 +63,35 @@ class AddAutoTask(APIView):
|
||||
elif task.policy:
|
||||
generate_agent_autotasks_task.delay(policy=task.policy.pk)
|
||||
|
||||
return Response("Task will be created shortly!")
|
||||
return Response(
|
||||
"The task has been created. It will show up on the agent on next checkin"
|
||||
)
|
||||
|
||||
|
||||
class AutoTask(APIView):
|
||||
permission_classes = [IsAuthenticated, ManageAutoTaskPerms]
|
||||
class GetEditDeleteAutoTask(APIView):
|
||||
permission_classes = [IsAuthenticated, AutoTaskPerms]
|
||||
|
||||
def get(self, request, pk):
|
||||
|
||||
agent = get_object_or_404(Agent, pk=pk)
|
||||
ctx = {
|
||||
"default_tz": get_default_timezone(),
|
||||
"agent_tz": agent.time_zone,
|
||||
}
|
||||
return Response(AutoTaskSerializer(agent, context=ctx).data)
|
||||
task = get_object_or_404(AutomatedTask, pk=pk)
|
||||
|
||||
if task.agent and not _has_perm_on_agent(request.user, task.agent.agent_id):
|
||||
raise PermissionDenied()
|
||||
|
||||
return Response(TaskSerializer(task).data)
|
||||
|
||||
def put(self, request, pk):
|
||||
from automation.tasks import update_policy_autotasks_fields_task
|
||||
|
||||
task = get_object_or_404(AutomatedTask, pk=pk)
|
||||
|
||||
if task.agent and not _has_perm_on_agent(request.user, task.agent.agent_id):
|
||||
raise PermissionDenied()
|
||||
|
||||
serializer = TaskSerializer(instance=task, data=request.data, partial=True)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
|
||||
if task.policy:
|
||||
update_policy_autotasks_fields_task.delay(task=task.pk)
|
||||
|
||||
return Response("ok")
|
||||
|
||||
def patch(self, request, pk):
|
||||
from automation.tasks import update_policy_autotasks_fields_task
|
||||
from autotasks.tasks import enable_or_disable_win_task
|
||||
|
||||
task = get_object_or_404(AutomatedTask, pk=pk)
|
||||
|
||||
if "enableordisable" in request.data:
|
||||
action = request.data["enableordisable"]
|
||||
task.enabled = action
|
||||
task.save(update_fields=["enabled"])
|
||||
action = "enabled" if action else "disabled"
|
||||
|
||||
if task.policy:
|
||||
update_policy_autotasks_fields_task.delay(
|
||||
task=task.pk, update_agent=True
|
||||
)
|
||||
elif task.agent:
|
||||
enable_or_disable_win_task.delay(pk=task.pk)
|
||||
|
||||
return Response(f"Task will be {action} shortly")
|
||||
|
||||
else:
|
||||
return notify_error("The request was invalid")
|
||||
return Response("The task was updated")
|
||||
|
||||
def delete(self, request, pk):
|
||||
from automation.tasks import delete_policy_autotasks_task
|
||||
@@ -118,6 +99,9 @@ class AutoTask(APIView):
|
||||
|
||||
task = get_object_or_404(AutomatedTask, pk=pk)
|
||||
|
||||
if task.agent and not _has_perm_on_agent(request.user, task.agent.agent_id):
|
||||
raise PermissionDenied()
|
||||
|
||||
if task.agent:
|
||||
delete_win_task_schedule.delay(pk=task.pk)
|
||||
elif task.policy:
|
||||
@@ -127,11 +111,16 @@ class AutoTask(APIView):
|
||||
return Response(f"{task.name} will be deleted shortly")
|
||||
|
||||
|
||||
@api_view()
|
||||
@permission_classes([IsAuthenticated, RunAutoTaskPerms])
|
||||
def run_task(request, pk):
|
||||
from autotasks.tasks import run_win_task
|
||||
class RunAutoTask(APIView):
|
||||
permission_classes = [IsAuthenticated, RunAutoTaskPerms]
|
||||
|
||||
task = get_object_or_404(AutomatedTask, pk=pk)
|
||||
run_win_task.delay(pk=pk)
|
||||
return Response(f"{task.name} will now be run on {task.agent.hostname}")
|
||||
def post(self, request, pk):
|
||||
from autotasks.tasks import run_win_task
|
||||
|
||||
task = get_object_or_404(AutomatedTask, pk=pk)
|
||||
|
||||
if task.agent and not _has_perm_on_agent(request.user, task.agent.agent_id):
|
||||
raise PermissionDenied()
|
||||
|
||||
run_win_task.delay(pk=pk)
|
||||
return Response(f"{task.name} will now be run on {task.agent.hostname}")
|
||||
|
||||
23
api/tacticalrmm/checks/migrations/0025_auto_20210917_1954.py
Normal file
23
api/tacticalrmm/checks/migrations/0025_auto_20210917_1954.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.2.6 on 2021-09-17 19:54
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("checks", "0024_auto_20210606_1632"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="check",
|
||||
name="created_by",
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="check",
|
||||
name="modified_by",
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
]
|
||||
@@ -12,6 +12,7 @@ from django.contrib.postgres.fields import ArrayField
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models
|
||||
from logs.models import BaseAuditModel
|
||||
from tacticalrmm.models import PermissionQuerySet
|
||||
|
||||
CHECK_TYPE_CHOICES = [
|
||||
("diskspace", "Disk Space Check"),
|
||||
@@ -50,6 +51,7 @@ EVT_LOG_FAIL_WHEN_CHOICES = [
|
||||
|
||||
|
||||
class Check(BaseAuditModel):
|
||||
objects = PermissionQuerySet.as_manager()
|
||||
|
||||
# common fields
|
||||
|
||||
@@ -230,16 +232,16 @@ class Check(BaseAuditModel):
|
||||
|
||||
return self.last_run
|
||||
|
||||
@property
|
||||
def non_editable_fields(self) -> list[str]:
|
||||
@staticmethod
|
||||
def non_editable_fields() -> list[str]:
|
||||
return [
|
||||
"check_type",
|
||||
"status",
|
||||
"more_info",
|
||||
"last_run",
|
||||
"fail_count",
|
||||
"outage_history",
|
||||
"extra_details",
|
||||
"status",
|
||||
"stdout",
|
||||
"stderr",
|
||||
"retcode",
|
||||
@@ -457,7 +459,7 @@ class Check(BaseAuditModel):
|
||||
|
||||
elif self.status == "passing":
|
||||
self.fail_count = 0
|
||||
self.save(update_fields=["status", "fail_count", "alert_severity"])
|
||||
self.save()
|
||||
if Alert.objects.filter(assigned_check=self, resolved=False).exists():
|
||||
Alert.handle_alert_resolve(self)
|
||||
|
||||
@@ -475,21 +477,6 @@ class Check(BaseAuditModel):
|
||||
|
||||
return CheckAuditSerializer(check).data
|
||||
|
||||
# for policy diskchecks
|
||||
@staticmethod
|
||||
def all_disks():
|
||||
return [f"{i}:" for i in string.ascii_uppercase]
|
||||
|
||||
# for policy service checks
|
||||
@staticmethod
|
||||
def load_default_services():
|
||||
with open(
|
||||
os.path.join(settings.BASE_DIR, "services/default_services.json")
|
||||
) as f:
|
||||
default_services = json.load(f)
|
||||
|
||||
return default_services
|
||||
|
||||
def create_policy_check(self, agent=None, policy=None):
|
||||
|
||||
if (not agent and not policy) or (agent and policy):
|
||||
@@ -684,10 +671,12 @@ class Check(BaseAuditModel):
|
||||
|
||||
|
||||
class CheckHistory(models.Model):
|
||||
objects = PermissionQuerySet.as_manager()
|
||||
|
||||
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.x
|
||||
return str(self.x)
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
from rest_framework import permissions
|
||||
|
||||
from tacticalrmm.permissions import _has_perm
|
||||
from tacticalrmm.permissions import _has_perm, _has_perm_on_agent
|
||||
|
||||
|
||||
class ManageChecksPerms(permissions.BasePermission):
|
||||
class ChecksPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
if r.method == "GET":
|
||||
return True
|
||||
|
||||
return _has_perm(r, "can_manage_checks")
|
||||
if r.method == "GET" or r.method == "PATCH":
|
||||
if "agent_id" in view.kwargs.keys():
|
||||
return _has_perm(r, "can_list_checks") and _has_perm_on_agent(
|
||||
r.user, view.kwargs["agent_id"]
|
||||
)
|
||||
else:
|
||||
return _has_perm(r, "can_list_checks")
|
||||
else:
|
||||
return _has_perm(r, "can_manage_checks")
|
||||
|
||||
|
||||
class RunChecksPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
return _has_perm(r, "can_run_checks")
|
||||
return _has_perm(r, "can_run_checks") and _has_perm_on_agent(
|
||||
r.user, view.kwargs["agent_id"]
|
||||
)
|
||||
|
||||
@@ -3,7 +3,7 @@ import validators as _v
|
||||
from rest_framework import serializers
|
||||
|
||||
from autotasks.models import AutomatedTask
|
||||
from scripts.serializers import ScriptCheckSerializer, ScriptSerializer
|
||||
from scripts.serializers import ScriptCheckSerializer
|
||||
|
||||
from .models import Check, CheckHistory
|
||||
from scripts.models import Script
|
||||
@@ -18,7 +18,6 @@ class AssignedTaskField(serializers.ModelSerializer):
|
||||
class CheckSerializer(serializers.ModelSerializer):
|
||||
|
||||
readable_desc = serializers.ReadOnlyField()
|
||||
script = ScriptSerializer(read_only=True)
|
||||
assigned_task = serializers.SerializerMethodField()
|
||||
last_run = serializers.ReadOnlyField(source="last_run_as_timezone")
|
||||
history_info = serializers.ReadOnlyField()
|
||||
@@ -57,6 +56,11 @@ class CheckSerializer(serializers.ModelSerializer):
|
||||
def validate(self, val):
|
||||
try:
|
||||
check_type = val["check_type"]
|
||||
filter = (
|
||||
{"agent": val["agent"]}
|
||||
if "agent" in val.keys()
|
||||
else {"policy": val["policy"]}
|
||||
)
|
||||
except KeyError:
|
||||
return val
|
||||
|
||||
@@ -65,7 +69,7 @@ class CheckSerializer(serializers.ModelSerializer):
|
||||
if check_type == "diskspace":
|
||||
if not self.instance: # only on create
|
||||
checks = (
|
||||
Check.objects.filter(**self.context)
|
||||
Check.objects.filter(**filter)
|
||||
.filter(check_type="diskspace")
|
||||
.exclude(managed_by_policy=True)
|
||||
)
|
||||
@@ -102,7 +106,7 @@ class CheckSerializer(serializers.ModelSerializer):
|
||||
|
||||
if check_type == "cpuload" and not self.instance:
|
||||
if (
|
||||
Check.objects.filter(**self.context, check_type="cpuload")
|
||||
Check.objects.filter(**filter, check_type="cpuload")
|
||||
.exclude(managed_by_policy=True)
|
||||
.exists()
|
||||
):
|
||||
@@ -126,7 +130,7 @@ class CheckSerializer(serializers.ModelSerializer):
|
||||
|
||||
if check_type == "memory" and not self.instance:
|
||||
if (
|
||||
Check.objects.filter(**self.context, check_type="memory")
|
||||
Check.objects.filter(**filter, check_type="memory")
|
||||
.exclude(managed_by_policy=True)
|
||||
.exists()
|
||||
):
|
||||
|
||||
@@ -8,21 +8,46 @@ from tacticalrmm.test import TacticalTestCase
|
||||
|
||||
from .serializers import CheckSerializer
|
||||
|
||||
base_url = "/checks"
|
||||
|
||||
|
||||
class TestCheckViews(TacticalTestCase):
|
||||
def setUp(self):
|
||||
self.authenticate()
|
||||
self.setup_coresettings()
|
||||
|
||||
def test_get_checks(self):
|
||||
url = f"{base_url}/"
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
baker.make("checks.Check", agent=agent, _quantity=4)
|
||||
baker.make("checks.Check", _quantity=4)
|
||||
|
||||
resp = self.client.get(url, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(len(resp.data), 8) # type: ignore
|
||||
|
||||
# test checks agent url
|
||||
url = f"/agents/{agent.agent_id}/checks/"
|
||||
resp = self.client.get(url, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(len(resp.data), 4) # type: ignore
|
||||
|
||||
# test agent doesn't exist
|
||||
url = f"/agents/jh3498uf8fkh4ro8hfd8df98/checks/"
|
||||
resp = self.client.get(url, format="json")
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
self.check_not_authenticated("get", url)
|
||||
|
||||
def test_delete_agent_check(self):
|
||||
# setup data
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
check = baker.make_recipe("checks.diskspace_check", agent=agent)
|
||||
|
||||
resp = self.client.delete("/checks/500/check/", format="json")
|
||||
resp = self.client.delete(f"{base_url}/500/", format="json")
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
url = f"/checks/{check.pk}/check/"
|
||||
url = f"{base_url}/{check.pk}/"
|
||||
|
||||
resp = self.client.delete(url, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
@@ -30,11 +55,11 @@ class TestCheckViews(TacticalTestCase):
|
||||
|
||||
self.check_not_authenticated("delete", url)
|
||||
|
||||
def test_get_disk_check(self):
|
||||
def test_get_check(self):
|
||||
# setup data
|
||||
disk_check = baker.make_recipe("checks.diskspace_check")
|
||||
|
||||
url = f"/checks/{disk_check.pk}/check/"
|
||||
url = f"{base_url}/{disk_check.pk}/"
|
||||
|
||||
resp = self.client.get(url, format="json")
|
||||
serializer = CheckSerializer(disk_check)
|
||||
@@ -46,296 +71,161 @@ class TestCheckViews(TacticalTestCase):
|
||||
def test_add_disk_check(self):
|
||||
# setup data
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
policy = baker.make("automation.Policy")
|
||||
|
||||
url = "/checks/checks/"
|
||||
url = f"{base_url}/"
|
||||
|
||||
valid_payload = {
|
||||
"pk": agent.pk,
|
||||
"check": {
|
||||
"check_type": "diskspace",
|
||||
"disk": "C:",
|
||||
"error_threshold": 55,
|
||||
"warning_threshold": 0,
|
||||
"fails_b4_alert": 3,
|
||||
},
|
||||
agent_payload = {
|
||||
"agent": agent.agent_id,
|
||||
"check_type": "diskspace",
|
||||
"disk": "C:",
|
||||
"error_threshold": 55,
|
||||
"warning_threshold": 0,
|
||||
"fails_b4_alert": 3,
|
||||
}
|
||||
|
||||
resp = self.client.post(url, valid_payload, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# this should fail because we already have a check for drive C: in setup
|
||||
invalid_payload = {
|
||||
"pk": agent.pk,
|
||||
"check": {
|
||||
"check_type": "diskspace",
|
||||
"disk": "C:",
|
||||
"error_threshold": 55,
|
||||
"warning_threshold": 0,
|
||||
"fails_b4_alert": 3,
|
||||
},
|
||||
policy_payload = {
|
||||
"policy": policy.id,
|
||||
"check_type": "diskspace",
|
||||
"disk": "C:",
|
||||
"error_threshold": 55,
|
||||
"warning_threshold": 0,
|
||||
"fails_b4_alert": 3,
|
||||
}
|
||||
|
||||
resp = self.client.post(url, invalid_payload, format="json")
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
for payload in [agent_payload, policy_payload]:
|
||||
|
||||
# this should fail because both error and warning threshold are 0
|
||||
invalid_payload = {
|
||||
"pk": agent.pk,
|
||||
"check": {
|
||||
"check_type": "diskspace",
|
||||
"disk": "C:",
|
||||
"error_threshold": 0,
|
||||
"warning_threshold": 0,
|
||||
"fails_b4_alert": 3,
|
||||
},
|
||||
}
|
||||
# add valid check
|
||||
resp = self.client.post(url, payload, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
resp = self.client.post(url, invalid_payload, format="json")
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
# this should fail since we just added it
|
||||
resp = self.client.post(url, payload, format="json")
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
|
||||
# this should fail because both error is greater than warning threshold
|
||||
invalid_payload = {
|
||||
"pk": agent.pk,
|
||||
"check": {
|
||||
"check_type": "diskspace",
|
||||
"disk": "C:",
|
||||
"error_threshold": 50,
|
||||
"warning_threshold": 30,
|
||||
"fails_b4_alert": 3,
|
||||
},
|
||||
}
|
||||
# this should fail because both error and warning threshold are 0
|
||||
payload["error_threshold"] = 0
|
||||
payload["warning_threshold"] = 0
|
||||
|
||||
resp = self.client.post(url, invalid_payload, format="json")
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
resp = self.client.post(url, payload, format="json")
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
|
||||
# this should fail because error threshold is greater than warning threshold
|
||||
payload["error_threshold"] = 50
|
||||
payload["warning_threshold"] = 30
|
||||
|
||||
resp = self.client.post(url, payload, format="json")
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
|
||||
self.check_not_authenticated("post", url)
|
||||
|
||||
def test_add_cpuload_check(self):
|
||||
url = "/checks/checks/"
|
||||
url = f"{base_url}/"
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
payload = {
|
||||
"pk": agent.pk,
|
||||
"check": {
|
||||
"check_type": "cpuload",
|
||||
"error_threshold": 66,
|
||||
"warning_threshold": 0,
|
||||
"fails_b4_alert": 9,
|
||||
},
|
||||
policy = baker.make("automation.Policy")
|
||||
|
||||
agent_payload = {
|
||||
"agent": agent.agent_id,
|
||||
"check_type": "cpuload",
|
||||
"error_threshold": 66,
|
||||
"warning_threshold": 0,
|
||||
"fails_b4_alert": 9,
|
||||
}
|
||||
|
||||
resp = self.client.post(url, payload, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
payload["error_threshold"] = 87
|
||||
resp = self.client.post(url, payload, format="json")
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
self.assertEqual(
|
||||
resp.json()["non_field_errors"][0],
|
||||
"A cpuload check for this agent already exists",
|
||||
)
|
||||
|
||||
# should fail because both error and warning thresholds are 0
|
||||
invalid_payload = {
|
||||
"pk": agent.pk,
|
||||
"check": {
|
||||
"check_type": "cpuload",
|
||||
"error_threshold": 0,
|
||||
"warning_threshold": 0,
|
||||
"fails_b4_alert": 9,
|
||||
},
|
||||
policy_payload = {
|
||||
"policy": policy.id,
|
||||
"check_type": "cpuload",
|
||||
"error_threshold": 66,
|
||||
"warning_threshold": 0,
|
||||
"fails_b4_alert": 9,
|
||||
}
|
||||
|
||||
resp = self.client.post(url, invalid_payload, format="json")
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
for payload in [agent_payload, policy_payload]:
|
||||
|
||||
# should fail because error is less than warning
|
||||
invalid_payload = {
|
||||
"pk": agent.pk,
|
||||
"check": {
|
||||
"check_type": "cpuload",
|
||||
"error_threshold": 10,
|
||||
"warning_threshold": 50,
|
||||
"fails_b4_alert": 9,
|
||||
},
|
||||
}
|
||||
# add cpu check
|
||||
resp = self.client.post(url, payload, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
resp = self.client.post(url, invalid_payload, format="json")
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
# should fail since cpu check already exists
|
||||
resp = self.client.post(url, payload, format="json")
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
|
||||
# this should fail because both error and warning threshold are 0
|
||||
payload["error_threshold"] = 0
|
||||
payload["warning_threshold"] = 0
|
||||
|
||||
resp = self.client.post(url, payload, format="json")
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
|
||||
# this should fail because error threshold is less than warning threshold
|
||||
payload["error_threshold"] = 20
|
||||
payload["warning_threshold"] = 30
|
||||
|
||||
resp = self.client.post(url, payload, format="json")
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
|
||||
self.check_not_authenticated("post", url)
|
||||
|
||||
def test_add_memory_check(self):
|
||||
url = "/checks/checks/"
|
||||
url = f"{base_url}/"
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
payload = {
|
||||
"pk": agent.pk,
|
||||
"check": {
|
||||
"check_type": "memory",
|
||||
"error_threshold": 78,
|
||||
"warning_threshold": 0,
|
||||
"fails_b4_alert": 1,
|
||||
},
|
||||
}
|
||||
|
||||
resp = self.client.post(url, payload, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
payload["error_threshold"] = 55
|
||||
resp = self.client.post(url, payload, format="json")
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
self.assertEqual(
|
||||
resp.json()["non_field_errors"][0],
|
||||
"A memory check for this agent already exists",
|
||||
)
|
||||
|
||||
# should fail because both error and warning thresholds are 0
|
||||
invalid_payload = {
|
||||
"pk": agent.pk,
|
||||
"check": {
|
||||
"check_type": "memory",
|
||||
"error_threshold": 0,
|
||||
"warning_threshold": 0,
|
||||
"fails_b4_alert": 9,
|
||||
},
|
||||
}
|
||||
|
||||
resp = self.client.post(url, invalid_payload, format="json")
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
|
||||
# should fail because error is less than warning
|
||||
invalid_payload = {
|
||||
"pk": agent.pk,
|
||||
"check": {
|
||||
"check_type": "memory",
|
||||
"error_threshold": 10,
|
||||
"warning_threshold": 50,
|
||||
"fails_b4_alert": 9,
|
||||
},
|
||||
}
|
||||
|
||||
resp = self.client.post(url, invalid_payload, format="json")
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
|
||||
def test_get_policy_disk_check(self):
|
||||
# setup data
|
||||
policy = baker.make("automation.Policy")
|
||||
disk_check = baker.make_recipe("checks.diskspace_check", policy=policy)
|
||||
|
||||
url = f"/checks/{disk_check.pk}/check/"
|
||||
agent_payload = {
|
||||
"agent": agent.agent_id,
|
||||
"check_type": "memory",
|
||||
"error_threshold": 78,
|
||||
"warning_threshold": 0,
|
||||
"fails_b4_alert": 1,
|
||||
}
|
||||
|
||||
resp = self.client.get(url, format="json")
|
||||
serializer = CheckSerializer(disk_check)
|
||||
policy_payload = {
|
||||
"policy": policy.id,
|
||||
"check_type": "memory",
|
||||
"error_threshold": 78,
|
||||
"warning_threshold": 0,
|
||||
"fails_b4_alert": 1,
|
||||
}
|
||||
|
||||
for payload in [agent_payload, policy_payload]:
|
||||
|
||||
# add memory check
|
||||
resp = self.client.post(url, payload, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# should fail since cpu check already exists
|
||||
resp = self.client.post(url, payload, format="json")
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
|
||||
# this should fail because both error and warning threshold are 0
|
||||
payload["error_threshold"] = 0
|
||||
payload["warning_threshold"] = 0
|
||||
|
||||
resp = self.client.post(url, payload, format="json")
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
|
||||
# this should fail because error threshold is less than warning threshold
|
||||
payload["error_threshold"] = 20
|
||||
payload["warning_threshold"] = 30
|
||||
|
||||
resp = self.client.post(url, payload, format="json")
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(resp.data, serializer.data) # type: ignore
|
||||
self.check_not_authenticated("post", url)
|
||||
|
||||
def test_add_policy_disk_check(self):
|
||||
# setup data
|
||||
policy = baker.make("automation.Policy")
|
||||
|
||||
url = "/checks/checks/"
|
||||
|
||||
valid_payload = {
|
||||
"policy": policy.pk, # type: ignore
|
||||
"check": {
|
||||
"check_type": "diskspace",
|
||||
"disk": "M:",
|
||||
"error_threshold": 86,
|
||||
"warning_threshold": 0,
|
||||
"fails_b4_alert": 2,
|
||||
},
|
||||
}
|
||||
|
||||
# should fail because both error and warning thresholds are 0
|
||||
invalid_payload = {
|
||||
"policy": policy.pk, # type: ignore
|
||||
"check": {
|
||||
"check_type": "diskspace",
|
||||
"error_threshold": 0,
|
||||
"warning_threshold": 0,
|
||||
"fails_b4_alert": 9,
|
||||
},
|
||||
}
|
||||
|
||||
resp = self.client.post(url, invalid_payload, format="json")
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
|
||||
# should fail because warning is less than error
|
||||
invalid_payload = {
|
||||
"policy": policy.pk, # type: ignore
|
||||
"check": {
|
||||
"check_type": "diskspace",
|
||||
"error_threshold": 80,
|
||||
"warning_threshold": 50,
|
||||
"fails_b4_alert": 9,
|
||||
},
|
||||
}
|
||||
|
||||
resp = self.client.post(url, valid_payload, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# this should fail because we already have a check for drive M: in setup
|
||||
invalid_payload = {
|
||||
"policy": policy.pk, # type: ignore
|
||||
"check": {
|
||||
"check_type": "diskspace",
|
||||
"disk": "M:",
|
||||
"error_threshold": 34,
|
||||
"warning_threshold": 0,
|
||||
"fails_b4_alert": 9,
|
||||
},
|
||||
}
|
||||
|
||||
resp = self.client.post(url, invalid_payload, format="json")
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
|
||||
def test_get_disks_for_policies(self):
|
||||
url = "/checks/getalldisks/"
|
||||
r = self.client.get(url)
|
||||
self.assertIsInstance(r.data, list) # type: ignore
|
||||
self.assertEqual(26, len(r.data)) # type: ignore
|
||||
|
||||
def test_edit_check_alert(self):
|
||||
# setup data
|
||||
policy = baker.make("automation.Policy")
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
|
||||
policy_disk_check = baker.make_recipe("checks.diskspace_check", policy=policy)
|
||||
agent_disk_check = baker.make_recipe("checks.diskspace_check", agent=agent)
|
||||
url_a = f"/checks/{agent_disk_check.pk}/check/"
|
||||
url_p = f"/checks/{policy_disk_check.pk}/check/"
|
||||
|
||||
valid_payload = {"email_alert": False, "check_alert": True}
|
||||
invalid_payload = {"email_alert": False}
|
||||
|
||||
with self.assertRaises(KeyError) as err:
|
||||
resp = self.client.patch(url_a, invalid_payload, format="json")
|
||||
|
||||
with self.assertRaises(KeyError) as err:
|
||||
resp = self.client.patch(url_p, invalid_payload, format="json")
|
||||
|
||||
resp = self.client.patch(url_a, valid_payload, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
resp = self.client.patch(url_p, valid_payload, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
self.check_not_authenticated("patch", url_a)
|
||||
|
||||
@patch("agents.models.Agent.nats_cmd")
|
||||
def test_run_checks(self, nats_cmd):
|
||||
agent = baker.make_recipe("agents.agent", version="1.4.1")
|
||||
agent_b4_141 = baker.make_recipe("agents.agent", version="1.4.0")
|
||||
|
||||
url = f"/checks/runchecks/{agent_b4_141.pk}/"
|
||||
url = f"{base_url}/{agent_b4_141.agent_id}/run/"
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
nats_cmd.assert_called_with({"func": "runchecks"}, wait=False)
|
||||
|
||||
nats_cmd.reset_mock()
|
||||
nats_cmd.return_value = "busy"
|
||||
url = f"/checks/runchecks/{agent.pk}/"
|
||||
url = f"{base_url}/{agent.agent_id}/run/"
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 400)
|
||||
nats_cmd.assert_called_with({"func": "runchecks"}, timeout=15)
|
||||
@@ -343,7 +233,7 @@ class TestCheckViews(TacticalTestCase):
|
||||
|
||||
nats_cmd.reset_mock()
|
||||
nats_cmd.return_value = "ok"
|
||||
url = f"/checks/runchecks/{agent.pk}/"
|
||||
url = f"{base_url}/{agent.agent_id}/run/"
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
nats_cmd.assert_called_with({"func": "runchecks"}, timeout=15)
|
||||
@@ -351,7 +241,7 @@ class TestCheckViews(TacticalTestCase):
|
||||
|
||||
nats_cmd.reset_mock()
|
||||
nats_cmd.return_value = "timeout"
|
||||
url = f"/checks/runchecks/{agent.pk}/"
|
||||
url = f"{base_url}/{agent.agent_id}/run/"
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 400)
|
||||
nats_cmd.assert_called_with({"func": "runchecks"}, timeout=15)
|
||||
@@ -379,7 +269,7 @@ class TestCheckViews(TacticalTestCase):
|
||||
resp = self.client.patch("/checks/history/500/", format="json")
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
url = f"/checks/history/{check.id}/"
|
||||
url = f"/checks/{check.id}/history/"
|
||||
|
||||
# test with timeFilter last 30 days
|
||||
data = {"timeFilter": 30}
|
||||
@@ -873,74 +763,7 @@ class TestCheckTasks(TacticalTestCase):
|
||||
self.assertEqual(new_check.status, "failing")
|
||||
self.assertEqual(new_check.alert_severity, "info")
|
||||
|
||||
""" # test failing and attempt start
|
||||
winsvc.restart_if_stopped = True
|
||||
winsvc.alert_severity = "warning"
|
||||
winsvc.save()
|
||||
|
||||
nats_cmd.return_value = "timeout"
|
||||
|
||||
data = {"id": winsvc.id, "exists": True, "status": "not running"}
|
||||
|
||||
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, "failing")
|
||||
self.assertEqual(new_check.alert_severity, "warning")
|
||||
nats_cmd.assert_called()
|
||||
nats_cmd.reset_mock()
|
||||
|
||||
# test failing and attempt start
|
||||
winsvc.alert_severity = "error"
|
||||
winsvc.save()
|
||||
nats_cmd.return_value = {"success": False, "errormsg": "Some Error"}
|
||||
|
||||
data = {"id": winsvc.id, "exists": True, "status": "not running"}
|
||||
|
||||
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, "failing")
|
||||
self.assertEqual(new_check.alert_severity, "error")
|
||||
nats_cmd.assert_called()
|
||||
nats_cmd.reset_mock()
|
||||
|
||||
# test success and attempt start
|
||||
nats_cmd.return_value = {"success": True}
|
||||
|
||||
data = {"id": winsvc.id, "exists": True, "status": "not running"}
|
||||
|
||||
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")
|
||||
nats_cmd.assert_called()
|
||||
nats_cmd.reset_mock()
|
||||
|
||||
# test failing and service not exist
|
||||
data = {"id": winsvc.id, "exists": False, "status": ""}
|
||||
|
||||
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, "failing")
|
||||
|
||||
# test success and service not exist
|
||||
winsvc.pass_if_svc_not_exist = True
|
||||
winsvc.save()
|
||||
data = {"id": winsvc.id, "exists": False, "status": ""}
|
||||
|
||||
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") """
|
||||
|
||||
""" def test_handle_eventlog_check(self):
|
||||
def test_handle_eventlog_check(self):
|
||||
from checks.models import Check
|
||||
|
||||
url = "/api/v3/checkrunner/"
|
||||
@@ -984,6 +807,8 @@ class TestCheckTasks(TacticalTestCase):
|
||||
],
|
||||
}
|
||||
|
||||
no_logs_data = {"id": eventlog.id, "log": []}
|
||||
|
||||
# test failing when contains
|
||||
resp = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
@@ -993,11 +818,8 @@ class TestCheckTasks(TacticalTestCase):
|
||||
self.assertEquals(new_check.alert_severity, "warning")
|
||||
self.assertEquals(new_check.status, "failing")
|
||||
|
||||
# test passing when not contains and message
|
||||
eventlog.event_message = "doesnt exist"
|
||||
eventlog.save()
|
||||
|
||||
resp = self.client.patch(url, data, format="json")
|
||||
# test passing when contains
|
||||
resp = self.client.patch(url, no_logs_data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
new_check = Check.objects.get(pk=eventlog.id)
|
||||
@@ -1007,11 +829,9 @@ class TestCheckTasks(TacticalTestCase):
|
||||
# test failing when not contains and message and source
|
||||
eventlog.fail_when = "not_contains"
|
||||
eventlog.alert_severity = "error"
|
||||
eventlog.event_message = "doesnt exist"
|
||||
eventlog.event_source = "doesnt exist"
|
||||
eventlog.save()
|
||||
|
||||
resp = self.client.patch(url, data, format="json")
|
||||
resp = self.client.patch(url, no_logs_data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
new_check = Check.objects.get(pk=eventlog.id)
|
||||
@@ -1020,10 +840,6 @@ class TestCheckTasks(TacticalTestCase):
|
||||
self.assertEquals(new_check.alert_severity, "error")
|
||||
|
||||
# test passing when contains with source and message
|
||||
eventlog.event_message = "test"
|
||||
eventlog.event_source = "source"
|
||||
eventlog.save()
|
||||
|
||||
resp = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
@@ -1031,115 +847,261 @@ class TestCheckTasks(TacticalTestCase):
|
||||
|
||||
self.assertEquals(new_check.status, "passing")
|
||||
|
||||
# test failing with wildcard not contains and source
|
||||
eventlog.event_id_is_wildcard = True
|
||||
eventlog.event_source = "doesn't exist"
|
||||
eventlog.event_message = ""
|
||||
eventlog.event_id = 0
|
||||
eventlog.save()
|
||||
|
||||
resp = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
class TestCheckPermissions(TacticalTestCase):
|
||||
def setUp(self):
|
||||
self.setup_coresettings()
|
||||
self.client_setup()
|
||||
|
||||
new_check = Check.objects.get(pk=eventlog.id)
|
||||
def test_get_checks_permissions(self):
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
policy = baker.make("automation.Policy")
|
||||
unauthorized_agent = baker.make_recipe("agents.agent")
|
||||
check = baker.make("checks.Check", agent=agent, _quantity=5)
|
||||
unauthorized_check = baker.make(
|
||||
"checks.Check", agent=unauthorized_agent, _quantity=7
|
||||
)
|
||||
|
||||
self.assertEquals(new_check.status, "failing")
|
||||
self.assertEquals(new_check.alert_severity, "error")
|
||||
policy_checks = baker.make("checks.Check", policy=policy, _quantity=2)
|
||||
|
||||
# test passing with wildcard contains
|
||||
eventlog.event_source = ""
|
||||
eventlog.event_message = ""
|
||||
eventlog.save()
|
||||
# test super user access
|
||||
self.check_authorized_superuser("get", f"{base_url}/")
|
||||
self.check_authorized_superuser("get", f"/agents/{agent.agent_id}/checks/")
|
||||
self.check_authorized_superuser(
|
||||
"get", f"/agents/{unauthorized_agent.agent_id}/checks/"
|
||||
)
|
||||
self.check_authorized_superuser(
|
||||
"get", f"/automation/policies/{policy.id}/checks/"
|
||||
)
|
||||
|
||||
resp = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
user = self.create_user_with_roles([])
|
||||
self.client.force_authenticate(user=user) # type: ignore
|
||||
|
||||
new_check = Check.objects.get(pk=eventlog.id)
|
||||
self.check_not_authorized("get", f"{base_url}/")
|
||||
self.check_not_authorized("get", f"/agents/{agent.agent_id}/checks/")
|
||||
self.check_not_authorized(
|
||||
"get", f"/agents/{unauthorized_agent.agent_id}/checks/"
|
||||
)
|
||||
self.check_not_authorized("get", f"/automation/policies/{policy.id}/checks/")
|
||||
|
||||
self.assertEquals(new_check.status, "passing")
|
||||
# add list software role to user
|
||||
user.role.can_list_checks = True
|
||||
user.role.save()
|
||||
|
||||
# test failing with wildcard contains and message
|
||||
eventlog.fail_when = "contains"
|
||||
eventlog.event_type = "error"
|
||||
eventlog.alert_severity = "info"
|
||||
eventlog.event_message = "test"
|
||||
eventlog.event_source = ""
|
||||
eventlog.save()
|
||||
r = self.check_authorized("get", f"{base_url}/")
|
||||
self.assertEqual(len(r.data), 14) # type: ignore
|
||||
r = self.check_authorized("get", f"/agents/{agent.agent_id}/checks/")
|
||||
self.assertEqual(len(r.data), 5) # type: ignore
|
||||
r = self.check_authorized(
|
||||
"get", f"/agents/{unauthorized_agent.agent_id}/checks/"
|
||||
)
|
||||
self.assertEqual(len(r.data), 7) # type: ignore
|
||||
r = self.check_authorized("get", f"/automation/policies/{policy.id}/checks/")
|
||||
self.assertEqual(len(r.data), 2) # type: ignore
|
||||
|
||||
resp = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
# test limiting to client
|
||||
user.role.can_view_clients.set([agent.client])
|
||||
self.check_not_authorized(
|
||||
"get", f"/agents/{unauthorized_agent.agent_id}/checks/"
|
||||
)
|
||||
self.check_authorized("get", f"/agents/{agent.agent_id}/checks/")
|
||||
self.check_authorized("get", f"/automation/policies/{policy.id}/checks/")
|
||||
|
||||
new_check = Check.objects.get(pk=eventlog.id)
|
||||
# make sure queryset is limited too
|
||||
r = self.client.get(f"{base_url}/")
|
||||
self.assertEqual(len(r.data), 7) # type: ignore
|
||||
|
||||
self.assertEquals(new_check.status, "failing")
|
||||
self.assertEquals(new_check.alert_severity, "info")
|
||||
def test_add_check_permissions(self):
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
unauthorized_agent = baker.make_recipe("agents.agent")
|
||||
policy = baker.make("automation.Policy")
|
||||
|
||||
# test passing with wildcard not contains message and source
|
||||
eventlog.event_message = "doesnt exist"
|
||||
eventlog.event_source = "doesnt exist"
|
||||
eventlog.save()
|
||||
policy_data = {
|
||||
"policy": policy.id,
|
||||
"check_type": "diskspace",
|
||||
"disk": "C:",
|
||||
"error_threshold": 55,
|
||||
"warning_threshold": 0,
|
||||
"fails_b4_alert": 3,
|
||||
}
|
||||
|
||||
resp = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
agent_data = {
|
||||
"agent": agent.agent_id,
|
||||
"check_type": "diskspace",
|
||||
"disk": "C:",
|
||||
"error_threshold": 55,
|
||||
"warning_threshold": 0,
|
||||
"fails_b4_alert": 3,
|
||||
}
|
||||
|
||||
new_check = Check.objects.get(pk=eventlog.id)
|
||||
unauthorized_agent_data = {
|
||||
"agent": unauthorized_agent.agent_id,
|
||||
"check_type": "diskspace",
|
||||
"disk": "C:",
|
||||
"error_threshold": 55,
|
||||
"warning_threshold": 0,
|
||||
"fails_b4_alert": 3,
|
||||
}
|
||||
|
||||
self.assertEquals(new_check.status, "passing")
|
||||
url = f"{base_url}/"
|
||||
|
||||
# test multiple events found and contains
|
||||
# this should pass since only two events are found
|
||||
eventlog.number_of_events_b4_alert = 3
|
||||
eventlog.event_id_is_wildcard = False
|
||||
eventlog.event_source = None
|
||||
eventlog.event_message = None
|
||||
eventlog.event_id = 123
|
||||
eventlog.event_type = "error"
|
||||
eventlog.fail_when = "contains"
|
||||
eventlog.save()
|
||||
for data in [policy_data, agent_data]:
|
||||
# test superuser access
|
||||
self.check_authorized_superuser("post", url, data)
|
||||
|
||||
resp = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
user = self.create_user_with_roles([])
|
||||
self.client.force_authenticate(user=user) # type: ignore
|
||||
|
||||
new_check = Check.objects.get(pk=eventlog.id)
|
||||
# test user without role
|
||||
self.check_not_authorized("post", url, data)
|
||||
|
||||
self.assertEquals(new_check.status, "passing")
|
||||
# add user to role and test
|
||||
setattr(user.role, "can_manage_checks", True)
|
||||
user.role.save()
|
||||
|
||||
# this should pass since there are two events returned
|
||||
eventlog.number_of_events_b4_alert = 2
|
||||
eventlog.save()
|
||||
self.check_authorized("post", url, data)
|
||||
|
||||
resp = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
# limit user to client
|
||||
user.role.can_view_clients.set([agent.client])
|
||||
if "agent" in data.keys():
|
||||
self.check_authorized("post", url, data)
|
||||
self.check_not_authorized("post", url, unauthorized_agent_data)
|
||||
else:
|
||||
self.check_authorized("post", url, data)
|
||||
|
||||
new_check = Check.objects.get(pk=eventlog.id)
|
||||
# mock the check delete method so it actually isn't deleted
|
||||
@patch("checks.models.Check.delete")
|
||||
def test_check_get_edit_delete_permissions(self, delete_check):
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
unauthorized_agent = baker.make_recipe("agents.agent")
|
||||
policy = baker.make("automation.Policy")
|
||||
check = baker.make("checks.Check", agent=agent)
|
||||
unauthorized_check = baker.make("checks.Check", agent=unauthorized_agent)
|
||||
policy_check = baker.make("checks.Check", policy=policy)
|
||||
|
||||
self.assertEquals(new_check.status, "failing")
|
||||
for method in ["get", "put", "delete"]:
|
||||
|
||||
# test not contains
|
||||
# this should fail since only two events are found
|
||||
eventlog.number_of_events_b4_alert = 3
|
||||
eventlog.event_id_is_wildcard = False
|
||||
eventlog.event_source = None
|
||||
eventlog.event_message = None
|
||||
eventlog.event_id = 123
|
||||
eventlog.event_type = "error"
|
||||
eventlog.fail_when = "not_contains"
|
||||
eventlog.save()
|
||||
url = f"{base_url}/{check.id}/"
|
||||
unauthorized_url = f"{base_url}/{unauthorized_check.id}/"
|
||||
policy_url = f"{base_url}/{policy_check.id}/"
|
||||
|
||||
resp = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
# test superuser access
|
||||
self.check_authorized_superuser(method, url)
|
||||
self.check_authorized_superuser(method, unauthorized_url)
|
||||
self.check_authorized_superuser(method, policy_url)
|
||||
|
||||
new_check = Check.objects.get(pk=eventlog.id)
|
||||
user = self.create_user_with_roles([])
|
||||
self.client.force_authenticate(user=user) # type: ignore
|
||||
|
||||
self.assertEquals(new_check.status, "failing")
|
||||
# test user without role
|
||||
self.check_not_authorized(method, url)
|
||||
self.check_not_authorized(method, unauthorized_url)
|
||||
self.check_not_authorized(method, policy_url)
|
||||
|
||||
# this should pass since there are two events returned
|
||||
eventlog.number_of_events_b4_alert = 2
|
||||
eventlog.save()
|
||||
# add user to role and test
|
||||
setattr(
|
||||
user.role,
|
||||
"can_list_checks" if method == "get" else "can_manage_checks",
|
||||
True,
|
||||
)
|
||||
user.role.save()
|
||||
|
||||
resp = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.check_authorized(method, url)
|
||||
self.check_authorized(method, unauthorized_url)
|
||||
self.check_authorized(method, policy_url)
|
||||
|
||||
new_check = Check.objects.get(pk=eventlog.id)
|
||||
# limit user to client if agent check
|
||||
user.role.can_view_clients.set([agent.client])
|
||||
|
||||
self.assertEquals(new_check.status, "passing") """
|
||||
self.check_authorized(method, url)
|
||||
self.check_not_authorized(method, unauthorized_url)
|
||||
self.check_authorized(method, policy_url)
|
||||
|
||||
def test_check_action_permissions(self):
|
||||
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
unauthorized_agent = baker.make_recipe("agents.agent")
|
||||
check = baker.make("checks.Check", agent=agent)
|
||||
unauthorized_check = baker.make("checks.Check", agent=unauthorized_agent)
|
||||
|
||||
for action in ["reset", "run"]:
|
||||
if action == "reset":
|
||||
url = f"{base_url}/{check.id}/{action}/"
|
||||
unauthorized_url = f"{base_url}/{unauthorized_check.id}/{action}/"
|
||||
else:
|
||||
url = f"{base_url}/{agent.agent_id}/{action}/"
|
||||
unauthorized_url = f"{base_url}/{unauthorized_agent.agent_id}/{action}/"
|
||||
|
||||
# test superuser access
|
||||
self.check_authorized_superuser("post", url)
|
||||
self.check_authorized_superuser("post", unauthorized_url)
|
||||
|
||||
user = self.create_user_with_roles([])
|
||||
self.client.force_authenticate(user=user) # type: ignore
|
||||
|
||||
# test user without role
|
||||
self.check_not_authorized("post", url)
|
||||
self.check_not_authorized("post", unauthorized_url)
|
||||
|
||||
# add user to role and test
|
||||
setattr(
|
||||
user.role,
|
||||
"can_manage_checks" if action == "reset" else "can_run_checks",
|
||||
True,
|
||||
)
|
||||
user.role.save()
|
||||
|
||||
self.check_authorized("post", url)
|
||||
self.check_authorized("post", unauthorized_url)
|
||||
|
||||
# limit user to client if agent check
|
||||
user.role.can_view_sites.set([agent.site])
|
||||
|
||||
self.check_authorized("post", url)
|
||||
self.check_not_authorized("post", unauthorized_url)
|
||||
|
||||
def test_check_history_permissions(self):
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
unauthorized_agent = baker.make_recipe("agents.agent")
|
||||
check = baker.make("checks.Check", agent=agent)
|
||||
unauthorized_check = baker.make("checks.Check", agent=unauthorized_agent)
|
||||
|
||||
url = f"{base_url}/{check.id}/history/"
|
||||
unauthorized_url = f"{base_url}/{unauthorized_check.id}/history/"
|
||||
|
||||
# test superuser access
|
||||
self.check_authorized_superuser("patch", url)
|
||||
self.check_authorized_superuser("patch", unauthorized_url)
|
||||
|
||||
user = self.create_user_with_roles([])
|
||||
self.client.force_authenticate(user=user) # type: ignore
|
||||
|
||||
# test user without role
|
||||
self.check_not_authorized("patch", url)
|
||||
self.check_not_authorized("patch", unauthorized_url)
|
||||
|
||||
# add user to role and test
|
||||
setattr(
|
||||
user.role,
|
||||
"can_list_checks",
|
||||
True,
|
||||
)
|
||||
user.role.save()
|
||||
|
||||
self.check_authorized("patch", url)
|
||||
self.check_authorized("patch", unauthorized_url)
|
||||
|
||||
# limit user to client if agent check
|
||||
user.role.can_view_sites.set([agent.site])
|
||||
|
||||
self.check_authorized("patch", url)
|
||||
self.check_not_authorized("patch", unauthorized_url)
|
||||
|
||||
def test_policy_fields_to_copy_exists(self):
|
||||
from .models import Check
|
||||
|
||||
fields = [i.name for i in Check._meta.get_fields()]
|
||||
check = baker.make("checks.Check")
|
||||
|
||||
for i in check.policy_fields_to_copy: # type: ignore
|
||||
self.assertIn(i, fields)
|
||||
|
||||
@@ -3,10 +3,9 @@ from django.urls import path
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path("checks/", views.AddCheck.as_view()),
|
||||
path("<int:pk>/check/", views.GetUpdateDeleteCheck.as_view()),
|
||||
path("<pk>/loadchecks/", views.load_checks),
|
||||
path("getalldisks/", views.get_disks_for_policies),
|
||||
path("runchecks/<pk>/", views.run_checks),
|
||||
path("history/<int:checkpk>/", views.GetCheckHistory.as_view()),
|
||||
path("", views.GetAddChecks.as_view()),
|
||||
path("<int:pk>/", views.GetUpdateDeleteCheck.as_view()),
|
||||
path("<int:pk>/reset/", views.ResetCheck.as_view()),
|
||||
path("<agent:agent_id>/run/", views.run_checks),
|
||||
path("<int:pk>/history/", views.GetCheckHistory.as_view()),
|
||||
]
|
||||
|
||||
@@ -9,57 +9,57 @@ 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 rest_framework.exceptions import PermissionDenied
|
||||
|
||||
from agents.models import Agent
|
||||
from automation.models import Policy
|
||||
from scripts.models import Script
|
||||
from tacticalrmm.utils import notify_error
|
||||
from tacticalrmm.permissions import _has_perm_on_agent
|
||||
|
||||
from .models import Check, CheckHistory
|
||||
from .permissions import ManageChecksPerms, RunChecksPerms
|
||||
from .permissions import ChecksPerms, RunChecksPerms
|
||||
from .serializers import CheckHistorySerializer, CheckSerializer
|
||||
|
||||
|
||||
class AddCheck(APIView):
|
||||
permission_classes = [IsAuthenticated, ManageChecksPerms]
|
||||
class GetAddChecks(APIView):
|
||||
permission_classes = [IsAuthenticated, ChecksPerms]
|
||||
|
||||
def get(self, request, agent_id=None, policy=None):
|
||||
if agent_id:
|
||||
agent = get_object_or_404(Agent, agent_id=agent_id)
|
||||
checks = Check.objects.filter(agent=agent)
|
||||
elif policy:
|
||||
policy = get_object_or_404(Policy, id=policy)
|
||||
checks = Check.objects.filter(policy=policy)
|
||||
else:
|
||||
checks = Check.objects.filter_by_role(request.user)
|
||||
return Response(CheckSerializer(checks, many=True).data)
|
||||
|
||||
def post(self, request):
|
||||
from automation.tasks import generate_agent_checks_task
|
||||
|
||||
policy = None
|
||||
agent = None
|
||||
data = request.data.copy()
|
||||
# Determine if adding check to Agent and replace agent_id with pk
|
||||
if "agent" in data.keys():
|
||||
agent = get_object_or_404(Agent, agent_id=data["agent"])
|
||||
if not _has_perm_on_agent(request.user, agent.agent_id):
|
||||
raise PermissionDenied()
|
||||
|
||||
# Determine if adding check to Policy or Agent
|
||||
if "policy" in request.data:
|
||||
policy = get_object_or_404(Policy, id=request.data["policy"])
|
||||
# Object used for filter and save
|
||||
parent = {"policy": policy}
|
||||
else:
|
||||
agent = get_object_or_404(Agent, pk=request.data["pk"])
|
||||
parent = {"agent": agent}
|
||||
|
||||
script = None
|
||||
if "script" in request.data["check"]:
|
||||
script = get_object_or_404(Script, pk=request.data["check"]["script"])
|
||||
data["agent"] = agent.pk
|
||||
|
||||
# set event id to 0 if wildcard because it needs to be an integer field for db
|
||||
# will be ignored anyway by the agent when doing wildcard check
|
||||
if (
|
||||
request.data["check"]["check_type"] == "eventlog"
|
||||
and request.data["check"]["event_id_is_wildcard"]
|
||||
):
|
||||
request.data["check"]["event_id"] = 0
|
||||
if data["check_type"] == "eventlog" and data["event_id_is_wildcard"]:
|
||||
data["event_id"] = 0
|
||||
|
||||
serializer = CheckSerializer(
|
||||
data=request.data["check"], partial=True, context=parent
|
||||
)
|
||||
serializer = CheckSerializer(data=data, partial=True)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
new_check = serializer.save(**parent, script=script)
|
||||
new_check = serializer.save()
|
||||
|
||||
# Generate policy Checks
|
||||
if policy:
|
||||
generate_agent_checks_task.delay(policy=policy.pk)
|
||||
elif agent:
|
||||
if "policy" in data.keys():
|
||||
generate_agent_checks_task.delay(policy=data["policy"])
|
||||
elif "agent" in data.keys():
|
||||
checks = agent.agentchecks.filter( # type: ignore
|
||||
check_type=new_check.check_type, managed_by_policy=True
|
||||
)
|
||||
@@ -81,44 +81,43 @@ class AddCheck(APIView):
|
||||
|
||||
|
||||
class GetUpdateDeleteCheck(APIView):
|
||||
permission_classes = [IsAuthenticated, ManageChecksPerms]
|
||||
permission_classes = [IsAuthenticated, ChecksPerms]
|
||||
|
||||
def get(self, request, pk):
|
||||
check = get_object_or_404(Check, pk=pk)
|
||||
if check.agent and not _has_perm_on_agent(request.user, check.agent.agent_id):
|
||||
raise PermissionDenied()
|
||||
|
||||
return Response(CheckSerializer(check).data)
|
||||
|
||||
def patch(self, request, pk):
|
||||
def put(self, request, pk):
|
||||
from automation.tasks import update_policy_check_fields_task
|
||||
|
||||
check = get_object_or_404(Check, pk=pk)
|
||||
|
||||
data = request.data.copy()
|
||||
|
||||
if check.agent and not _has_perm_on_agent(request.user, check.agent.agent_id):
|
||||
raise PermissionDenied()
|
||||
|
||||
# remove fields that should not be changed when editing a check from the frontend
|
||||
if (
|
||||
"check_alert" not in request.data.keys()
|
||||
and "check_reset" not in request.data.keys()
|
||||
):
|
||||
[request.data.pop(i) for i in check.non_editable_fields]
|
||||
[data.pop(i) for i in Check.non_editable_fields() if i in data.keys()]
|
||||
|
||||
# set event id to 0 if wildcard because it needs to be an integer field for db
|
||||
# will be ignored anyway by the agent when doing wildcard check
|
||||
if check.check_type == "eventlog":
|
||||
try:
|
||||
request.data["event_id_is_wildcard"]
|
||||
data["event_id_is_wildcard"]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
if request.data["event_id_is_wildcard"]:
|
||||
request.data["event_id"] = 0
|
||||
if data["event_id_is_wildcard"]:
|
||||
data["event_id"] = 0
|
||||
|
||||
serializer = CheckSerializer(instance=check, data=request.data, partial=True)
|
||||
serializer = CheckSerializer(instance=check, data=data, partial=True)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
check = serializer.save()
|
||||
|
||||
# resolve any alerts that are open
|
||||
if "check_reset" in request.data.keys():
|
||||
if check.alert.filter(resolved=False).exists():
|
||||
check.alert.get(resolved=False).resolve()
|
||||
|
||||
if check.policy:
|
||||
update_policy_check_fields_task.delay(check=check.pk)
|
||||
|
||||
@@ -129,6 +128,9 @@ class GetUpdateDeleteCheck(APIView):
|
||||
|
||||
check = get_object_or_404(Check, pk=pk)
|
||||
|
||||
if check.agent and not _has_perm_on_agent(request.user, check.agent.agent_id):
|
||||
raise PermissionDenied()
|
||||
|
||||
check.delete()
|
||||
|
||||
# Policy check deleted
|
||||
@@ -137,18 +139,42 @@ class GetUpdateDeleteCheck(APIView):
|
||||
|
||||
# Re-evaluate agent checks is policy was enforced
|
||||
if check.policy.enforced:
|
||||
generate_agent_checks_task.delay(policy=check.policy)
|
||||
generate_agent_checks_task.delay(policy=check.policy.pk)
|
||||
|
||||
# Agent check deleted
|
||||
elif check.agent:
|
||||
check.agent.generate_checks_from_policies()
|
||||
generate_agent_checks_task.delay(agents=[check.agent.pk])
|
||||
|
||||
return Response(f"{check.readable_desc} was deleted!")
|
||||
|
||||
|
||||
class ResetCheck(APIView):
|
||||
permission_classes = [IsAuthenticated, ChecksPerms]
|
||||
|
||||
def post(self, request, pk):
|
||||
check = get_object_or_404(Check, pk=pk)
|
||||
|
||||
if check.agent and not _has_perm_on_agent(request.user, check.agent.agent_id):
|
||||
raise PermissionDenied()
|
||||
|
||||
check.status = "passing"
|
||||
check.save()
|
||||
|
||||
# resolve any alerts that are open
|
||||
if check.alert.filter(resolved=False).exists():
|
||||
check.alert.get(resolved=False).resolve()
|
||||
|
||||
return Response("The check status was reset")
|
||||
|
||||
|
||||
class GetCheckHistory(APIView):
|
||||
def patch(self, request, checkpk):
|
||||
check = get_object_or_404(Check, pk=checkpk)
|
||||
permission_classes = [IsAuthenticated, ChecksPerms]
|
||||
|
||||
def patch(self, request, pk):
|
||||
check = get_object_or_404(Check, pk=pk)
|
||||
|
||||
if check.agent and not _has_perm_on_agent(request.user, check.agent.agent_id):
|
||||
raise PermissionDenied()
|
||||
|
||||
timeFilter = Q()
|
||||
|
||||
@@ -160,7 +186,7 @@ class GetCheckHistory(APIView):
|
||||
- djangotime.timedelta(days=request.data["timeFilter"]),
|
||||
)
|
||||
|
||||
check_history = CheckHistory.objects.filter(check_id=checkpk).filter(timeFilter).order_by("-x") # type: ignore
|
||||
check_history = CheckHistory.objects.filter(check_id=pk).filter(timeFilter).order_by("-x") # type: ignore
|
||||
|
||||
return Response(
|
||||
CheckHistorySerializer(
|
||||
@@ -171,8 +197,8 @@ class GetCheckHistory(APIView):
|
||||
|
||||
@api_view()
|
||||
@permission_classes([IsAuthenticated, RunChecksPerms])
|
||||
def run_checks(request, pk):
|
||||
agent = get_object_or_404(Agent, pk=pk)
|
||||
def run_checks(request, agent_id):
|
||||
agent = get_object_or_404(Agent, agent_id=agent_id)
|
||||
|
||||
if pyver.parse(agent.version) >= pyver.parse("1.4.1"):
|
||||
r = asyncio.run(agent.nats_cmd({"func": "runchecks"}, timeout=15))
|
||||
@@ -185,14 +211,3 @@ def run_checks(request, pk):
|
||||
else:
|
||||
asyncio.run(agent.nats_cmd({"func": "runchecks"}, wait=False))
|
||||
return Response(f"Checks will now be re-run on {agent.hostname}")
|
||||
|
||||
|
||||
@api_view()
|
||||
def load_checks(request, pk):
|
||||
checks = Check.objects.filter(agent__pk=pk)
|
||||
return Response(CheckSerializer(checks, many=True).data)
|
||||
|
||||
|
||||
@api_view()
|
||||
def get_disks_for_policies(request):
|
||||
return Response(Check.all_disks())
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# Generated by Django 3.2.6 on 2021-10-10 02:49
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('clients', '0017_auto_20210417_0125'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='client',
|
||||
name='created_by',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='client',
|
||||
name='modified_by',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='site',
|
||||
name='created_by',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='site',
|
||||
name='modified_by',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 3.2.6 on 2021-10-28 00:12
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('clients', '0018_auto_20211010_0249'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='deployment',
|
||||
name='client',
|
||||
),
|
||||
]
|
||||
@@ -5,9 +5,13 @@ from django.db import models
|
||||
|
||||
from agents.models import Agent
|
||||
from logs.models import BaseAuditModel
|
||||
from tacticalrmm.models import PermissionQuerySet
|
||||
from tacticalrmm.utils import AGENT_DEFER
|
||||
|
||||
|
||||
class Client(BaseAuditModel):
|
||||
objects = PermissionQuerySet.as_manager()
|
||||
|
||||
name = models.CharField(max_length=255, unique=True)
|
||||
block_policy_inheritance = models.BooleanField(default=False)
|
||||
workstation_policy = models.ForeignKey(
|
||||
@@ -70,29 +74,20 @@ class Client(BaseAuditModel):
|
||||
|
||||
@property
|
||||
def agent_count(self) -> int:
|
||||
return Agent.objects.filter(site__client=self).count()
|
||||
return Agent.objects.defer(*AGENT_DEFER).filter(site__client=self).count()
|
||||
|
||||
@property
|
||||
def has_maintenanace_mode_agents(self):
|
||||
return (
|
||||
Agent.objects.filter(site__client=self, maintenance_mode=True).count() > 0
|
||||
Agent.objects.defer(*AGENT_DEFER)
|
||||
.filter(site__client=self, maintenance_mode=True)
|
||||
.count()
|
||||
> 0
|
||||
)
|
||||
|
||||
@property
|
||||
def has_failing_checks(self):
|
||||
agents = (
|
||||
Agent.objects.only(
|
||||
"pk",
|
||||
"overdue_email_alert",
|
||||
"overdue_text_alert",
|
||||
"last_seen",
|
||||
"overdue_time",
|
||||
"offline_time",
|
||||
)
|
||||
.filter(site__client=self)
|
||||
.prefetch_related("agentchecks", "autotasks")
|
||||
)
|
||||
|
||||
agents = Agent.objects.defer(*AGENT_DEFER).filter(site__client=self)
|
||||
data = {"error": False, "warning": False}
|
||||
|
||||
for agent in agents:
|
||||
@@ -130,6 +125,8 @@ class Client(BaseAuditModel):
|
||||
|
||||
|
||||
class Site(BaseAuditModel):
|
||||
objects = PermissionQuerySet.as_manager()
|
||||
|
||||
client = models.ForeignKey(Client, related_name="sites", on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=255)
|
||||
block_policy_inheritance = models.BooleanField(default=False)
|
||||
@@ -189,23 +186,21 @@ class Site(BaseAuditModel):
|
||||
|
||||
@property
|
||||
def agent_count(self) -> int:
|
||||
return Agent.objects.filter(site=self).count()
|
||||
return Agent.objects.defer(*AGENT_DEFER).filter(site=self).count()
|
||||
|
||||
@property
|
||||
def has_maintenanace_mode_agents(self):
|
||||
return Agent.objects.filter(site=self, maintenance_mode=True).count() > 0
|
||||
return (
|
||||
Agent.objects.defer(*AGENT_DEFER)
|
||||
.filter(site=self, maintenance_mode=True)
|
||||
.count()
|
||||
> 0
|
||||
)
|
||||
|
||||
@property
|
||||
def has_failing_checks(self):
|
||||
agents = (
|
||||
Agent.objects.only(
|
||||
"pk",
|
||||
"overdue_email_alert",
|
||||
"overdue_text_alert",
|
||||
"last_seen",
|
||||
"overdue_time",
|
||||
"offline_time",
|
||||
)
|
||||
Agent.objects.defer(*AGENT_DEFER)
|
||||
.filter(site=self)
|
||||
.prefetch_related("agentchecks", "autotasks")
|
||||
)
|
||||
@@ -257,10 +252,9 @@ ARCH_CHOICES = [
|
||||
|
||||
|
||||
class Deployment(models.Model):
|
||||
objects = PermissionQuerySet.as_manager()
|
||||
|
||||
uid = models.UUIDField(primary_key=False, default=uuid.uuid4, editable=False)
|
||||
client = models.ForeignKey(
|
||||
"clients.Client", related_name="deployclients", on_delete=models.CASCADE
|
||||
)
|
||||
site = models.ForeignKey(
|
||||
"clients.Site", related_name="deploysites", on_delete=models.CASCADE
|
||||
)
|
||||
@@ -279,6 +273,10 @@ class Deployment(models.Model):
|
||||
def __str__(self):
|
||||
return f"{self.client} - {self.site} - {self.mon_type}"
|
||||
|
||||
@property
|
||||
def client(self):
|
||||
return self.site.client
|
||||
|
||||
|
||||
class ClientCustomField(models.Model):
|
||||
client = models.ForeignKey(
|
||||
|
||||
@@ -1,27 +1,45 @@
|
||||
from rest_framework import permissions
|
||||
|
||||
from tacticalrmm.permissions import _has_perm
|
||||
from tacticalrmm.permissions import _has_perm, _has_perm_on_client, _has_perm_on_site
|
||||
|
||||
|
||||
class ManageClientsPerms(permissions.BasePermission):
|
||||
class ClientsPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
if r.method == "GET":
|
||||
return True
|
||||
|
||||
return _has_perm(r, "can_manage_clients")
|
||||
if "pk" in view.kwargs.keys():
|
||||
return _has_perm(r, "can_list_clients") and _has_perm_on_client(
|
||||
r.user, view.kwargs["pk"]
|
||||
)
|
||||
else:
|
||||
return _has_perm(r, "can_list_clients")
|
||||
elif r.method == "PUT" or r.method == "DELETE":
|
||||
return _has_perm(r, "can_manage_clients") and _has_perm_on_client(
|
||||
r.user, view.kwargs["pk"]
|
||||
)
|
||||
else:
|
||||
return _has_perm(r, "can_manage_clients")
|
||||
|
||||
|
||||
class ManageSitesPerms(permissions.BasePermission):
|
||||
class SitesPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
if r.method == "GET":
|
||||
return True
|
||||
|
||||
return _has_perm(r, "can_manage_sites")
|
||||
if "pk" in view.kwargs.keys():
|
||||
return _has_perm(r, "can_list_sites") and _has_perm_on_site(
|
||||
r.user, view.kwargs["pk"]
|
||||
)
|
||||
else:
|
||||
return _has_perm(r, "can_list_sites")
|
||||
elif r.method == "PUT" or r.method == "DELETE":
|
||||
return _has_perm(r, "can_manage_sites") and _has_perm_on_site(
|
||||
r.user, view.kwargs["pk"]
|
||||
)
|
||||
else:
|
||||
return _has_perm(r, "can_manage_sites")
|
||||
|
||||
|
||||
class ManageDeploymentPerms(permissions.BasePermission):
|
||||
class DeploymentPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
if r.method == "GET":
|
||||
return True
|
||||
|
||||
return _has_perm(r, "can_manage_deployments")
|
||||
return _has_perm(r, "can_list_deployments")
|
||||
else:
|
||||
return _has_perm(r, "can_manage_deployments")
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
from django.db.models.base import Model
|
||||
from rest_framework.serializers import (
|
||||
ModelSerializer,
|
||||
ReadOnlyField,
|
||||
Serializer,
|
||||
ValidationError,
|
||||
SerializerMethodField,
|
||||
)
|
||||
|
||||
from .models import Client, ClientCustomField, Deployment, Site, SiteCustomField
|
||||
@@ -32,6 +31,8 @@ class SiteSerializer(ModelSerializer):
|
||||
client_name = ReadOnlyField(source="client.name")
|
||||
custom_fields = SiteCustomFieldSerializer(many=True, read_only=True)
|
||||
agent_count = ReadOnlyField()
|
||||
maintenance_mode = ReadOnlyField(source="has_maintenanace_mode_agents")
|
||||
failing_checks = ReadOnlyField(source="has_failing_checks")
|
||||
|
||||
class Meta:
|
||||
model = Site
|
||||
@@ -46,6 +47,8 @@ class SiteSerializer(ModelSerializer):
|
||||
"custom_fields",
|
||||
"agent_count",
|
||||
"block_policy_inheritance",
|
||||
"maintenance_mode",
|
||||
"failing_checks",
|
||||
)
|
||||
|
||||
def validate(self, val):
|
||||
@@ -55,6 +58,20 @@ class SiteSerializer(ModelSerializer):
|
||||
return val
|
||||
|
||||
|
||||
class SiteMinimumSerializer(ModelSerializer):
|
||||
client_name = ReadOnlyField(source="client.name")
|
||||
|
||||
class Meta:
|
||||
model = Site
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class ClientMinimumSerializer(ModelSerializer):
|
||||
class Meta:
|
||||
model = Client
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class ClientCustomFieldSerializer(ModelSerializer):
|
||||
class Meta:
|
||||
model = ClientCustomField
|
||||
@@ -75,9 +92,17 @@ class ClientCustomFieldSerializer(ModelSerializer):
|
||||
|
||||
|
||||
class ClientSerializer(ModelSerializer):
|
||||
sites = SiteSerializer(many=True, read_only=True)
|
||||
sites = SerializerMethodField()
|
||||
custom_fields = ClientCustomFieldSerializer(many=True, read_only=True)
|
||||
agent_count = ReadOnlyField()
|
||||
maintenance_mode = ReadOnlyField(source="has_maintenanace_mode_agents")
|
||||
failing_checks = ReadOnlyField(source="has_failing_checks")
|
||||
|
||||
def get_sites(self, obj):
|
||||
return SiteSerializer(
|
||||
obj.sites.select_related("client").filter_by_role(self.context["user"]),
|
||||
many=True,
|
||||
).data
|
||||
|
||||
class Meta:
|
||||
model = Client
|
||||
@@ -91,6 +116,8 @@ class ClientSerializer(ModelSerializer):
|
||||
"sites",
|
||||
"custom_fields",
|
||||
"agent_count",
|
||||
"maintenance_mode",
|
||||
"failing_checks",
|
||||
)
|
||||
|
||||
def validate(self, val):
|
||||
@@ -100,25 +127,6 @@ class ClientSerializer(ModelSerializer):
|
||||
return val
|
||||
|
||||
|
||||
class SiteTreeSerializer(ModelSerializer):
|
||||
maintenance_mode = ReadOnlyField(source="has_maintenanace_mode_agents")
|
||||
failing_checks = ReadOnlyField(source="has_failing_checks")
|
||||
|
||||
class Meta:
|
||||
model = Site
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class ClientTreeSerializer(ModelSerializer):
|
||||
sites = SiteTreeSerializer(many=True, read_only=True)
|
||||
maintenance_mode = ReadOnlyField(source="has_maintenanace_mode_agents")
|
||||
failing_checks = ReadOnlyField(source="has_failing_checks")
|
||||
|
||||
class Meta:
|
||||
model = Client
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class DeploymentSerializer(ModelSerializer):
|
||||
client_id = ReadOnlyField(source="client.id")
|
||||
site_id = ReadOnlyField(source="site.id")
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
import uuid
|
||||
from unittest.mock import patch
|
||||
from itertools import cycle
|
||||
|
||||
from model_bakery import baker
|
||||
from rest_framework.serializers import ValidationError
|
||||
from rest_framework.response import Response
|
||||
|
||||
from tacticalrmm.test import TacticalTestCase
|
||||
|
||||
from .models import Client, ClientCustomField, Deployment, Site, SiteCustomField
|
||||
from .serializers import (
|
||||
ClientSerializer,
|
||||
ClientTreeSerializer,
|
||||
DeploymentSerializer,
|
||||
SiteSerializer,
|
||||
)
|
||||
|
||||
base_url = "/clients"
|
||||
|
||||
|
||||
class TestClientViews(TacticalTestCase):
|
||||
def setUp(self):
|
||||
@@ -25,16 +28,15 @@ class TestClientViews(TacticalTestCase):
|
||||
baker.make("clients.Client", _quantity=5)
|
||||
clients = Client.objects.all()
|
||||
|
||||
url = "/clients/clients/"
|
||||
url = f"{base_url}/"
|
||||
r = self.client.get(url, format="json")
|
||||
serializer = ClientSerializer(clients, many=True)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(r.data, serializer.data) # type: ignore
|
||||
self.assertEqual(len(r.data), 5)
|
||||
|
||||
self.check_not_authenticated("get", url)
|
||||
|
||||
def test_add_client(self):
|
||||
url = "/clients/clients/"
|
||||
url = f"{base_url}/"
|
||||
|
||||
# test successfull add client
|
||||
payload = {
|
||||
@@ -115,11 +117,9 @@ class TestClientViews(TacticalTestCase):
|
||||
# setup data
|
||||
client = baker.make("clients.Client")
|
||||
|
||||
url = f"/clients/{client.id}/client/" # type: ignore
|
||||
url = f"{base_url}/{client.id}/" # type: ignore
|
||||
r = self.client.get(url, format="json")
|
||||
serializer = ClientSerializer(client)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(r.data, serializer.data) # type: ignore
|
||||
|
||||
self.check_not_authenticated("get", url)
|
||||
|
||||
@@ -128,12 +128,12 @@ class TestClientViews(TacticalTestCase):
|
||||
client = baker.make("clients.Client", name="OldClientName")
|
||||
|
||||
# test invalid id
|
||||
r = self.client.put("/clients/500/client/", format="json")
|
||||
r = self.client.put(f"{base_url}/500/", format="json")
|
||||
self.assertEqual(r.status_code, 404)
|
||||
|
||||
# test successfull edit client
|
||||
data = {"client": {"name": "NewClientName"}, "custom_fields": []}
|
||||
url = f"/clients/{client.id}/client/" # type: ignore
|
||||
url = f"{base_url}/{client.id}/" # type: ignore
|
||||
r = self.client.put(url, data, format="json")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertTrue(Client.objects.filter(name="NewClientName").exists())
|
||||
@@ -141,7 +141,6 @@ class TestClientViews(TacticalTestCase):
|
||||
|
||||
# test edit client with | in name
|
||||
data = {"client": {"name": "NewClie|ntName"}, "custom_fields": []}
|
||||
url = f"/clients/{client.id}/client/" # type: ignore
|
||||
r = self.client.put(url, data, format="json")
|
||||
self.assertEqual(r.status_code, 400)
|
||||
|
||||
@@ -189,10 +188,10 @@ class TestClientViews(TacticalTestCase):
|
||||
agent = baker.make_recipe("agents.agent", site=site_to_move)
|
||||
|
||||
# test invalid id
|
||||
r = self.client.delete("/clients/334/953/", format="json")
|
||||
r = self.client.delete(f"{base_url}/334/", format="json")
|
||||
self.assertEqual(r.status_code, 404)
|
||||
|
||||
url = f"/clients/{client_to_delete.id}/{site_to_move.id}/" # type: ignore
|
||||
url = f"/clients/{client_to_delete.id}/?site_to_move={site_to_move.id}" # type: ignore
|
||||
|
||||
# test successful deletion
|
||||
r = self.client.delete(url, format="json")
|
||||
@@ -208,7 +207,7 @@ class TestClientViews(TacticalTestCase):
|
||||
baker.make("clients.Site", _quantity=5)
|
||||
sites = Site.objects.all()
|
||||
|
||||
url = "/clients/sites/"
|
||||
url = f"{base_url}/sites/"
|
||||
r = self.client.get(url, format="json")
|
||||
serializer = SiteSerializer(sites, many=True)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
@@ -221,7 +220,7 @@ class TestClientViews(TacticalTestCase):
|
||||
client = baker.make("clients.Client")
|
||||
site = baker.make("clients.Site", client=client)
|
||||
|
||||
url = "/clients/sites/"
|
||||
url = f"{base_url}/sites/"
|
||||
|
||||
# test success add
|
||||
payload = {
|
||||
@@ -279,7 +278,7 @@ class TestClientViews(TacticalTestCase):
|
||||
# setup data
|
||||
site = baker.make("clients.Site")
|
||||
|
||||
url = f"/clients/sites/{site.id}/" # type: ignore
|
||||
url = f"{base_url}/sites/{site.id}/" # type: ignore
|
||||
r = self.client.get(url, format="json")
|
||||
serializer = SiteSerializer(site)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
@@ -293,7 +292,7 @@ class TestClientViews(TacticalTestCase):
|
||||
site = baker.make("clients.Site", client=client)
|
||||
|
||||
# test invalid id
|
||||
r = self.client.put("/clients/sites/688/", format="json")
|
||||
r = self.client.put(f"{base_url}/sites/688/", format="json")
|
||||
self.assertEqual(r.status_code, 404)
|
||||
|
||||
data = {
|
||||
@@ -301,7 +300,7 @@ class TestClientViews(TacticalTestCase):
|
||||
"custom_fields": [],
|
||||
}
|
||||
|
||||
url = f"/clients/sites/{site.id}/" # type: ignore
|
||||
url = f"{base_url}/sites/{site.id}/" # type: ignore
|
||||
r = self.client.put(url, data, format="json")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertTrue(
|
||||
@@ -358,10 +357,10 @@ class TestClientViews(TacticalTestCase):
|
||||
agent = baker.make_recipe("agents.agent", site=site_to_delete)
|
||||
|
||||
# test invalid id
|
||||
r = self.client.delete("/clients/500/445/", format="json")
|
||||
r = self.client.delete("{base_url}/500/", format="json")
|
||||
self.assertEqual(r.status_code, 404)
|
||||
|
||||
url = f"/clients/sites/{site_to_delete.id}/{site_to_move.id}/" # type: ignore
|
||||
url = f"/clients/sites/{site_to_delete.id}/?move_to_site={site_to_move.id}" # type: ignore
|
||||
|
||||
# test deleting with last site under client
|
||||
r = self.client.delete(url, format="json")
|
||||
@@ -378,25 +377,11 @@ class TestClientViews(TacticalTestCase):
|
||||
|
||||
self.check_not_authenticated("delete", url)
|
||||
|
||||
def test_get_tree(self):
|
||||
# setup data
|
||||
baker.make("clients.Site", _quantity=10)
|
||||
clients = Client.objects.all()
|
||||
|
||||
url = "/clients/tree/"
|
||||
|
||||
r = self.client.get(url, format="json")
|
||||
serializer = ClientTreeSerializer(clients, many=True)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(r.data, serializer.data) # type: ignore
|
||||
|
||||
self.check_not_authenticated("get", url)
|
||||
|
||||
def test_get_deployments(self):
|
||||
# setup data
|
||||
deployments = baker.make("clients.Deployment", _quantity=5)
|
||||
|
||||
url = "/clients/deployments/"
|
||||
url = f"{base_url}/deployments/"
|
||||
r = self.client.get(url)
|
||||
serializer = DeploymentSerializer(deployments, many=True)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
@@ -408,7 +393,7 @@ class TestClientViews(TacticalTestCase):
|
||||
# setup data
|
||||
site = baker.make("clients.Site")
|
||||
|
||||
url = "/clients/deployments/"
|
||||
url = f"{base_url}/deployments/"
|
||||
payload = {
|
||||
"client": site.client.id, # type: ignore
|
||||
"site": site.id, # type: ignore
|
||||
@@ -437,21 +422,19 @@ class TestClientViews(TacticalTestCase):
|
||||
# setup data
|
||||
deployment = baker.make("clients.Deployment")
|
||||
|
||||
url = "/clients/deployments/"
|
||||
|
||||
url = f"/clients/{deployment.id}/deployment/" # type: ignore
|
||||
url = f"{base_url}/deployments/{deployment.id}/" # type: ignore
|
||||
r = self.client.delete(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertFalse(Deployment.objects.filter(pk=deployment.id).exists()) # type: ignore
|
||||
|
||||
url = "/clients/32348/deployment/"
|
||||
url = f"{base_url}/deployments/32348/"
|
||||
r = self.client.delete(url)
|
||||
self.assertEqual(r.status_code, 404)
|
||||
|
||||
self.check_not_authenticated("delete", url)
|
||||
|
||||
def test_generate_deployment(self):
|
||||
# TODO complete this
|
||||
@patch("tacticalrmm.utils.generate_winagent_exe", return_value=Response("ok"))
|
||||
def test_generate_deployment(self, post):
|
||||
url = "/clients/asdkj234kasdasjd-asdkj234-asdk34-sad/deploy/"
|
||||
|
||||
r = self.client.get(url)
|
||||
@@ -462,3 +445,429 @@ class TestClientViews(TacticalTestCase):
|
||||
url = f"/clients/{uid}/deploy/"
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 404)
|
||||
|
||||
# test valid download
|
||||
deployment = baker.make(
|
||||
"clients.Deployment",
|
||||
install_flags={"rdp": True, "ping": False, "power": False},
|
||||
)
|
||||
|
||||
url = f"/clients/{deployment.uid}/deploy/"
|
||||
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
|
||||
class TestClientPermissions(TacticalTestCase):
|
||||
def setUp(self):
|
||||
self.client_setup()
|
||||
self.setup_coresettings()
|
||||
|
||||
def test_get_clients_permissions(self):
|
||||
# create user with empty role
|
||||
user = self.create_user_with_roles([])
|
||||
self.client.force_authenticate(user=user) # type: ignore
|
||||
|
||||
url = f"{base_url}/"
|
||||
|
||||
clients = baker.make("clients.Client", _quantity=5)
|
||||
|
||||
# test getting all clients
|
||||
|
||||
# user with empty role should fail
|
||||
self.check_not_authorized("get", url)
|
||||
|
||||
# add can_list_agents roles and should succeed
|
||||
user.role.can_list_clients = True
|
||||
user.role.save()
|
||||
|
||||
# all agents should be returned
|
||||
response = self.check_authorized("get", url)
|
||||
self.assertEqual(len(response.data), 5) # type: ignore
|
||||
|
||||
# limit user to specific client. only 1 client should be returned
|
||||
user.role.can_view_clients.set([clients[3]])
|
||||
response = self.check_authorized("get", url)
|
||||
self.assertEqual(len(response.data), 1) # type: ignore
|
||||
|
||||
# 2 should be returned now
|
||||
user.role.can_view_clients.set([clients[0], clients[1]])
|
||||
response = self.check_authorized("get", url)
|
||||
self.assertEqual(len(response.data), 2) # type: ignore
|
||||
|
||||
# limit to a specific site. The site shouldn't be in client returned sites
|
||||
sites = baker.make("clients.Site", client=clients[4], _quantity=3)
|
||||
baker.make("clients.Site", client=clients[0], _quantity=4)
|
||||
baker.make("clients.Site", client=clients[1], _quantity=5)
|
||||
|
||||
user.role.can_view_sites.set([sites[0]])
|
||||
response = self.check_authorized("get", url)
|
||||
self.assertEqual(len(response.data), 3) # type: ignore
|
||||
for client in response.data: # type: ignore
|
||||
if client["id"] == clients[0].id:
|
||||
self.assertEqual(len(client["sites"]), 4)
|
||||
elif client["id"] == clients[1].id:
|
||||
self.assertEqual(len(client["sites"]), 5)
|
||||
elif client["id"] == clients[4].id:
|
||||
self.assertEqual(len(client["sites"]), 1)
|
||||
|
||||
# make sure superusers work
|
||||
self.check_authorized_superuser("get", url)
|
||||
|
||||
@patch("clients.models.Client.save")
|
||||
@patch("clients.models.Client.delete")
|
||||
def test_add_clients_permissions(self, save, delete):
|
||||
|
||||
data = {"client": {"name": "Client Name"}, "site": {"name": "Site Name"}}
|
||||
|
||||
url = f"{base_url}/"
|
||||
|
||||
# test superuser access
|
||||
self.check_authorized_superuser("post", url, data)
|
||||
|
||||
user = self.create_user_with_roles([])
|
||||
self.client.force_authenticate(user=user) # type: ignore
|
||||
|
||||
# test user without role
|
||||
self.check_not_authorized("post", url, data)
|
||||
|
||||
# add user to role and test
|
||||
user.role.can_manage_clients = True
|
||||
user.role.save()
|
||||
|
||||
self.check_authorized("post", url, data)
|
||||
|
||||
@patch("clients.models.Client.delete")
|
||||
def test_get_edit_delete_clients_permissions(self, delete):
|
||||
# create user with empty role
|
||||
user = self.create_user_with_roles([])
|
||||
self.client.force_authenticate(user=user) # type: ignore
|
||||
|
||||
client = baker.make("clients.Client")
|
||||
unauthorized_client = baker.make("clients.Client")
|
||||
|
||||
methods = ["get", "put", "delete"]
|
||||
url = f"{base_url}/{client.id}/"
|
||||
|
||||
# test user with no roles
|
||||
for method in methods:
|
||||
self.check_not_authorized(method, url)
|
||||
|
||||
# add correct roles for view edit and delete
|
||||
user.role.can_list_clients = True
|
||||
user.role.can_manage_clients = True
|
||||
user.role.save()
|
||||
|
||||
for method in methods:
|
||||
self.check_authorized(method, url)
|
||||
|
||||
# test limiting users to clients and sites
|
||||
|
||||
# limit to client
|
||||
user.role.can_view_clients.set([client])
|
||||
|
||||
for method in methods:
|
||||
self.check_not_authorized(method, f"{base_url}/{unauthorized_client.id}/")
|
||||
self.check_authorized(method, url)
|
||||
|
||||
# make sure superusers work
|
||||
for method in methods:
|
||||
self.check_authorized_superuser(
|
||||
method, f"{base_url}/{unauthorized_client.id}/"
|
||||
)
|
||||
|
||||
def test_get_sites_permissions(self):
|
||||
# create user with empty role
|
||||
user = self.create_user_with_roles([])
|
||||
self.client.force_authenticate(user=user) # type: ignore
|
||||
|
||||
url = f"{base_url}/sites/"
|
||||
|
||||
clients = baker.make("clients.Client", _quantity=3)
|
||||
sites = baker.make("clients.Site", client=cycle(clients), _quantity=10)
|
||||
|
||||
# test getting all sites
|
||||
|
||||
# user with empty role should fail
|
||||
self.check_not_authorized("get", url)
|
||||
|
||||
# add can_list_sites roles and should succeed
|
||||
user.role.can_list_sites = True
|
||||
user.role.save()
|
||||
|
||||
# all sites should be returned
|
||||
response = self.check_authorized("get", url)
|
||||
self.assertEqual(len(response.data), 10) # type: ignore
|
||||
|
||||
# limit user to specific site. only 1 site should be returned
|
||||
user.role.can_view_sites.set([sites[3]])
|
||||
response = self.check_authorized("get", url)
|
||||
self.assertEqual(len(response.data), 1) # type: ignore
|
||||
|
||||
# 2 should be returned now
|
||||
user.role.can_view_sites.set([sites[0], sites[1]])
|
||||
response = self.check_authorized("get", url)
|
||||
self.assertEqual(len(response.data), 2) # type: ignore
|
||||
|
||||
# check if limiting user to client works
|
||||
user.role.can_view_sites.clear()
|
||||
user.role.can_view_clients.set([clients[0]])
|
||||
response = self.check_authorized("get", url)
|
||||
self.assertEqual(len(response.data), 4) # type: ignore
|
||||
|
||||
# add a site to see if the results still work
|
||||
user.role.can_view_sites.set([sites[1], sites[0]])
|
||||
response = self.check_authorized("get", url)
|
||||
self.assertEqual(len(response.data), 5) # type: ignore
|
||||
|
||||
# make sure superusers work
|
||||
self.check_authorized_superuser("get", url)
|
||||
|
||||
@patch("clients.models.Site.save")
|
||||
@patch("clients.models.Site.delete")
|
||||
def test_add_sites_permissions(self, delete, save):
|
||||
client = baker.make("clients.Client")
|
||||
unauthorized_client = baker.make("clients.Client")
|
||||
data = {"client": client.id, "name": "Site Name"}
|
||||
|
||||
url = f"{base_url}/sites/"
|
||||
|
||||
# test superuser access
|
||||
self.check_authorized_superuser("post", url, data)
|
||||
|
||||
user = self.create_user_with_roles([])
|
||||
self.client.force_authenticate(user=user) # type: ignore
|
||||
|
||||
# test user without role
|
||||
self.check_not_authorized("post", url, data)
|
||||
|
||||
# add user to role and test
|
||||
user.role.can_manage_sites = True
|
||||
user.role.save()
|
||||
|
||||
self.check_authorized("post", url, data)
|
||||
|
||||
# limit to client and test
|
||||
user.role.can_view_clients.set([client])
|
||||
self.check_authorized("post", url, data)
|
||||
|
||||
# test adding to unauthorized client
|
||||
data = {"client": unauthorized_client.id, "name": "Site Name"}
|
||||
self.check_not_authorized("post", url, data)
|
||||
|
||||
@patch("clients.models.Site.delete")
|
||||
def test_get_edit_delete_sites_permissions(self, delete):
|
||||
# create user with empty role
|
||||
user = self.create_user_with_roles([])
|
||||
self.client.force_authenticate(user=user) # type: ignore
|
||||
|
||||
site = baker.make("clients.Site")
|
||||
unauthorized_site = baker.make("clients.Site")
|
||||
|
||||
methods = ["get", "put", "delete"]
|
||||
url = f"{base_url}/sites/{site.id}/"
|
||||
|
||||
# test user with no roles
|
||||
for method in methods:
|
||||
self.check_not_authorized(method, url)
|
||||
|
||||
# add correct roles for view edit and delete
|
||||
user.role.can_list_sites = True
|
||||
user.role.can_manage_sites = True
|
||||
user.role.save()
|
||||
|
||||
for method in methods:
|
||||
self.check_authorized(method, url)
|
||||
|
||||
# test limiting users to clients and sites
|
||||
|
||||
# limit to site
|
||||
user.role.can_view_sites.set([site])
|
||||
|
||||
for method in methods:
|
||||
self.check_not_authorized(method, f"{base_url}/{unauthorized_site.id}/")
|
||||
self.check_authorized(method, url)
|
||||
|
||||
# test limit to only client
|
||||
user.role.can_view_sites.clear()
|
||||
user.role.can_view_clients.set([site.client])
|
||||
|
||||
for method in methods:
|
||||
self.check_not_authorized(method, f"{base_url}/{unauthorized_site.id}/")
|
||||
self.check_authorized(method, url)
|
||||
|
||||
# make sure superusers work
|
||||
for method in methods:
|
||||
self.check_authorized_superuser(
|
||||
method, f"{base_url}/{unauthorized_site.id}/"
|
||||
)
|
||||
|
||||
def test_get_pendingactions_permissions(self):
|
||||
url = f"{base_url}/deployments/"
|
||||
|
||||
site = baker.make("clients.Site")
|
||||
other_site = baker.make("clients.Site")
|
||||
deployments = baker.make("clients.Deployment", site=site, _quantity=5)
|
||||
other_deployments = baker.make(
|
||||
"clients.Deployment", site=other_site, _quantity=7
|
||||
)
|
||||
|
||||
# test getting all deployments
|
||||
# make sure superusers work
|
||||
self.check_authorized_superuser("get", url)
|
||||
|
||||
# create user with empty role
|
||||
user = self.create_user_with_roles([])
|
||||
self.client.force_authenticate(user=user) # type: ignore
|
||||
|
||||
# user with empty role should fail
|
||||
self.check_not_authorized("get", url)
|
||||
|
||||
# add can_list_sites roles and should succeed
|
||||
user.role.can_list_deployments = True
|
||||
user.role.save()
|
||||
|
||||
# all sites should be returned
|
||||
response = self.check_authorized("get", url)
|
||||
self.assertEqual(len(response.data), 12) # type: ignore
|
||||
|
||||
# limit user to specific site. only 1 site should be returned
|
||||
user.role.can_view_sites.set([site])
|
||||
response = self.check_authorized("get", url)
|
||||
self.assertEqual(len(response.data), 5) # type: ignore
|
||||
|
||||
# all should be returned now
|
||||
user.role.can_view_clients.set([other_site.client])
|
||||
response = self.check_authorized("get", url)
|
||||
self.assertEqual(len(response.data), 12) # type: ignore
|
||||
|
||||
# check if limiting user to client works
|
||||
user.role.can_view_sites.clear()
|
||||
user.role.can_view_clients.set([other_site.client])
|
||||
response = self.check_authorized("get", url)
|
||||
self.assertEqual(len(response.data), 7) # type: ignore
|
||||
|
||||
@patch("clients.models.Deployment.save")
|
||||
def test_add_deployments_permissions(self, save):
|
||||
site = baker.make("clients.Site")
|
||||
unauthorized_site = baker.make("clients.Site")
|
||||
data = {
|
||||
"site": site.id,
|
||||
}
|
||||
|
||||
# test adding to unauthorized client
|
||||
unauthorized_data = {
|
||||
"site": unauthorized_site.id,
|
||||
}
|
||||
|
||||
url = f"{base_url}/deployments/"
|
||||
|
||||
# test superuser access
|
||||
self.check_authorized_superuser("post", url, data)
|
||||
|
||||
user = self.create_user_with_roles([])
|
||||
self.client.force_authenticate(user=user) # type: ignore
|
||||
|
||||
# test user without role
|
||||
self.check_not_authorized("post", url, data)
|
||||
|
||||
# add user to role and test
|
||||
user.role.can_manage_deployments = True
|
||||
user.role.save()
|
||||
|
||||
self.check_authorized("post", url, data)
|
||||
|
||||
# limit to client and test
|
||||
user.role.can_view_clients.set([site.client])
|
||||
self.check_authorized("post", url, data)
|
||||
self.check_not_authorized("post", url, unauthorized_data)
|
||||
|
||||
# limit to site and test
|
||||
user.role.can_view_clients.clear()
|
||||
user.role.can_view_sites.set([site])
|
||||
self.check_authorized("post", url, data)
|
||||
self.check_not_authorized("post", url, unauthorized_data)
|
||||
|
||||
@patch("clients.models.Deployment.delete")
|
||||
def test_delete_deployments_permissions(self, delete):
|
||||
site = baker.make("clients.Site")
|
||||
unauthorized_site = baker.make("clients.Site")
|
||||
deployment = baker.make("clients.Deployment", site=site)
|
||||
unauthorized_deployment = baker.make(
|
||||
"clients.Deployment", site=unauthorized_site
|
||||
)
|
||||
|
||||
url = f"{base_url}/deployments/{deployment.id}/"
|
||||
unauthorized_url = f"{base_url}/deployments/{unauthorized_deployment.id}/"
|
||||
|
||||
# make sure superusers work
|
||||
self.check_authorized_superuser("delete", url)
|
||||
self.check_authorized_superuser("delete", unauthorized_url)
|
||||
|
||||
# create user with empty role
|
||||
user = self.create_user_with_roles([])
|
||||
self.client.force_authenticate(user=user) # type: ignore
|
||||
|
||||
# make sure user with empty role is unauthorized
|
||||
self.check_not_authorized("delete", url)
|
||||
self.check_not_authorized("delete", unauthorized_url)
|
||||
|
||||
# add correct roles for view edit and delete
|
||||
user.role.can_manage_deployments = True
|
||||
user.role.save()
|
||||
|
||||
self.check_authorized("delete", url)
|
||||
self.check_authorized("delete", unauthorized_url)
|
||||
|
||||
# test limiting users to clients and sites
|
||||
|
||||
# limit to site
|
||||
user.role.can_view_sites.set([site])
|
||||
|
||||
# recreate deployment since it is being deleted even though I am mocking delete on Deployment model???
|
||||
unauthorized_deployment = baker.make(
|
||||
"clients.Deployment", site=unauthorized_site
|
||||
)
|
||||
unauthorized_url = f"{base_url}/deployments/{unauthorized_deployment.id}/"
|
||||
|
||||
self.check_authorized("delete", url)
|
||||
self.check_not_authorized("delete", unauthorized_url)
|
||||
|
||||
# test limit to only client
|
||||
user.role.can_view_sites.clear()
|
||||
user.role.can_view_clients.set([site.client])
|
||||
|
||||
self.check_authorized("delete", url)
|
||||
self.check_not_authorized("delete", unauthorized_url)
|
||||
|
||||
def test_restricted_user_creating_clients(self):
|
||||
from accounts.models import User
|
||||
|
||||
# when a user that is limited to a specific subset of clients creates a client. It should allow access to that client
|
||||
client = baker.make("clients.Client")
|
||||
user = self.create_user_with_roles(["can_manage_clients"])
|
||||
self.client.force_authenticate(user=user) # type: ignore
|
||||
user.role.can_view_clients.set([client])
|
||||
|
||||
data = {"client": {"name": "New Client"}, "site": {"name": "New Site"}}
|
||||
|
||||
self.client.post(f"{base_url}/", data, format="json")
|
||||
|
||||
# make sure two clients are allowed now
|
||||
self.assertEqual(User.objects.get(id=user.id).role.can_view_clients.count(), 2)
|
||||
|
||||
def test_restricted_user_creating_sites(self):
|
||||
from accounts.models import User
|
||||
|
||||
# when a user that is limited to a specific subset of clients creates a client. It should allow access to that client
|
||||
site = baker.make("clients.Site")
|
||||
user = self.create_user_with_roles(["can_manage_sites"])
|
||||
self.client.force_authenticate(user=user) # type: ignore
|
||||
user.role.can_view_sites.set([site])
|
||||
|
||||
data = {"site": {"client": site.client.id, "name": "New Site"}}
|
||||
|
||||
self.client.post(f"{base_url}/sites/", data, format="json")
|
||||
|
||||
# make sure two sites are allowed now
|
||||
self.assertEqual(User.objects.get(id=user.id).role.can_view_sites.count(), 2)
|
||||
|
||||
@@ -3,14 +3,11 @@ from django.urls import path
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path("clients/", views.GetAddClients.as_view()),
|
||||
path("<int:pk>/client/", views.GetUpdateClient.as_view()),
|
||||
path("<int:pk>/<int:sitepk>/", views.DeleteClient.as_view()),
|
||||
path("tree/", views.GetClientTree.as_view()),
|
||||
path("", views.GetAddClients.as_view()),
|
||||
path("<int:pk>/", views.GetUpdateDeleteClient.as_view()),
|
||||
path("sites/", views.GetAddSites.as_view()),
|
||||
path("sites/<int:pk>/", views.GetUpdateSite.as_view()),
|
||||
path("sites/<int:pk>/<int:sitepk>/", views.DeleteSite.as_view()),
|
||||
path("sites/<int:pk>/", views.GetUpdateDeleteSite.as_view()),
|
||||
path("deployments/", views.AgentDeployment.as_view()),
|
||||
path("<int:pk>/deployment/", views.AgentDeployment.as_view()),
|
||||
path("deployments/<int:pk>/", views.AgentDeployment.as_view()),
|
||||
path("<str:uid>/deploy/", views.GenerateAgent.as_view()),
|
||||
]
|
||||
|
||||
@@ -8,17 +8,22 @@ from django.utils import timezone as djangotime
|
||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
|
||||
from agents.models import Agent
|
||||
from core.models import CoreSettings
|
||||
from tacticalrmm.utils import notify_error
|
||||
from tacticalrmm.permissions import _has_perm_on_client, _has_perm_on_site
|
||||
|
||||
from .models import Client, ClientCustomField, Deployment, Site, SiteCustomField
|
||||
from .permissions import ManageClientsPerms, ManageDeploymentPerms, ManageSitesPerms
|
||||
from .permissions import (
|
||||
ClientsPerms,
|
||||
DeploymentPerms,
|
||||
SitesPerms,
|
||||
)
|
||||
from .serializers import (
|
||||
ClientCustomFieldSerializer,
|
||||
ClientSerializer,
|
||||
ClientTreeSerializer,
|
||||
DeploymentSerializer,
|
||||
SiteCustomFieldSerializer,
|
||||
SiteSerializer,
|
||||
@@ -26,11 +31,15 @@ from .serializers import (
|
||||
|
||||
|
||||
class GetAddClients(APIView):
|
||||
permission_classes = [IsAuthenticated, ManageClientsPerms]
|
||||
permission_classes = [IsAuthenticated, ClientsPerms]
|
||||
|
||||
def get(self, request):
|
||||
clients = Client.objects.all()
|
||||
return Response(ClientSerializer(clients, many=True).data)
|
||||
clients = Client.objects.select_related(
|
||||
"workstation_policy", "server_policy", "alert_template"
|
||||
).filter_by_role(request.user)
|
||||
return Response(
|
||||
ClientSerializer(clients, context={"user": request.user}, many=True).data
|
||||
)
|
||||
|
||||
def post(self, request):
|
||||
# create client
|
||||
@@ -67,15 +76,19 @@ class GetAddClients(APIView):
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
|
||||
return Response(f"{client} was added!")
|
||||
# add user to allowed clients in role if restricted user created the client
|
||||
if request.user.role and request.user.role.can_view_clients.exists():
|
||||
request.user.role.can_view_clients.add(client)
|
||||
|
||||
return Response(f"{client.name} was added")
|
||||
|
||||
|
||||
class GetUpdateClient(APIView):
|
||||
permission_classes = [IsAuthenticated, ManageClientsPerms]
|
||||
class GetUpdateDeleteClient(APIView):
|
||||
permission_classes = [IsAuthenticated, ClientsPerms]
|
||||
|
||||
def get(self, request, pk):
|
||||
client = get_object_or_404(Client, pk=pk)
|
||||
return Response(ClientSerializer(client).data)
|
||||
return Response(ClientSerializer(client, context={"user": request.user}).data)
|
||||
|
||||
def put(self, request, pk):
|
||||
client = get_object_or_404(Client, pk=pk)
|
||||
@@ -107,46 +120,41 @@ class GetUpdateClient(APIView):
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
|
||||
return Response("The Client was updated")
|
||||
return Response("{client} was updated")
|
||||
|
||||
|
||||
class DeleteClient(APIView):
|
||||
permission_classes = [IsAuthenticated, ManageClientsPerms]
|
||||
|
||||
def delete(self, request, pk, sitepk):
|
||||
def delete(self, request, pk):
|
||||
from automation.tasks import generate_agent_checks_task
|
||||
|
||||
client = get_object_or_404(Client, pk=pk)
|
||||
agents = Agent.objects.filter(site__client=client)
|
||||
|
||||
if not sitepk:
|
||||
# only run tasks if it affects clients
|
||||
if client.agent_count > 0 and "move_to_site" in request.query_params.keys():
|
||||
agents = Agent.objects.filter(site__client=client)
|
||||
site = get_object_or_404(Site, pk=request.query_params["move_to_site"])
|
||||
agents.update(site=site)
|
||||
generate_agent_checks_task.delay(all=True, create_tasks=True)
|
||||
|
||||
elif client.agent_count > 0:
|
||||
return notify_error(
|
||||
"There needs to be a site specified to move existing agents to"
|
||||
"Agents exist under this client. There needs to be a site specified to move existing agents to"
|
||||
)
|
||||
|
||||
site = get_object_or_404(Site, pk=sitepk)
|
||||
agents.update(site=site)
|
||||
|
||||
generate_agent_checks_task.delay(all=True, create_tasks=True)
|
||||
|
||||
client.delete()
|
||||
return Response(f"{client.name} was deleted!")
|
||||
|
||||
|
||||
class GetClientTree(APIView):
|
||||
def get(self, request):
|
||||
clients = Client.objects.all()
|
||||
return Response(ClientTreeSerializer(clients, many=True).data)
|
||||
return Response(f"{client.name} was deleted")
|
||||
|
||||
|
||||
class GetAddSites(APIView):
|
||||
permission_classes = [IsAuthenticated, ManageSitesPerms]
|
||||
permission_classes = [IsAuthenticated, SitesPerms]
|
||||
|
||||
def get(self, request):
|
||||
sites = Site.objects.all()
|
||||
sites = Site.objects.filter_by_role(request.user)
|
||||
return Response(SiteSerializer(sites, many=True).data)
|
||||
|
||||
def post(self, request):
|
||||
|
||||
if not _has_perm_on_client(request.user, request.data["site"]["client"]):
|
||||
raise PermissionDenied()
|
||||
|
||||
serializer = SiteSerializer(data=request.data["site"])
|
||||
serializer.is_valid(raise_exception=True)
|
||||
site = serializer.save()
|
||||
@@ -163,11 +171,15 @@ class GetAddSites(APIView):
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
|
||||
# add user to allowed sites in role if restricted user created the client
|
||||
if request.user.role and request.user.role.can_view_sites.exists():
|
||||
request.user.role.can_view_sites.add(site)
|
||||
|
||||
return Response(f"Site {site.name} was added!")
|
||||
|
||||
|
||||
class GetUpdateSite(APIView):
|
||||
permission_classes = [IsAuthenticated, ManageSitesPerms]
|
||||
class GetUpdateDeleteSite(APIView):
|
||||
permission_classes = [IsAuthenticated, SitesPerms]
|
||||
|
||||
def get(self, request, pk):
|
||||
site = get_object_or_404(Site, pk=pk)
|
||||
@@ -208,50 +220,47 @@ class GetUpdateSite(APIView):
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
|
||||
return Response("Site was edited!")
|
||||
return Response("Site was edited")
|
||||
|
||||
|
||||
class DeleteSite(APIView):
|
||||
permission_classes = [IsAuthenticated, ManageSitesPerms]
|
||||
|
||||
def delete(self, request, pk, sitepk):
|
||||
def delete(self, request, pk):
|
||||
from automation.tasks import generate_agent_checks_task
|
||||
|
||||
site = get_object_or_404(Site, pk=pk)
|
||||
if site.client.sites.count() == 1:
|
||||
return notify_error("A client must have at least 1 site.")
|
||||
|
||||
agents = Agent.objects.filter(site=site)
|
||||
# only run tasks if it affects clients
|
||||
if site.agent_count > 0 and "move_to_site" in request.query_params.keys():
|
||||
agents = Agent.objects.filter(site=site)
|
||||
new_site = get_object_or_404(Site, pk=request.query_params["move_to_site"])
|
||||
agents.update(site=new_site)
|
||||
generate_agent_checks_task.delay(all=True, create_tasks=True)
|
||||
|
||||
if not sitepk:
|
||||
elif site.agent_count > 0:
|
||||
return notify_error(
|
||||
"There needs to be a site specified to move the agents to"
|
||||
)
|
||||
|
||||
agent_site = get_object_or_404(Site, pk=sitepk)
|
||||
|
||||
agents.update(site=agent_site)
|
||||
|
||||
generate_agent_checks_task.delay(all=True, create_tasks=True)
|
||||
|
||||
site.delete()
|
||||
return Response(f"{site.name} was deleted!")
|
||||
return Response(f"{site.name} was deleted")
|
||||
|
||||
|
||||
class AgentDeployment(APIView):
|
||||
permission_classes = [IsAuthenticated, ManageDeploymentPerms]
|
||||
permission_classes = [IsAuthenticated, DeploymentPerms]
|
||||
|
||||
def get(self, request):
|
||||
deps = Deployment.objects.all()
|
||||
deps = Deployment.objects.filter_by_role(request.user)
|
||||
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"])
|
||||
|
||||
if not _has_perm_on_site(request.user, site.pk):
|
||||
raise PermissionDenied()
|
||||
|
||||
installer_user = User.objects.filter(is_installer_user=True).first()
|
||||
|
||||
expires = dt.datetime.strptime(
|
||||
@@ -268,7 +277,6 @@ class AgentDeployment(APIView):
|
||||
}
|
||||
|
||||
Deployment(
|
||||
client=client,
|
||||
site=site,
|
||||
expiry=expires,
|
||||
mon_type=request.data["agenttype"],
|
||||
@@ -277,17 +285,21 @@ class AgentDeployment(APIView):
|
||||
token_key=token,
|
||||
install_flags=flags,
|
||||
).save()
|
||||
return Response("ok")
|
||||
return Response("The deployment was added successfully")
|
||||
|
||||
def delete(self, request, pk):
|
||||
d = get_object_or_404(Deployment, pk=pk)
|
||||
|
||||
if not _has_perm_on_site(request.user, d.site.pk):
|
||||
raise PermissionDenied()
|
||||
|
||||
try:
|
||||
d.auth_token.delete()
|
||||
except:
|
||||
pass
|
||||
|
||||
d.delete()
|
||||
return Response("ok")
|
||||
return Response("The deployment was deleted")
|
||||
|
||||
|
||||
class GenerateAgent(APIView):
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import os
|
||||
import json
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Generate conf for nats-api"
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
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"],
|
||||
}
|
||||
conf = os.path.join(settings.BASE_DIR, "nats-api.conf")
|
||||
with open(conf, "w") as f:
|
||||
json.dump(config, f)
|
||||
@@ -2,6 +2,7 @@ from django.core.management.base import BaseCommand
|
||||
|
||||
from logs.models import PendingAction
|
||||
from scripts.models import Script
|
||||
from accounts.models import User
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@@ -13,3 +14,9 @@ class Command(BaseCommand):
|
||||
|
||||
# load community scripts into the db
|
||||
Script.load_community_scripts()
|
||||
|
||||
# make sure installer user is set to block_dashboard_logins
|
||||
if User.objects.filter(is_installer_user=True).exists():
|
||||
for user in User.objects.filter(is_installer_user=True):
|
||||
user.block_dashboard_login = True
|
||||
user.save()
|
||||
|
||||
53
api/tacticalrmm/core/migrations/0028_auto_20210917_1954.py
Normal file
53
api/tacticalrmm/core/migrations/0028_auto_20210917_1954.py
Normal file
@@ -0,0 +1,53 @@
|
||||
# Generated by Django 3.2.6 on 2021-09-17 19:54
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("core", "0027_auto_20210905_1606"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="coresettings",
|
||||
name="created_by",
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="coresettings",
|
||||
name="modified_by",
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="customfield",
|
||||
name="created_by",
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="customfield",
|
||||
name="modified_by",
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="globalkvstore",
|
||||
name="created_by",
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="globalkvstore",
|
||||
name="modified_by",
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="urlaction",
|
||||
name="created_by",
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="urlaction",
|
||||
name="modified_by",
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
]
|
||||
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
||||
import requests
|
||||
import smtplib
|
||||
from email.message import EmailMessage
|
||||
from django.db.models.enums import Choices
|
||||
|
||||
import pytz
|
||||
from django.conf import settings
|
||||
@@ -8,6 +8,7 @@ from django.contrib.postgres.fields import ArrayField
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from twilio.rest import Client as TwClient
|
||||
from twilio.base.exceptions import TwilioRestException
|
||||
|
||||
from logs.models import BaseAuditModel, DebugLog, LOG_LEVEL_CHOICES
|
||||
|
||||
@@ -118,7 +119,6 @@ class CoreSettings(BaseAuditModel):
|
||||
def sms_is_configured(self):
|
||||
return all(
|
||||
[
|
||||
self.sms_alert_recipients,
|
||||
self.twilio_auth_token,
|
||||
self.twilio_account_sid,
|
||||
self.twilio_number,
|
||||
@@ -130,7 +130,6 @@ class CoreSettings(BaseAuditModel):
|
||||
# smtp with username/password authentication
|
||||
if (
|
||||
self.smtp_requires_auth
|
||||
and self.email_alert_recipients
|
||||
and self.smtp_from_email
|
||||
and self.smtp_host
|
||||
and self.smtp_host_user
|
||||
@@ -141,7 +140,6 @@ class CoreSettings(BaseAuditModel):
|
||||
# smtp relay
|
||||
elif (
|
||||
not self.smtp_requires_auth
|
||||
and self.email_alert_recipients
|
||||
and self.smtp_from_email
|
||||
and self.smtp_host
|
||||
and self.smtp_port
|
||||
@@ -151,10 +149,10 @@ class CoreSettings(BaseAuditModel):
|
||||
return False
|
||||
|
||||
def send_mail(self, subject, body, alert_template=None, test=False):
|
||||
|
||||
if not alert_template and not self.email_is_configured:
|
||||
if test:
|
||||
return "Missing required fields (need at least 1 recipient)"
|
||||
if test and not self.email_is_configured:
|
||||
return "There needs to be at least one email recipient configured"
|
||||
# return since email must be configured to continue
|
||||
elif not self.email_is_configured:
|
||||
return False
|
||||
|
||||
# override email from if alert_template is passed and is set
|
||||
@@ -169,6 +167,9 @@ class CoreSettings(BaseAuditModel):
|
||||
else:
|
||||
email_recipients = ", ".join(self.email_alert_recipients)
|
||||
|
||||
if not email_recipients:
|
||||
return "There needs to be at least one email recipient configured"
|
||||
|
||||
try:
|
||||
msg = EmailMessage()
|
||||
msg["Subject"] = subject
|
||||
@@ -195,22 +196,29 @@ class CoreSettings(BaseAuditModel):
|
||||
else:
|
||||
return True
|
||||
|
||||
def send_sms(self, body, alert_template=None):
|
||||
if not alert_template or not self.sms_is_configured:
|
||||
return
|
||||
def send_sms(self, body, alert_template=None, test=False):
|
||||
if not self.sms_is_configured:
|
||||
return "Sms alerting is not setup correctly."
|
||||
|
||||
# override email recipients if alert_template is passed and is set
|
||||
if alert_template and alert_template.text_recipients:
|
||||
text_recipients = alert_template.email_recipients
|
||||
text_recipients = alert_template.text_recipients
|
||||
else:
|
||||
text_recipients = self.sms_alert_recipients
|
||||
|
||||
if not text_recipients:
|
||||
return "No sms recipients found"
|
||||
|
||||
tw_client = TwClient(self.twilio_account_sid, self.twilio_auth_token)
|
||||
for num in text_recipients:
|
||||
try:
|
||||
tw_client.messages.create(body=body, to=num, from_=self.twilio_number)
|
||||
except Exception as e:
|
||||
except TwilioRestException as e:
|
||||
DebugLog.error(message=f"SMS failed to send: {e}")
|
||||
if test:
|
||||
return str(e)
|
||||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def serialize(core):
|
||||
@@ -306,6 +314,31 @@ class CodeSignToken(models.Model):
|
||||
|
||||
super(CodeSignToken, self).save(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def is_valid(self) -> bool:
|
||||
if not self.token:
|
||||
return False
|
||||
|
||||
errors = []
|
||||
for url in settings.EXE_GEN_URLS:
|
||||
try:
|
||||
r = requests.post(
|
||||
f"{url}/api/v1/checktoken",
|
||||
json={"token": self.token},
|
||||
headers={"Content-type": "application/json"},
|
||||
timeout=15,
|
||||
)
|
||||
except Exception as e:
|
||||
errors.append(str(e))
|
||||
else:
|
||||
errors = []
|
||||
break
|
||||
|
||||
if errors:
|
||||
return False
|
||||
|
||||
return r.status_code == 200
|
||||
|
||||
def __str__(self):
|
||||
return "Code signing token"
|
||||
|
||||
|
||||
@@ -3,14 +3,17 @@ from rest_framework import permissions
|
||||
from tacticalrmm.permissions import _has_perm
|
||||
|
||||
|
||||
class ViewCoreSettingsPerms(permissions.BasePermission):
|
||||
class CoreSettingsPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
return _has_perm(r, "can_view_core_settings")
|
||||
if r.method == "GET":
|
||||
return _has_perm(r, "can_view_core_settings")
|
||||
else:
|
||||
return _has_perm(r, "can_edit_core_settings")
|
||||
|
||||
|
||||
class EditCoreSettingsPerms(permissions.BasePermission):
|
||||
class URLActionPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
return _has_perm(r, "can_edit_core_settings")
|
||||
return _has_perm(r, "can_run_urlactions")
|
||||
|
||||
|
||||
class ServerMaintPerms(permissions.BasePermission):
|
||||
@@ -21,3 +24,11 @@ class ServerMaintPerms(permissions.BasePermission):
|
||||
class CodeSignPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
return _has_perm(r, "can_code_sign")
|
||||
|
||||
|
||||
class CustomFieldPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
if r.method == "GET":
|
||||
return _has_perm(r, "can_view_customfields")
|
||||
else:
|
||||
return _has_perm(r, "can_manage_customfields")
|
||||
|
||||
@@ -82,7 +82,7 @@ class TestCoreTasks(TacticalTestCase):
|
||||
self.check_not_authenticated("get", url)
|
||||
|
||||
def test_get_core_settings(self):
|
||||
url = "/core/getcoresettings/"
|
||||
url = "/core/settings/"
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
@@ -90,7 +90,7 @@ class TestCoreTasks(TacticalTestCase):
|
||||
|
||||
@patch("automation.tasks.generate_agent_checks_task.delay")
|
||||
def test_edit_coresettings(self, generate_agent_checks_task):
|
||||
url = "/core/editsettings/"
|
||||
url = "/core/settings/"
|
||||
|
||||
# setup
|
||||
policies = baker.make("automation.Policy", _quantity=2)
|
||||
@@ -99,7 +99,7 @@ class TestCoreTasks(TacticalTestCase):
|
||||
"smtp_from_email": "newexample@example.com",
|
||||
"mesh_token": "New_Mesh_Token",
|
||||
}
|
||||
r = self.client.patch(url, data)
|
||||
r = self.client.put(url, data)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(
|
||||
CoreSettings.objects.first().smtp_from_email, data["smtp_from_email"]
|
||||
@@ -113,7 +113,7 @@ class TestCoreTasks(TacticalTestCase):
|
||||
"workstation_policy": policies[0].id, # type: ignore
|
||||
"server_policy": policies[1].id, # type: ignore
|
||||
}
|
||||
r = self.client.patch(url, data)
|
||||
r = self.client.put(url, data)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(CoreSettings.objects.first().server_policy.id, policies[1].id) # type: ignore
|
||||
self.assertEqual(
|
||||
@@ -128,13 +128,13 @@ class TestCoreTasks(TacticalTestCase):
|
||||
data = {
|
||||
"workstation_policy": "",
|
||||
}
|
||||
r = self.client.patch(url, data)
|
||||
r = self.client.put(url, data)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(CoreSettings.objects.first().workstation_policy, None)
|
||||
|
||||
self.assertEqual(generate_agent_checks_task.call_count, 1)
|
||||
|
||||
self.check_not_authenticated("patch", url)
|
||||
self.check_not_authenticated("put", url)
|
||||
|
||||
@patch("tacticalrmm.utils.reload_nats")
|
||||
@patch("autotasks.tasks.remove_orphaned_win_tasks.delay")
|
||||
@@ -404,10 +404,10 @@ class TestCoreTasks(TacticalTestCase):
|
||||
|
||||
url = "/core/urlaction/run/"
|
||||
# test not found
|
||||
r = self.client.patch(url, {"agent": 500, "action": 500})
|
||||
r = self.client.patch(url, {"agent_id": 500, "action": 500})
|
||||
self.assertEqual(r.status_code, 404)
|
||||
|
||||
data = {"agent": agent.id, "action": action.id} # type: ignore
|
||||
data = {"agent_id": agent.agent_id, "action": action.id} # type: ignore
|
||||
r = self.client.patch(url, data)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
@@ -417,3 +417,9 @@ class TestCoreTasks(TacticalTestCase):
|
||||
)
|
||||
|
||||
self.check_not_authenticated("patch", url)
|
||||
|
||||
|
||||
class TestCorePermissions(TacticalTestCase):
|
||||
def setUp(self):
|
||||
self.client_setup()
|
||||
self.setup_coresettings()
|
||||
|
||||
@@ -4,8 +4,7 @@ from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path("uploadmesh/", views.UploadMeshAgent.as_view()),
|
||||
path("getcoresettings/", views.get_core_settings),
|
||||
path("editsettings/", views.edit_settings),
|
||||
path("settings/", views.GetEditCoreSettings.as_view()),
|
||||
path("version/", views.version),
|
||||
path("emailtest/", views.email_test),
|
||||
path("dashinfo/", views.dashboard_info),
|
||||
|
||||
@@ -1,28 +1,31 @@
|
||||
import os
|
||||
import pprint
|
||||
import re
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.models.fields import IPAddressField
|
||||
from django.shortcuts import get_object_or_404
|
||||
from logs.models import AuditLog
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import api_view, permission_classes
|
||||
from rest_framework.exceptions import ParseError
|
||||
from rest_framework.exceptions import ParseError, PermissionDenied
|
||||
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 tacticalrmm.permissions import (
|
||||
_has_perm_on_client,
|
||||
_has_perm_on_agent,
|
||||
_has_perm_on_site,
|
||||
)
|
||||
|
||||
from .models import CodeSignToken, CoreSettings, CustomField, GlobalKVStore, URLAction
|
||||
from .permissions import (
|
||||
CodeSignPerms,
|
||||
ViewCoreSettingsPerms,
|
||||
EditCoreSettingsPerms,
|
||||
CoreSettingsPerms,
|
||||
ServerMaintPerms,
|
||||
URLActionPerms,
|
||||
CustomFieldPerms,
|
||||
)
|
||||
from .serializers import (
|
||||
CodeSignTokenSerializer,
|
||||
@@ -34,7 +37,7 @@ from .serializers import (
|
||||
|
||||
|
||||
class UploadMeshAgent(APIView):
|
||||
permission_classes = [IsAuthenticated, MeshPerms]
|
||||
permission_classes = [IsAuthenticated, CoreSettingsPerms]
|
||||
parser_class = (FileUploadParser,)
|
||||
|
||||
def put(self, request, format=None):
|
||||
@@ -50,25 +53,25 @@ class UploadMeshAgent(APIView):
|
||||
for chunk in f.chunks():
|
||||
j.write(chunk)
|
||||
|
||||
return Response(status=status.HTTP_201_CREATED)
|
||||
return Response(
|
||||
"Mesh Agent uploaded successfully", status=status.HTTP_201_CREATED
|
||||
)
|
||||
|
||||
|
||||
@api_view()
|
||||
@permission_classes([IsAuthenticated, ViewCoreSettingsPerms])
|
||||
def get_core_settings(request):
|
||||
settings = CoreSettings.objects.first()
|
||||
return Response(CoreSettingsSerializer(settings).data)
|
||||
class GetEditCoreSettings(APIView):
|
||||
permission_classes = [IsAuthenticated, CoreSettingsPerms]
|
||||
|
||||
def get(self, 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)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
def put(self, request):
|
||||
coresettings = CoreSettings.objects.first()
|
||||
serializer = CoreSettingsSerializer(instance=coresettings, data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
|
||||
return Response("ok")
|
||||
return Response("ok")
|
||||
|
||||
|
||||
@api_view()
|
||||
@@ -100,7 +103,8 @@ def dashboard_info(request):
|
||||
)
|
||||
|
||||
|
||||
@api_view()
|
||||
@api_view(["POST"])
|
||||
@permission_classes([IsAuthenticated, CoreSettingsPerms])
|
||||
def email_test(request):
|
||||
core = CoreSettings.objects.first()
|
||||
r = core.send_mail(
|
||||
@@ -169,10 +173,13 @@ def server_maintenance(request):
|
||||
|
||||
|
||||
class GetAddCustomFields(APIView):
|
||||
permission_classes = [IsAuthenticated, EditCoreSettingsPerms]
|
||||
permission_classes = [IsAuthenticated, CustomFieldPerms]
|
||||
|
||||
def get(self, request):
|
||||
fields = CustomField.objects.all()
|
||||
if "model" in request.query_params.keys():
|
||||
fields = CustomField.objects.filter(model=request.query_params["model"])
|
||||
else:
|
||||
fields = CustomField.objects.all()
|
||||
return Response(CustomFieldSerializer(fields, many=True).data)
|
||||
|
||||
def patch(self, request):
|
||||
@@ -191,7 +198,7 @@ class GetAddCustomFields(APIView):
|
||||
|
||||
|
||||
class GetUpdateDeleteCustomFields(APIView):
|
||||
permission_classes = [IsAuthenticated, EditCoreSettingsPerms]
|
||||
permission_classes = [IsAuthenticated, CustomFieldPerms]
|
||||
|
||||
def get(self, request, pk):
|
||||
custom_field = get_object_or_404(CustomField, pk=pk)
|
||||
@@ -274,13 +281,15 @@ class CodeSign(APIView):
|
||||
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)
|
||||
agent_ids: list[str] = list(
|
||||
Agent.objects.only("pk", "agent_id").values_list("agent_id", flat=True)
|
||||
)
|
||||
force_code_sign.delay(agent_ids=agent_ids)
|
||||
return Response("Agents will be code signed shortly")
|
||||
|
||||
|
||||
class GetAddKeyStore(APIView):
|
||||
permission_classes = [IsAuthenticated, EditCoreSettingsPerms]
|
||||
permission_classes = [IsAuthenticated, CoreSettingsPerms]
|
||||
|
||||
def get(self, request):
|
||||
keys = GlobalKVStore.objects.all()
|
||||
@@ -295,7 +304,7 @@ class GetAddKeyStore(APIView):
|
||||
|
||||
|
||||
class UpdateDeleteKeyStore(APIView):
|
||||
permission_classes = [IsAuthenticated, EditCoreSettingsPerms]
|
||||
permission_classes = [IsAuthenticated, CoreSettingsPerms]
|
||||
|
||||
def put(self, request, pk):
|
||||
key = get_object_or_404(GlobalKVStore, pk=pk)
|
||||
@@ -313,6 +322,8 @@ class UpdateDeleteKeyStore(APIView):
|
||||
|
||||
|
||||
class GetAddURLAction(APIView):
|
||||
permission_classes = [IsAuthenticated, CoreSettingsPerms]
|
||||
|
||||
def get(self, request):
|
||||
actions = URLAction.objects.all()
|
||||
return Response(URLActionSerializer(actions, many=True).data)
|
||||
@@ -326,6 +337,8 @@ class GetAddURLAction(APIView):
|
||||
|
||||
|
||||
class UpdateDeleteURLAction(APIView):
|
||||
permission_classes = [IsAuthenticated, CoreSettingsPerms]
|
||||
|
||||
def put(self, request, pk):
|
||||
action = get_object_or_404(URLAction, pk=pk)
|
||||
|
||||
@@ -344,6 +357,8 @@ class UpdateDeleteURLAction(APIView):
|
||||
|
||||
|
||||
class RunURLAction(APIView):
|
||||
permission_classes = [IsAuthenticated, URLActionPerms]
|
||||
|
||||
def patch(self, request):
|
||||
from requests.utils import requote_uri
|
||||
|
||||
@@ -351,11 +366,20 @@ class RunURLAction(APIView):
|
||||
from clients.models import Client, Site
|
||||
from tacticalrmm.utils import replace_db_values
|
||||
|
||||
if "agent" in request.data.keys():
|
||||
instance = get_object_or_404(Agent, pk=request.data["agent"])
|
||||
if "agent_id" in request.data.keys():
|
||||
if not _has_perm_on_agent(request.user, request.data["agent_id"]):
|
||||
raise PermissionDenied()
|
||||
|
||||
instance = get_object_or_404(Agent, agent_id=request.data["agent_id"])
|
||||
elif "site" in request.data.keys():
|
||||
if not _has_perm_on_site(request.user, request.data["site"]):
|
||||
raise PermissionDenied()
|
||||
|
||||
instance = get_object_or_404(Site, pk=request.data["site"])
|
||||
elif "client" in request.data.keys():
|
||||
if not _has_perm_on_client(request.user, request.data["client"]):
|
||||
raise PermissionDenied()
|
||||
|
||||
instance = get_object_or_404(Client, pk=request.data["client"])
|
||||
else:
|
||||
return notify_error("received an incorrect request")
|
||||
@@ -382,8 +406,9 @@ class RunURLAction(APIView):
|
||||
|
||||
|
||||
class TwilioSMSTest(APIView):
|
||||
def get(self, request):
|
||||
from twilio.rest import Client as TwClient
|
||||
permission_classes = [IsAuthenticated, CoreSettingsPerms]
|
||||
|
||||
def post(self, request):
|
||||
|
||||
core = CoreSettings.objects.first()
|
||||
if not core.sms_is_configured:
|
||||
@@ -391,14 +416,9 @@ class TwilioSMSTest(APIView):
|
||||
"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))
|
||||
r = core.send_sms("TacticalRMM Test SMS", test=True)
|
||||
|
||||
return Response("SMS Test OK!")
|
||||
if not isinstance(r, bool) and isinstance(r, str):
|
||||
return notify_error(r)
|
||||
|
||||
return Response("SMS Test sent successfully!")
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.6 on 2021-09-17 19:54
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("logs", "0018_auto_20210905_1606"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="auditlog",
|
||||
name="username",
|
||||
field=models.CharField(max_length=255),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.6 on 2021-10-14 17:05
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('logs', '0019_alter_auditlog_username'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='auditlog',
|
||||
name='agent_id',
|
||||
field=models.CharField(blank=True, max_length=50, null=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.6 on 2021-10-18 03:04
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('logs', '0020_alter_auditlog_agent_id'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='auditlog',
|
||||
name='agent_id',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
]
|
||||
26
api/tacticalrmm/logs/migrations/0022_auto_20211105_0158.py
Normal file
26
api/tacticalrmm/logs/migrations/0022_auto_20211105_0158.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# Generated by Django 3.2.6 on 2021-11-05 01:58
|
||||
|
||||
from django.db import migrations
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
|
||||
|
||||
def update_agent_field(apps, schema_editor):
|
||||
AuditLog = apps.get_model("logs", "AuditLog")
|
||||
Agent = apps.get_model("agents", "Agent")
|
||||
for log in AuditLog.objects.exclude(agent_id=None):
|
||||
try:
|
||||
log.agent_id = Agent.objects.get(pk=log.agent_id).agent_id
|
||||
log.save()
|
||||
except (ObjectDoesNotExist, ValueError):
|
||||
pass
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('logs', '0021_alter_auditlog_agent_id'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(update_agent_field, migrations.RunPython.noop),
|
||||
]
|
||||
@@ -3,6 +3,7 @@ from abc import abstractmethod
|
||||
|
||||
from django.db import models
|
||||
from tacticalrmm.middleware import get_debug_info, get_username
|
||||
from tacticalrmm.models import PermissionQuerySet
|
||||
|
||||
|
||||
def get_debug_level():
|
||||
@@ -65,9 +66,9 @@ STATUS_CHOICES = [
|
||||
|
||||
|
||||
class AuditLog(models.Model):
|
||||
username = models.CharField(max_length=100)
|
||||
username = models.CharField(max_length=255)
|
||||
agent = models.CharField(max_length=255, null=True, blank=True)
|
||||
agent_id = models.PositiveIntegerField(blank=True, null=True)
|
||||
agent_id = models.CharField(max_length=255, blank=True, null=True)
|
||||
entry_time = models.DateTimeField(auto_now_add=True)
|
||||
action = models.CharField(max_length=100, choices=AUDIT_ACTION_TYPE_CHOICES)
|
||||
object_type = models.CharField(max_length=100, choices=AUDIT_OBJECT_TYPE_CHOICES)
|
||||
@@ -94,7 +95,7 @@ class AuditLog(models.Model):
|
||||
AuditLog.objects.create(
|
||||
username=username,
|
||||
agent=agent.hostname,
|
||||
agent_id=agent.id,
|
||||
agent_id=agent.agent_id,
|
||||
object_type="agent",
|
||||
action="remote_session",
|
||||
message=f"{username} used Mesh Central to initiate a remote session to {agent.hostname}.",
|
||||
@@ -106,6 +107,7 @@ class AuditLog(models.Model):
|
||||
AuditLog.objects.create(
|
||||
username=username,
|
||||
agent=agent.hostname,
|
||||
agent_id=agent.agent_id,
|
||||
object_type="agent",
|
||||
action="execute_command",
|
||||
message=f"{username} issued {shell} command on {agent.hostname}.",
|
||||
@@ -120,7 +122,8 @@ class AuditLog(models.Model):
|
||||
AuditLog.objects.create(
|
||||
username=username,
|
||||
object_type=object_type,
|
||||
agent_id=before["id"] if object_type == "agent" else None,
|
||||
agent=before["hostname"] if object_type == "agent" else None,
|
||||
agent_id=before["agent_id"] if object_type == "agent" else None,
|
||||
action="modify",
|
||||
message=f"{username} modified {object_type} {name}",
|
||||
before_value=before,
|
||||
@@ -133,7 +136,8 @@ class AuditLog(models.Model):
|
||||
AuditLog.objects.create(
|
||||
username=username,
|
||||
object_type=object_type,
|
||||
agent=after["id"] if object_type == "agent" else None,
|
||||
agent=after["hostname"] if object_type == "agent" else None,
|
||||
agent_id=after["agent_id"] if object_type == "agent" else None,
|
||||
action="add",
|
||||
message=f"{username} added {object_type} {name}",
|
||||
after_value=after,
|
||||
@@ -145,7 +149,8 @@ class AuditLog(models.Model):
|
||||
AuditLog.objects.create(
|
||||
username=username,
|
||||
object_type=object_type,
|
||||
agent=before["id"] if object_type == "agent" else None,
|
||||
agent=before["hostname"] if object_type == "agent" else None,
|
||||
agent_id=before["agent_id"] if object_type == "agent" else None,
|
||||
action="delete",
|
||||
message=f"{username} deleted {object_type} {name}",
|
||||
before_value=before,
|
||||
@@ -156,7 +161,7 @@ class AuditLog(models.Model):
|
||||
def audit_script_run(username, agent, script, debug_info={}):
|
||||
AuditLog.objects.create(
|
||||
agent=agent.hostname,
|
||||
agent_id=agent.id,
|
||||
agent_id=agent.agent_id,
|
||||
username=username,
|
||||
object_type="agent",
|
||||
action="execute_script",
|
||||
@@ -202,7 +207,7 @@ class AuditLog(models.Model):
|
||||
AuditLog.objects.create(
|
||||
username=username,
|
||||
agent=instance.hostname if classname == "Agent" else None,
|
||||
agent_id=instance.id if classname == "Agent" else None,
|
||||
agent_id=instance.agent_id if classname == "Agent" else None,
|
||||
object_type=classname.lower(),
|
||||
action="url_action",
|
||||
message=f"{username} ran url action: {urlaction.pattern} on {classname}: {name}",
|
||||
@@ -227,7 +232,7 @@ class AuditLog(models.Model):
|
||||
site = Site.objects.get(pk=affected["site"])
|
||||
target = f"on all agents within site: {site.client.name}\\{site.name}"
|
||||
elif affected["target"] == "agents":
|
||||
agents = Agent.objects.filter(pk__in=affected["agents"]).values_list(
|
||||
agents = Agent.objects.filter(agent_id__in=affected["agents"]).values_list(
|
||||
"hostname", flat=True
|
||||
)
|
||||
target = "on multiple agents"
|
||||
@@ -266,6 +271,8 @@ LOG_TYPE_CHOICES = [
|
||||
|
||||
|
||||
class DebugLog(models.Model):
|
||||
objects = PermissionQuerySet.as_manager()
|
||||
|
||||
entry_time = models.DateTimeField(auto_now_add=True)
|
||||
agent = models.ForeignKey(
|
||||
"agents.Agent",
|
||||
@@ -317,6 +324,7 @@ class DebugLog(models.Model):
|
||||
|
||||
|
||||
class PendingAction(models.Model):
|
||||
objects = PermissionQuerySet.as_manager()
|
||||
|
||||
agent = models.ForeignKey(
|
||||
"agents.Agent",
|
||||
@@ -377,9 +385,9 @@ class BaseAuditModel(models.Model):
|
||||
abstract = True
|
||||
|
||||
# create audit fields
|
||||
created_by = models.CharField(max_length=100, null=True, blank=True)
|
||||
created_by = models.CharField(max_length=255, null=True, blank=True)
|
||||
created_time = models.DateTimeField(auto_now_add=True, null=True, blank=True)
|
||||
modified_by = models.CharField(max_length=100, null=True, blank=True)
|
||||
modified_by = models.CharField(max_length=255, null=True, blank=True)
|
||||
modified_time = models.DateTimeField(auto_now=True, null=True, blank=True)
|
||||
|
||||
@abstractmethod
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from rest_framework import permissions
|
||||
|
||||
from tacticalrmm.permissions import _has_perm
|
||||
from tacticalrmm.permissions import _has_perm, _has_perm_on_agent
|
||||
|
||||
|
||||
class AuditLogPerms(permissions.BasePermission):
|
||||
@@ -8,12 +8,17 @@ class AuditLogPerms(permissions.BasePermission):
|
||||
return _has_perm(r, "can_view_auditlogs")
|
||||
|
||||
|
||||
class ManagePendingActionPerms(permissions.BasePermission):
|
||||
class PendingActionPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
if r.method == "PATCH":
|
||||
return True
|
||||
|
||||
return _has_perm(r, "can_manage_pendingactions")
|
||||
if r.method == "GET":
|
||||
if "agent_id" in view.kwargs.keys():
|
||||
return _has_perm(r, "can_list_pendingactions") and _has_perm_on_agent(
|
||||
r.user, view.kwargs["agent_id"]
|
||||
)
|
||||
else:
|
||||
return _has_perm(r, "can_list_pendingactions")
|
||||
else:
|
||||
return _has_perm(r, "can_manage_pendingactions")
|
||||
|
||||
|
||||
class DebugLogPerms(permissions.BasePermission):
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from tacticalrmm.utils import get_default_timezone
|
||||
|
||||
from .models import AuditLog, DebugLog, PendingAction
|
||||
|
||||
@@ -14,8 +13,8 @@ class AuditLogSerializer(serializers.ModelSerializer):
|
||||
fields = "__all__"
|
||||
|
||||
def get_entry_time(self, log):
|
||||
timezone = get_default_timezone()
|
||||
return log.entry_time.astimezone(timezone).strftime("%m %d %Y %H:%M:%S")
|
||||
tz = self.context["default_tz"]
|
||||
return log.entry_time.astimezone(tz).strftime("%m %d %Y %H:%M:%S")
|
||||
|
||||
|
||||
class PendingActionSerializer(serializers.ModelSerializer):
|
||||
@@ -40,5 +39,5 @@ class DebugLogSerializer(serializers.ModelSerializer):
|
||||
fields = "__all__"
|
||||
|
||||
def get_entry_time(self, log):
|
||||
timezone = get_default_timezone()
|
||||
return log.entry_time.astimezone(timezone).strftime("%m %d %Y %H:%M:%S")
|
||||
tz = self.context["default_tz"]
|
||||
return log.entry_time.astimezone(tz).strftime("%m %d %Y %H:%M:%S")
|
||||
|
||||
@@ -5,7 +5,7 @@ from django.utils import timezone as djangotime
|
||||
from model_bakery import baker, seq
|
||||
from tacticalrmm.test import TacticalTestCase
|
||||
|
||||
from logs.models import PendingAction
|
||||
base_url = "/logs"
|
||||
|
||||
|
||||
class TestAuditViews(TacticalTestCase):
|
||||
@@ -26,14 +26,14 @@ class TestAuditViews(TacticalTestCase):
|
||||
"logs.agent_logs",
|
||||
username="jim",
|
||||
agent="AgentHostname1",
|
||||
agent_id=agent1.id,
|
||||
agent_id=agent1.agent_id,
|
||||
_quantity=15,
|
||||
)
|
||||
baker.make_recipe(
|
||||
"logs.agent_logs",
|
||||
username="jim",
|
||||
agent="AgentHostname2",
|
||||
agent_id=agent2.id,
|
||||
agent_id=agent2.agent_id,
|
||||
_quantity=8,
|
||||
)
|
||||
|
||||
@@ -42,14 +42,14 @@ class TestAuditViews(TacticalTestCase):
|
||||
"logs.agent_logs",
|
||||
username="james",
|
||||
agent="AgentHostname1",
|
||||
agent_id=agent1.id,
|
||||
agent_id=agent1.agent_id,
|
||||
_quantity=7,
|
||||
)
|
||||
baker.make_recipe(
|
||||
"logs.agent_logs",
|
||||
username="james",
|
||||
agent="AgentHostname2",
|
||||
agent_id=agent2.id,
|
||||
agent_id=agent2.agent_id,
|
||||
_quantity=10,
|
||||
)
|
||||
|
||||
@@ -57,7 +57,7 @@ class TestAuditViews(TacticalTestCase):
|
||||
baker.make_recipe(
|
||||
"logs.agent_logs",
|
||||
agent=seq("AgentHostname"),
|
||||
agent_id=seq(agent1.id),
|
||||
agent_id=seq(agent1.agent_id),
|
||||
_quantity=5,
|
||||
)
|
||||
|
||||
@@ -85,7 +85,7 @@ class TestAuditViews(TacticalTestCase):
|
||||
return {"site": site, "agents": [agent0, agent1, agent2]}
|
||||
|
||||
def test_get_audit_logs(self):
|
||||
url = "/logs/auditlogs/"
|
||||
url = "/logs/audit/"
|
||||
|
||||
# create data
|
||||
data = self.create_audit_records()
|
||||
@@ -96,14 +96,14 @@ class TestAuditViews(TacticalTestCase):
|
||||
{
|
||||
"filter": {
|
||||
"timeFilter": 45,
|
||||
"agentFilter": [data["agents"][2].id],
|
||||
"agentFilter": [data["agents"][2].agent_id],
|
||||
},
|
||||
"count": 19,
|
||||
"count": 18,
|
||||
},
|
||||
{
|
||||
"filter": {
|
||||
"userFilter": ["jim"],
|
||||
"agentFilter": [data["agents"][1].id],
|
||||
"agentFilter": [data["agents"][1].agent_id],
|
||||
},
|
||||
"count": 15,
|
||||
},
|
||||
@@ -111,7 +111,7 @@ class TestAuditViews(TacticalTestCase):
|
||||
"filter": {
|
||||
"timeFilter": 180,
|
||||
"userFilter": ["james"],
|
||||
"agentFilter": [data["agents"][1].id],
|
||||
"agentFilter": [data["agents"][1].agent_id],
|
||||
},
|
||||
"count": 7,
|
||||
},
|
||||
@@ -122,8 +122,8 @@ class TestAuditViews(TacticalTestCase):
|
||||
"timeFilter": 35,
|
||||
"userFilter": ["james", "jim"],
|
||||
"agentFilter": [
|
||||
data["agents"][1].id,
|
||||
data["agents"][2].id,
|
||||
data["agents"][1].agent_id,
|
||||
data["agents"][2].agent_id,
|
||||
],
|
||||
},
|
||||
"count": 40,
|
||||
@@ -133,7 +133,7 @@ class TestAuditViews(TacticalTestCase):
|
||||
{"filter": {"actionFilter": ["login"]}, "count": 12},
|
||||
{
|
||||
"filter": {"clientFilter": [data["site"].client.id]},
|
||||
"count": 23,
|
||||
"count": 22,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -180,36 +180,15 @@ class TestAuditViews(TacticalTestCase):
|
||||
_quantity=14,
|
||||
)
|
||||
|
||||
data = {"showCompleted": False}
|
||||
r = self.client.patch(url, data, format="json")
|
||||
r = self.client.get(url, format="json")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(len(r.data["actions"]), 12) # type: ignore
|
||||
self.assertEqual(r.data["completed_count"], 14) # type: ignore
|
||||
self.assertEqual(r.data["total"], 26) # type: ignore
|
||||
self.assertEqual(len(r.data), 26) # type: ignore
|
||||
|
||||
PendingAction.objects.filter(action_type="chocoinstall").update(
|
||||
status="completed"
|
||||
)
|
||||
data = {"showCompleted": True}
|
||||
r = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(len(r.data["actions"]), 26) # type: ignore
|
||||
self.assertEqual(r.data["completed_count"], 26) # type: ignore
|
||||
self.assertEqual(r.data["total"], 26) # type: ignore
|
||||
|
||||
data = {"showCompleted": True, "agentPK": agent1.pk}
|
||||
r = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(len(r.data["actions"]), 12) # type: ignore
|
||||
self.assertEqual(r.data["completed_count"], 12) # type: ignore
|
||||
self.assertEqual(r.data["total"], 12) # type: ignore
|
||||
|
||||
self.check_not_authenticated("patch", url)
|
||||
self.check_not_authenticated("get", url)
|
||||
|
||||
@patch("agents.models.Agent.nats_cmd")
|
||||
def test_cancel_pending_action(self, nats_cmd):
|
||||
nats_cmd.return_value = "ok"
|
||||
url = "/logs/pendingactions/"
|
||||
agent = baker.make_recipe("agents.online_agent")
|
||||
action = baker.make(
|
||||
"logs.PendingAction",
|
||||
@@ -221,8 +200,9 @@ class TestAuditViews(TacticalTestCase):
|
||||
},
|
||||
)
|
||||
|
||||
data = {"pk": action.pk} # type: ignore
|
||||
r = self.client.delete(url, data, format="json")
|
||||
url = f"{base_url}/pendingactions/{action.id}/"
|
||||
|
||||
r = self.client.delete(url, format="json")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
nats_data = {
|
||||
"func": "delschedtask",
|
||||
@@ -231,7 +211,7 @@ class TestAuditViews(TacticalTestCase):
|
||||
nats_cmd.assert_called_with(nats_data, timeout=10)
|
||||
|
||||
# try request again and it should 404 since pending action doesn't exist
|
||||
r = self.client.delete(url, data, format="json")
|
||||
r = self.client.delete(url, format="json")
|
||||
self.assertEqual(r.status_code, 404)
|
||||
|
||||
nats_cmd.reset_mock()
|
||||
@@ -246,16 +226,17 @@ class TestAuditViews(TacticalTestCase):
|
||||
},
|
||||
)
|
||||
|
||||
data = {"pk": action2.pk} # type: ignore
|
||||
nats_cmd.return_value = "error deleting sched task"
|
||||
r = self.client.delete(url, data, format="json")
|
||||
r = self.client.delete(
|
||||
f"{base_url}/pendingactions/{action2.id}/", format="json"
|
||||
)
|
||||
self.assertEqual(r.status_code, 400)
|
||||
self.assertEqual(r.data, "error deleting sched task") # type: ignore
|
||||
|
||||
self.check_not_authenticated("delete", url)
|
||||
|
||||
def test_get_debug_log(self):
|
||||
url = "/logs/debuglog/"
|
||||
url = "/logs/debug/"
|
||||
|
||||
# create data
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
@@ -275,13 +256,13 @@ class TestAuditViews(TacticalTestCase):
|
||||
)
|
||||
|
||||
# test agent filter
|
||||
data = {"agentFilter": agent.id}
|
||||
data = {"agentFilter": agent.agent_id}
|
||||
resp = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(len(resp.data), 4) # type: ignore
|
||||
|
||||
# test log type filter and agent
|
||||
data = {"agentFilter": agent.id, "logLevelFilter": "warning"}
|
||||
data = {"agentFilter": agent.agent_id, "logLevelFilter": "warning"}
|
||||
resp = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(len(resp.data), 1) # type: ignore
|
||||
@@ -294,6 +275,203 @@ class TestAuditViews(TacticalTestCase):
|
||||
|
||||
self.check_not_authenticated("patch", url)
|
||||
|
||||
def test_auditlog_permissions(self):
|
||||
site = self.create_audit_records()["site"]
|
||||
|
||||
url = f"{base_url}/audit/"
|
||||
|
||||
data = {
|
||||
"pagination": {
|
||||
"rowsPerPage": 100,
|
||||
"page": 1,
|
||||
"sortBy": "entry_time",
|
||||
"descending": True,
|
||||
}
|
||||
}
|
||||
|
||||
# test superuser access
|
||||
self.check_authorized_superuser("patch", url, data)
|
||||
|
||||
user = self.create_user_with_roles([])
|
||||
self.client.force_authenticate(user=user) # type: ignore
|
||||
|
||||
# test user without role
|
||||
self.check_not_authorized("patch", url, data)
|
||||
|
||||
# add user to role and test
|
||||
user.role.can_view_auditlogs = True
|
||||
user.role.save()
|
||||
|
||||
response = self.check_authorized("patch", url, data)
|
||||
self.assertEqual(len(response.data["audit_logs"]), 86) # type: ignore
|
||||
|
||||
# limit user to client if agent check
|
||||
user.role.can_view_sites.set([site])
|
||||
|
||||
response = self.check_authorized("patch", url, data)
|
||||
self.assertEqual(len(response.data["audit_logs"]), 63) # type: ignore
|
||||
|
||||
# limit user to client if agent check
|
||||
user.role.can_view_clients.set([site.client])
|
||||
response = self.check_authorized("patch", url, data)
|
||||
self.assertEqual(len(response.data["audit_logs"]), 63) # type: ignore
|
||||
|
||||
def test_debuglog_permissions(self):
|
||||
|
||||
# create data
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
agent2 = baker.make_recipe("agents.agent")
|
||||
baker.make(
|
||||
"logs.DebugLog",
|
||||
log_level=cycle(["error", "info", "warning", "critical"]),
|
||||
log_type="agent_issues",
|
||||
agent=agent,
|
||||
_quantity=4,
|
||||
)
|
||||
|
||||
baker.make(
|
||||
"logs.DebugLog",
|
||||
log_level=cycle(["error", "info", "warning", "critical"]),
|
||||
log_type="agent_issues",
|
||||
agent=agent2,
|
||||
_quantity=8,
|
||||
)
|
||||
|
||||
baker.make(
|
||||
"logs.DebugLog",
|
||||
log_type="system_issues",
|
||||
log_level=cycle(["error", "info", "warning", "critical"]),
|
||||
_quantity=15,
|
||||
)
|
||||
|
||||
url = f"{base_url}/debug/"
|
||||
|
||||
# test superuser access
|
||||
self.check_authorized_superuser(
|
||||
"patch",
|
||||
url,
|
||||
)
|
||||
|
||||
user = self.create_user_with_roles([])
|
||||
self.client.force_authenticate(user=user) # type: ignore
|
||||
|
||||
# test user without role
|
||||
self.check_not_authorized("patch", url)
|
||||
|
||||
# add user to role and test
|
||||
user.role.can_view_debuglogs = True
|
||||
user.role.save()
|
||||
|
||||
response = self.check_authorized("patch", url)
|
||||
self.assertEqual(len(response.data), 27) # type: ignore
|
||||
|
||||
# limit user to site
|
||||
user.role.can_view_sites.set([agent.site])
|
||||
|
||||
response = self.check_authorized("patch", url)
|
||||
self.assertEqual(len(response.data), 19) # type: ignore
|
||||
|
||||
# limit user to client
|
||||
user.role.can_view_sites.clear()
|
||||
user.role.can_view_clients.set([agent2.site.client])
|
||||
response = self.check_authorized("patch", url)
|
||||
self.assertEqual(len(response.data), 23) # type: ignore
|
||||
|
||||
# limit user to client and site
|
||||
user.role.can_view_sites.set([agent.site])
|
||||
user.role.can_view_clients.set([agent2.site.client])
|
||||
response = self.check_authorized("patch", url)
|
||||
self.assertEqual(len(response.data), 27) # type: ignore
|
||||
|
||||
def test_get_pendingaction_permissions(self):
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
unauthorized_agent = baker.make_recipe("agents.agent")
|
||||
actions = baker.make("logs.PendingAction", agent=agent, _quantity=5)
|
||||
unauthorized_actions = baker.make(
|
||||
"logs.PendingAction", agent=unauthorized_agent, _quantity=7
|
||||
)
|
||||
|
||||
# test super user access
|
||||
self.check_authorized_superuser("get", f"{base_url}/pendingactions/")
|
||||
self.check_authorized_superuser(
|
||||
"get", f"/agents/{agent.agent_id}/pendingactions/"
|
||||
)
|
||||
self.check_authorized_superuser(
|
||||
"get", f"/agents/{unauthorized_agent.agent_id}/pendingactions/"
|
||||
)
|
||||
|
||||
user = self.create_user_with_roles([])
|
||||
self.client.force_authenticate(user=user) # type: ignore
|
||||
|
||||
self.check_not_authorized("get", f"{base_url}/pendingactions/")
|
||||
self.check_not_authorized("get", f"/agents/{agent.agent_id}/pendingactions/")
|
||||
self.check_not_authorized(
|
||||
"get", f"/agents/{unauthorized_agent.agent_id}/pendingactions/"
|
||||
)
|
||||
|
||||
# add list software role to user
|
||||
user.role.can_list_pendingactions = True
|
||||
user.role.save()
|
||||
|
||||
r = self.check_authorized("get", f"{base_url}/pendingactions/")
|
||||
self.assertEqual(len(r.data), 12) # type: ignore
|
||||
r = self.check_authorized("get", f"/agents/{agent.agent_id}/pendingactions/")
|
||||
self.assertEqual(len(r.data), 5) # type: ignore
|
||||
r = self.check_authorized(
|
||||
"get", f"/agents/{unauthorized_agent.agent_id}/pendingactions/"
|
||||
)
|
||||
self.assertEqual(len(r.data), 7) # type: ignore
|
||||
|
||||
# test limiting to client
|
||||
user.role.can_view_clients.set([agent.client])
|
||||
self.check_not_authorized(
|
||||
"get", f"/agents/{unauthorized_agent.agent_id}/pendingactions/"
|
||||
)
|
||||
self.check_authorized("get", f"/agents/{agent.agent_id}/pendingactions/")
|
||||
|
||||
# make sure queryset is limited too
|
||||
r = self.client.get(f"{base_url}/pendingactions/")
|
||||
self.assertEqual(len(r.data), 5) # type: ignore
|
||||
|
||||
@patch("agents.models.Agent.nats_cmd", return_value="ok")
|
||||
@patch("logs.models.PendingAction.delete")
|
||||
def test_delete_pendingaction_permissions(self, delete, nats_cmd):
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
unauthorized_agent = baker.make_recipe("agents.agent")
|
||||
action = baker.make(
|
||||
"logs.PendingAction", agent=agent, details={"taskname": "Task"}
|
||||
)
|
||||
unauthorized_action = baker.make(
|
||||
"logs.PendingAction", agent=unauthorized_agent, details={"taskname": "Task"}
|
||||
)
|
||||
|
||||
url = f"{base_url}/pendingactions/{action.id}/"
|
||||
unauthorized_url = f"{base_url}/pendingactions/{unauthorized_action.id}/"
|
||||
|
||||
# test superuser access
|
||||
self.check_authorized_superuser("delete", url)
|
||||
self.check_authorized_superuser("delete", unauthorized_url)
|
||||
|
||||
user = self.create_user_with_roles([])
|
||||
self.client.force_authenticate(user=user) # type: ignore
|
||||
|
||||
# test user without role
|
||||
self.check_not_authorized("delete", url)
|
||||
self.check_not_authorized("delete", unauthorized_url)
|
||||
|
||||
# add user to role and test
|
||||
user.role.can_manage_pendingactions = True
|
||||
user.role.save()
|
||||
|
||||
self.check_authorized("delete", url)
|
||||
self.check_authorized("delete", unauthorized_url)
|
||||
|
||||
# limit user to site
|
||||
user.role.can_view_sites.set([agent.site])
|
||||
|
||||
self.check_authorized("delete", url)
|
||||
self.check_not_authorized("delete", unauthorized_url)
|
||||
|
||||
|
||||
class TestLogTasks(TacticalTestCase):
|
||||
def test_prune_debug_log(self):
|
||||
|
||||
@@ -4,6 +4,7 @@ from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path("pendingactions/", views.PendingActions.as_view()),
|
||||
path("auditlogs/", views.GetAuditLogs.as_view()),
|
||||
path("debuglog/", views.GetDebugLog.as_view()),
|
||||
path("pendingactions/<int:pk>/", views.PendingActions.as_view()),
|
||||
path("audit/", views.GetAuditLogs.as_view()),
|
||||
path("debug/", views.GetDebugLog.as_view()),
|
||||
]
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
import asyncio
|
||||
from datetime import datetime as dt
|
||||
|
||||
from accounts.models import User
|
||||
from accounts.serializers import UserSerializer
|
||||
from agents.models import Agent
|
||||
from agents.serializers import AgentHostnameSerializer
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils import timezone as djangotime
|
||||
from rest_framework import status
|
||||
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 rest_framework.exceptions import PermissionDenied
|
||||
from tacticalrmm.utils import notify_error, get_default_timezone
|
||||
from tacticalrmm.permissions import _audit_log_filter, _has_perm_on_agent
|
||||
|
||||
from .models import AuditLog, PendingAction, DebugLog
|
||||
from .permissions import AuditLogPerms, DebugLogPerms, ManagePendingActionPerms
|
||||
from agents.models import Agent
|
||||
from .permissions import AuditLogPerms, DebugLogPerms, PendingActionPerms
|
||||
from .serializers import AuditLogSerializer, DebugLogSerializer, PendingActionSerializer
|
||||
|
||||
|
||||
@@ -46,13 +44,11 @@ class GetAuditLogs(APIView):
|
||||
agentFilter = Q(agent_id__in=request.data["agentFilter"])
|
||||
|
||||
elif "clientFilter" in request.data:
|
||||
clients = Client.objects.filter(
|
||||
pk__in=request.data["clientFilter"]
|
||||
).values_list("id")
|
||||
agents = Agent.objects.filter(site__client_id__in=clients).values_list(
|
||||
"hostname"
|
||||
clients = Client.objects.filter(pk__in=request.data["clientFilter"])
|
||||
agents = Agent.objects.filter(site__client__in=clients).values_list(
|
||||
"agent_id"
|
||||
)
|
||||
clientFilter = Q(agent__in=agents)
|
||||
clientFilter = Q(agent_id__in=agents)
|
||||
|
||||
if "userFilter" in request.data:
|
||||
userFilter = Q(username__in=request.data["userFilter"])
|
||||
@@ -76,14 +72,16 @@ class GetAuditLogs(APIView):
|
||||
.filter(actionFilter)
|
||||
.filter(objectFilter)
|
||||
.filter(timeFilter)
|
||||
.filter(_audit_log_filter(request.user))
|
||||
).order_by(order_by)
|
||||
|
||||
paginator = Paginator(audit_logs, pagination["rowsPerPage"])
|
||||
ctx = {"default_tz": get_default_timezone()}
|
||||
|
||||
return Response(
|
||||
{
|
||||
"audit_logs": AuditLogSerializer(
|
||||
paginator.get_page(pagination["page"]), many=True
|
||||
paginator.get_page(pagination["page"]), many=True, context=ctx
|
||||
).data,
|
||||
"total": paginator.count,
|
||||
}
|
||||
@@ -91,37 +89,23 @@ class GetAuditLogs(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():
|
||||
actions = PendingAction.objects.filter(
|
||||
agent__pk=request.data["agentPK"], status=status_filter
|
||||
)
|
||||
total = PendingAction.objects.filter(
|
||||
agent__pk=request.data["agentPK"]
|
||||
).count()
|
||||
completed = PendingAction.objects.filter(
|
||||
agent__pk=request.data["agentPK"], status="completed"
|
||||
).count()
|
||||
permission_classes = [IsAuthenticated, PendingActionPerms]
|
||||
|
||||
def get(self, request, agent_id=None):
|
||||
if agent_id:
|
||||
agent = get_object_or_404(Agent, agent_id=agent_id)
|
||||
actions = PendingAction.objects.filter(agent=agent)
|
||||
else:
|
||||
actions = PendingAction.objects.filter(status=status_filter).select_related(
|
||||
"agent"
|
||||
)
|
||||
total = PendingAction.objects.count()
|
||||
completed = PendingAction.objects.filter(status="completed").count()
|
||||
actions = PendingAction.objects.filter_by_role(request.user)
|
||||
|
||||
ret = {
|
||||
"actions": PendingActionSerializer(actions, many=True).data,
|
||||
"completed_count": completed,
|
||||
"total": total,
|
||||
}
|
||||
return Response(ret)
|
||||
return Response(PendingActionSerializer(actions, many=True).data)
|
||||
|
||||
def delete(self, request, pk):
|
||||
action = get_object_or_404(PendingAction, pk=pk)
|
||||
|
||||
if not _has_perm_on_agent(request.user, action.agent.agent_id):
|
||||
raise PermissionDenied()
|
||||
|
||||
def delete(self, request):
|
||||
action = get_object_or_404(PendingAction, pk=request.data["pk"])
|
||||
nats_data = {
|
||||
"func": "delschedtask",
|
||||
"schedtaskpayload": {"name": action.details["taskname"]},
|
||||
@@ -138,7 +122,6 @@ class GetDebugLog(APIView):
|
||||
permission_classes = [IsAuthenticated, DebugLogPerms]
|
||||
|
||||
def patch(self, request):
|
||||
|
||||
agentFilter = Q()
|
||||
logTypeFilter = Q()
|
||||
logLevelFilter = Q()
|
||||
@@ -150,12 +133,18 @@ class GetDebugLog(APIView):
|
||||
logLevelFilter = Q(log_level=request.data["logLevelFilter"])
|
||||
|
||||
if "agentFilter" in request.data:
|
||||
agentFilter = Q(agent=request.data["agentFilter"])
|
||||
agentFilter = Q(agent__agent_id=request.data["agentFilter"])
|
||||
|
||||
debug_logs = (
|
||||
DebugLog.objects.filter(logLevelFilter)
|
||||
DebugLog.objects.prefetch_related("agent")
|
||||
.filter_by_role(request.user)
|
||||
.filter(logLevelFilter)
|
||||
.filter(agentFilter)
|
||||
.filter(logTypeFilter)
|
||||
)
|
||||
|
||||
return Response(DebugLogSerializer(debug_logs, many=True).data)
|
||||
ctx = {"default_tz": get_default_timezone()}
|
||||
ret = DebugLogSerializer(
|
||||
debug_logs.order_by("-entry_time")[0:1000], many=True, context=ctx
|
||||
).data
|
||||
return Response(ret)
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
coverage
|
||||
coveralls
|
||||
coveralls==3.2.0
|
||||
model_bakery
|
||||
@@ -1,37 +1,38 @@
|
||||
asgiref==3.4.1
|
||||
asyncio-nats-client==0.11.4
|
||||
celery==5.1.2
|
||||
certifi==2021.5.30
|
||||
cffi==1.14.6
|
||||
celery==5.2.1
|
||||
certifi==2021.10.8
|
||||
cffi==1.15.0
|
||||
channels==3.0.4
|
||||
channels_redis==3.3.0
|
||||
channels_redis==3.3.1
|
||||
chardet==4.0.0
|
||||
cryptography==3.4.8
|
||||
cryptography==35.0.0
|
||||
daphne==3.0.2
|
||||
Django==3.2.7
|
||||
django-cors-headers==3.8.0
|
||||
django-ipware==3.0.2
|
||||
Django==3.2.9
|
||||
django-cors-headers==3.10.0
|
||||
django-ipware==4.0.0
|
||||
django-rest-knox==4.1.0
|
||||
djangorestframework==3.12.4
|
||||
future==0.18.2
|
||||
loguru==0.5.3
|
||||
msgpack==1.0.2
|
||||
packaging==21.0
|
||||
psycopg2-binary==2.9.1
|
||||
pycparser==2.20
|
||||
pycryptodome==3.10.1
|
||||
packaging==21.3
|
||||
psycopg2-binary==2.9.2
|
||||
pycparser==2.21
|
||||
pycryptodome==3.11.0
|
||||
pyotp==2.6.0
|
||||
pyparsing==2.4.7
|
||||
pytz==2021.1
|
||||
pytz==2021.3
|
||||
qrcode==6.1
|
||||
redis==3.5.3
|
||||
requests==2.26.0
|
||||
six==1.16.0
|
||||
sqlparse==0.4.1
|
||||
twilio==6.63.1
|
||||
urllib3==1.26.6
|
||||
uWSGI==2.0.19.1
|
||||
sqlparse==0.4.2
|
||||
twilio==7.3.1
|
||||
urllib3==1.26.7
|
||||
uWSGI==2.0.20
|
||||
validators==0.18.2
|
||||
vine==5.0.0
|
||||
websockets==9.1
|
||||
zipp==3.5.0
|
||||
zipp==3.6.0
|
||||
drf_spectacular==0.21.0
|
||||
@@ -30,18 +30,27 @@
|
||||
"default_timeout": "300"
|
||||
},
|
||||
{
|
||||
"guid": "2ee134d5-76aa-4160-b334-a1efbc62079f",
|
||||
"filename": "Win_Install_Duplicati.ps1",
|
||||
"submittedBy": "https://github.com/Omnicef",
|
||||
"name": "Duplicati - Install",
|
||||
"description": "This script installs Duplicati 2.0.5.1 as a service.",
|
||||
"shell": "powershell",
|
||||
"guid": "7b1d90a1-3eda-48ab-9c49-20e714c9e82a",
|
||||
"filename": "Win_Duplicati_Install.bat",
|
||||
"submittedBy": "https://github.com/dinger1986",
|
||||
"name": "Duplicati - Install 2.0.6.100 to work with Community Check Status",
|
||||
"description": "This script installs Duplicati 2.0.6.100 as a service and creates status files to be used with commuity check",
|
||||
"shell": "cmd",
|
||||
"category": "TRMM (Win):3rd Party Software",
|
||||
"default_timeout": "300"
|
||||
},
|
||||
{
|
||||
"guid": "7c14beb4-d1c3-41aa-8e70-92a267d6e080",
|
||||
"filename": "Win_Duplicati_Status.ps1",
|
||||
"submittedBy": "https://github.com/dinger1986",
|
||||
"name": "Duplicati - Check Status",
|
||||
"description": "Checks Duplicati Backup is running properly over the last 24 hours",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):3rd Party Software>Monitoring"
|
||||
},
|
||||
{
|
||||
"guid": "81cc5bcb-01bf-4b0c-89b9-0ac0f3fe0c04",
|
||||
"filename": "Win_Reset_Windows_Update.ps1",
|
||||
"filename": "Win_Windows_Update_Reset.ps1",
|
||||
"submittedBy": "https://github.com/Omnicef",
|
||||
"name": "Windows Update - Reset",
|
||||
"description": "This script will reset all of the Windows Updates components to DEFAULT SETTINGS.",
|
||||
@@ -91,17 +100,19 @@
|
||||
"guid": "9d34f482-1f0c-4b2f-b65f-a9cf3c13ef5f",
|
||||
"filename": "Win_TRMM_Rename_Installed_App.ps1",
|
||||
"submittedBy": "https://github.com/bradhawkins85",
|
||||
"name": "TacticalRMM Agent Rename",
|
||||
"name": "TacticalRMM - Agent Rename",
|
||||
"description": "Updates the DisplayName registry entry for the Tactical RMM windows agent to your desired name. This script takes 1 required argument: the name you wish to set.",
|
||||
"syntax": "<string>",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):TacticalRMM Related"
|
||||
},
|
||||
{
|
||||
"guid": "525ae965-1dcf-4c17-92b3-5da3cf6819f5",
|
||||
"filename": "Win_Bitlocker_Encrypted_Drive_c.ps1",
|
||||
"submittedBy": "https://github.com/ThatsNASt",
|
||||
"name": "Bitlocker - Check C Drive for Status",
|
||||
"description": "Runs a check on drive C for Bitlocker status.",
|
||||
"filename": "Win_Bitlocker_Drive_Check_Status.ps1",
|
||||
"submittedBy": "https://github.com/silversword411",
|
||||
"name": "Bitlocker - Check Drive for Status",
|
||||
"description": "Runs a check on drive for Bitlocker status. Returns 0 if Bitlocker is not enabled, 1 if Bitlocker is enabled",
|
||||
"syntax": "[Drive <string>]",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Storage"
|
||||
},
|
||||
@@ -227,22 +238,23 @@
|
||||
"default_timeout": "25000"
|
||||
},
|
||||
{
|
||||
"guid": "375323e5-cac6-4f35-a304-bb7cef35902d",
|
||||
"filename": "Win_Disk_Status.ps1",
|
||||
"submittedBy": "https://github.com/dinger1986",
|
||||
"name": "Disk Hardware Health Check (using Event Viewer errors)",
|
||||
"description": "Checks local disks for errors reported in event viewer within the last 24 hours",
|
||||
"guid": "4d0ba685-2259-44be-9010-8ed2fa48bf74",
|
||||
"filename": "Win_Win11_Ready.ps1",
|
||||
"submittedBy": "https://github.com/adamjrberry/",
|
||||
"name": "Windows 11 Upgrade capable check",
|
||||
"description": "Checks to see if machine is Win11 capable",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Hardware"
|
||||
"category": "TRMM (Win):Updates",
|
||||
"default_timeout": "3600"
|
||||
},
|
||||
{
|
||||
"guid": "7c14beb4-d1c3-41aa-8e70-92a267d6e080",
|
||||
"filename": "Win_Duplicati_Status.ps1",
|
||||
"guid": "375323e5-cac6-4f35-a304-bb7cef35902d",
|
||||
"filename": "Win_Disk_Volume_Status.ps1",
|
||||
"submittedBy": "https://github.com/dinger1986",
|
||||
"name": "Duplicati - Check Status",
|
||||
"description": "Checks Duplicati Backup is running properly over the last 24 hours",
|
||||
"name": "Disk Drive Volume Health Check (using Event Viewer errors)",
|
||||
"description": "Checks Drive Volumes for errors reported in event viewer within the last 24 hours",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):3rd Party Software"
|
||||
"category": "TRMM (Win):Hardware"
|
||||
},
|
||||
{
|
||||
"guid": "907652a5-9ec1-4759-9871-a7743f805ff2",
|
||||
@@ -317,7 +329,7 @@
|
||||
},
|
||||
{
|
||||
"guid": "a821975c-60df-4d58-8990-6cf8a55b4ee0",
|
||||
"filename": "Win_Sync_Time.bat",
|
||||
"filename": "Win_Time_Sync.bat",
|
||||
"submittedBy": "https://github.com/dinger1986",
|
||||
"name": "ADDC - Sync DC Time",
|
||||
"description": "Syncs time with domain controller",
|
||||
@@ -425,6 +437,7 @@
|
||||
"submittedBy": "https://github.com/silversword411",
|
||||
"name": "Chocolatey - Install, Uninstall and Upgrade Software",
|
||||
"description": "This script installs, uninstalls and updates software using Chocolatey with logic to slow tasks to minimize hitting community limits. Mode install/uninstall/upgrade Hosts x",
|
||||
"syntax": "-$PackageName <string>\n[-Hosts <string>]\n[-mode {(install) | update | uninstall}]",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):3rd Party Software>Chocolatey",
|
||||
"default_timeout": "600"
|
||||
@@ -478,17 +491,18 @@
|
||||
"guid": "08ca81f2-f044-4dfc-ad47-090b19b19d76",
|
||||
"filename": "Win_User_Logged_in_with_Temp_Profile.ps1",
|
||||
"submittedBy": "https://github.com/dinger1986",
|
||||
"name": "User Logged in with temp profile check",
|
||||
"name": "User Check - See if user logged in with temp profile",
|
||||
"description": "Check if users are logged in with a temp profile",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Other"
|
||||
},
|
||||
{
|
||||
"guid": "5d905886-9eb1-4129-8b81-a013f842eb24",
|
||||
"filename": "Win_Rename_Computer.ps1",
|
||||
"filename": "Win_Computer_Rename.ps1",
|
||||
"submittedBy": "https://github.com/silversword411",
|
||||
"name": "Rename Computer",
|
||||
"description": "Rename computer. First parameter will be new PC name. 2nd parameter if yes will auto-reboot machine",
|
||||
"syntax": "-NewName <string>\n[-Username <string>]\n[-Password <string>]\n[-Restart]",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Other",
|
||||
"default_timeout": 30
|
||||
@@ -499,6 +513,7 @@
|
||||
"submittedBy": "https://github.com/tremor021",
|
||||
"name": "Power - Restart or Shutdown PC",
|
||||
"description": "Restart PC. Add parameter: shutdown if you want to shutdown computer",
|
||||
"syntax": "[shutdown]",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Updates"
|
||||
},
|
||||
@@ -523,7 +538,7 @@
|
||||
"-url {{client.ScreenConnectInstaller}}",
|
||||
"-clientname {{client.name}}",
|
||||
"-sitename {{site.name}}",
|
||||
"-action install"
|
||||
"-action {(install) | uninstall | start | stop}"
|
||||
],
|
||||
"default_timeout": "90",
|
||||
"shell": "powershell",
|
||||
@@ -573,7 +588,7 @@
|
||||
"guid": "7c0c7e37-60ff-462f-9c34-b5cd4c4796a7",
|
||||
"filename": "Win_Wifi_SSID_and_Password_Retrieval.ps1",
|
||||
"submittedBy": "https://github.com/silversword411",
|
||||
"name": "Network Wireless - Retrieve Saved passwords",
|
||||
"name": "Network Wireless - Retrieve Saved WiFi passwords",
|
||||
"description": "Returns all saved wifi passwords stored on the computer",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Network",
|
||||
@@ -624,7 +639,7 @@
|
||||
"filename": "Win_Network_TCP_Reset_Stack.bat",
|
||||
"submittedBy": "https://github.com/silversword411",
|
||||
"name": "Network - Reset tcp using netsh",
|
||||
"description": "resets tcp stack using netsh",
|
||||
"description": "Resets TCP stack using netsh",
|
||||
"shell": "cmd",
|
||||
"category": "TRMM (Win):Network",
|
||||
"default_timeout": "120"
|
||||
@@ -633,7 +648,7 @@
|
||||
"guid": "6ce5682a-49db-4c0b-9417-609cf905ac43",
|
||||
"filename": "Win_Win10_Change_Key_and_Activate.ps1",
|
||||
"submittedBy": "https://github.com/silversword411",
|
||||
"name": "Product Key in Win10 Change and Activate",
|
||||
"name": "Product Key in Win10 - Change and Activate",
|
||||
"description": "Insert new product key and Activate. Requires 1 parameter the product key you want to use",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Other",
|
||||
@@ -653,7 +668,7 @@
|
||||
"guid": "83f6c6ea-6120-4fd3-bec8-d3abc505dcdf",
|
||||
"filename": "Win_TRMM_Start_Menu_Delete_Shortcut.ps1",
|
||||
"submittedBy": "https://github.com/silversword411",
|
||||
"name": "TacticalRMM Delete Start Menu Shortcut for App",
|
||||
"name": "TacticalRMM - Delete Start Menu Shortcut for App",
|
||||
"description": "Delete its application shortcut that's installed in the start menu by default",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):TacticalRMM Related",
|
||||
@@ -735,19 +750,20 @@
|
||||
"guid": "6a52f495-d43e-40f4-91a9-bbe4f578e6d1",
|
||||
"filename": "Win_User_Create.ps1",
|
||||
"submittedBy": "https://github.com/brodur",
|
||||
"name": "Create Local User",
|
||||
"name": "User - Create Local",
|
||||
"description": "Create a local user. Parameters are: username, password and optional: description, fullname, group (adds to Users if not specified)",
|
||||
"syntax": "-username <string>\n-password <string>\n[-description <string>]\n[-fullname <string>]\n[-group <string>]",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Other"
|
||||
"category": "TRMM (Win):User Management"
|
||||
},
|
||||
{
|
||||
"guid": "57997ec7-b293-4fd5-9f90-a25426d0eb90",
|
||||
"filename": "Win_Users_List.ps1",
|
||||
"submittedBy": "https://github.com/tremor021",
|
||||
"name": "Get Computer Users",
|
||||
"name": "Users - List Local Users and Enabled/Disabled Status",
|
||||
"description": "Get list of computer users and show which one is enabled",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Other"
|
||||
"category": "TRMM (Win):User Management"
|
||||
},
|
||||
{
|
||||
"guid": "77da9c87-5a7a-4ba1-bdde-3eeb3b01d62d",
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.2.6 on 2021-09-17 19:54
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("scripts", "0011_auto_20210731_1707"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="script",
|
||||
name="created_by",
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="script",
|
||||
name="modified_by",
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
]
|
||||
18
api/tacticalrmm/scripts/migrations/0013_script_syntax.py
Normal file
18
api/tacticalrmm/scripts/migrations/0013_script_syntax.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.6 on 2021-11-13 16:25
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('scripts', '0012_auto_20210917_1954'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='script',
|
||||
name='syntax',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.6 on 2021-11-19 15:44
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('scripts', '0013_script_syntax'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='script',
|
||||
name='filename',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
]
|
||||
@@ -24,7 +24,7 @@ class Script(BaseAuditModel):
|
||||
guid = models.CharField(max_length=64, null=True, blank=True)
|
||||
name = models.CharField(max_length=255)
|
||||
description = models.TextField(null=True, blank=True, default="")
|
||||
filename = models.CharField(max_length=255) # deprecated
|
||||
filename = models.CharField(max_length=255, null=True, blank=True)
|
||||
shell = models.CharField(
|
||||
max_length=100, choices=SCRIPT_SHELLS, default="powershell"
|
||||
)
|
||||
@@ -37,6 +37,7 @@ class Script(BaseAuditModel):
|
||||
blank=True,
|
||||
default=list,
|
||||
)
|
||||
syntax = TextField(null=True, blank=True)
|
||||
favorite = models.BooleanField(default=False)
|
||||
category = models.CharField(max_length=100, null=True, blank=True)
|
||||
code_base64 = models.TextField(null=True, blank=True, default="")
|
||||
@@ -115,6 +116,8 @@ class Script(BaseAuditModel):
|
||||
|
||||
args = script["args"] if "args" in script.keys() else []
|
||||
|
||||
syntax = script["syntax"] if "syntax" in script.keys() else ""
|
||||
|
||||
if s.exists():
|
||||
i = s.first()
|
||||
i.name = script["name"] # type: ignore
|
||||
@@ -123,6 +126,8 @@ class Script(BaseAuditModel):
|
||||
i.shell = script["shell"] # type: ignore
|
||||
i.default_timeout = default_timeout # type: ignore
|
||||
i.args = args # type: ignore
|
||||
i.syntax = syntax # type: ignore
|
||||
i.filename = script["filename"] # type: ignore
|
||||
|
||||
with open(os.path.join(scripts_dir, script["filename"]), "rb") as f:
|
||||
script_bytes = (
|
||||
@@ -139,6 +144,8 @@ class Script(BaseAuditModel):
|
||||
"code_base64",
|
||||
"shell",
|
||||
"args",
|
||||
"filename",
|
||||
"syntax",
|
||||
]
|
||||
)
|
||||
|
||||
@@ -157,6 +164,8 @@ class Script(BaseAuditModel):
|
||||
s.shell = script["shell"]
|
||||
s.default_timeout = default_timeout
|
||||
s.args = args
|
||||
s.filename = script["filename"]
|
||||
s.syntax = syntax
|
||||
|
||||
with open(
|
||||
os.path.join(scripts_dir, script["filename"]), "rb"
|
||||
@@ -178,6 +187,8 @@ class Script(BaseAuditModel):
|
||||
"code_base64",
|
||||
"shell",
|
||||
"args",
|
||||
"filename",
|
||||
"syntax",
|
||||
]
|
||||
)
|
||||
|
||||
@@ -200,6 +211,8 @@ class Script(BaseAuditModel):
|
||||
category=category,
|
||||
default_timeout=default_timeout,
|
||||
args=args,
|
||||
filename=script["filename"],
|
||||
syntax=syntax,
|
||||
).save()
|
||||
|
||||
# delete community scripts that had their name changed
|
||||
|
||||
@@ -3,9 +3,9 @@ from rest_framework import permissions
|
||||
from tacticalrmm.permissions import _has_perm
|
||||
|
||||
|
||||
class ManageScriptsPerms(permissions.BasePermission):
|
||||
class ScriptsPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
if r.method == "GET":
|
||||
return True
|
||||
|
||||
return _has_perm(r, "can_manage_scripts")
|
||||
return _has_perm(r, "can_list_scripts")
|
||||
else:
|
||||
return _has_perm(r, "can_manage_scripts")
|
||||
|
||||
@@ -16,6 +16,8 @@ class ScriptTableSerializer(ModelSerializer):
|
||||
"category",
|
||||
"favorite",
|
||||
"default_timeout",
|
||||
"syntax",
|
||||
"filename",
|
||||
]
|
||||
|
||||
|
||||
@@ -32,6 +34,8 @@ class ScriptSerializer(ModelSerializer):
|
||||
"favorite",
|
||||
"code_base64",
|
||||
"default_timeout",
|
||||
"syntax",
|
||||
"filename",
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -118,14 +118,12 @@ class TestScriptViews(TacticalTestCase):
|
||||
|
||||
self.check_not_authenticated("get", url)
|
||||
|
||||
@patch("agents.models.Agent.nats_cmd")
|
||||
@patch("agents.models.Agent.nats_cmd", return_value="return value")
|
||||
def test_test_script(self, run_script):
|
||||
url = "/scripts/testscript/"
|
||||
|
||||
run_script.return_value = "return value"
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
url = f"/scripts/{agent.agent_id}/test/"
|
||||
|
||||
data = {
|
||||
"agent": agent.pk,
|
||||
"code": "some_code",
|
||||
"timeout": 90,
|
||||
"args": [],
|
||||
@@ -161,7 +159,7 @@ class TestScriptViews(TacticalTestCase):
|
||||
|
||||
def test_download_script(self):
|
||||
# test a call where script doesn't exist
|
||||
resp = self.client.get("/scripts/download/500/", format="json")
|
||||
resp = self.client.get("/scripts/500/download/", format="json")
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
# return script code property should be "Test"
|
||||
@@ -170,7 +168,7 @@ class TestScriptViews(TacticalTestCase):
|
||||
script = baker.make(
|
||||
"scripts.Script", code_base64="VGVzdA==", shell="powershell"
|
||||
)
|
||||
url = f"/scripts/download/{script.pk}/" # type: ignore
|
||||
url = f"/scripts/{script.pk}/download/" # type: ignore
|
||||
|
||||
resp = self.client.get(url, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
@@ -178,7 +176,7 @@ class TestScriptViews(TacticalTestCase):
|
||||
|
||||
# test batch file
|
||||
script = baker.make("scripts.Script", code_base64="VGVzdA==", shell="cmd")
|
||||
url = f"/scripts/download/{script.pk}/" # type: ignore
|
||||
url = f"/scripts/{script.pk}/download/" # type: ignore
|
||||
|
||||
resp = self.client.get(url, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
@@ -186,7 +184,7 @@ class TestScriptViews(TacticalTestCase):
|
||||
|
||||
# test python file
|
||||
script = baker.make("scripts.Script", code_base64="VGVzdA==", shell="python")
|
||||
url = f"/scripts/download/{script.pk}/" # type: ignore
|
||||
url = f"/scripts/{script.pk}/download/" # type: ignore
|
||||
|
||||
resp = self.client.get(url, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
@@ -7,6 +7,6 @@ urlpatterns = [
|
||||
path("<int:pk>/", views.GetUpdateDeleteScript.as_view()),
|
||||
path("snippets/", views.GetAddScriptSnippets.as_view()),
|
||||
path("snippets/<int:pk>/", views.GetUpdateDeleteScriptSnippet.as_view()),
|
||||
path("testscript/", views.TestScript.as_view()),
|
||||
path("download/<int:pk>/", views.download),
|
||||
path("<agent:agent_id>/test/", views.TestScript.as_view()),
|
||||
path("<int:pk>/download/", views.download),
|
||||
]
|
||||
|
||||
@@ -9,7 +9,7 @@ from rest_framework.views import APIView
|
||||
from tacticalrmm.utils import notify_error
|
||||
|
||||
from .models import Script, ScriptSnippet
|
||||
from .permissions import ManageScriptsPerms
|
||||
from .permissions import ScriptsPerms
|
||||
from agents.permissions import RunScriptPerms
|
||||
from .serializers import (
|
||||
ScriptSerializer,
|
||||
@@ -19,7 +19,7 @@ from .serializers import (
|
||||
|
||||
|
||||
class GetAddScripts(APIView):
|
||||
permission_classes = [IsAuthenticated, ManageScriptsPerms]
|
||||
permission_classes = [IsAuthenticated, ScriptsPerms]
|
||||
|
||||
def get(self, request):
|
||||
|
||||
@@ -41,7 +41,7 @@ class GetAddScripts(APIView):
|
||||
|
||||
|
||||
class GetUpdateDeleteScript(APIView):
|
||||
permission_classes = [IsAuthenticated, ManageScriptsPerms]
|
||||
permission_classes = [IsAuthenticated, ScriptsPerms]
|
||||
|
||||
def get(self, request, pk):
|
||||
script = get_object_or_404(Script, pk=pk)
|
||||
@@ -78,7 +78,7 @@ class GetUpdateDeleteScript(APIView):
|
||||
|
||||
|
||||
class GetAddScriptSnippets(APIView):
|
||||
permission_classes = [IsAuthenticated, ManageScriptsPerms]
|
||||
permission_classes = [IsAuthenticated, ScriptsPerms]
|
||||
|
||||
def get(self, request):
|
||||
snippets = ScriptSnippet.objects.all()
|
||||
@@ -94,7 +94,7 @@ class GetAddScriptSnippets(APIView):
|
||||
|
||||
|
||||
class GetUpdateDeleteScriptSnippet(APIView):
|
||||
permission_classes = [IsAuthenticated, ManageScriptsPerms]
|
||||
permission_classes = [IsAuthenticated, ScriptsPerms]
|
||||
|
||||
def get(self, request, pk):
|
||||
snippet = get_object_or_404(ScriptSnippet, pk=pk)
|
||||
@@ -121,11 +121,11 @@ class GetUpdateDeleteScriptSnippet(APIView):
|
||||
class TestScript(APIView):
|
||||
permission_classes = [IsAuthenticated, RunScriptPerms]
|
||||
|
||||
def post(self, request):
|
||||
def post(self, request, agent_id):
|
||||
from .models import Script
|
||||
from agents.models import Agent
|
||||
|
||||
agent = get_object_or_404(Agent, pk=request.data["agent"])
|
||||
agent = get_object_or_404(Agent, agent_id=agent_id)
|
||||
|
||||
parsed_args = Script.parse_script_args(
|
||||
agent, request.data["shell"], request.data["args"]
|
||||
@@ -148,8 +148,8 @@ class TestScript(APIView):
|
||||
return Response(r)
|
||||
|
||||
|
||||
@api_view()
|
||||
@permission_classes([IsAuthenticated, ManageScriptsPerms])
|
||||
@api_view(["GET"])
|
||||
@permission_classes([IsAuthenticated, ScriptsPerms])
|
||||
def download(request, pk):
|
||||
script = get_object_or_404(Script, pk=pk)
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,13 @@
|
||||
from rest_framework import permissions
|
||||
|
||||
from tacticalrmm.permissions import _has_perm
|
||||
from tacticalrmm.permissions import _has_perm, _has_perm_on_agent
|
||||
|
||||
|
||||
class ManageWinSvcsPerms(permissions.BasePermission):
|
||||
class WinSvcsPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
return _has_perm(r, "can_manage_winsvcs")
|
||||
if "agent_id" in view.kwargs.keys():
|
||||
return _has_perm(r, "can_manage_winsvcs") and _has_perm_on_agent(
|
||||
r.user, view.kwargs["agent_id"]
|
||||
)
|
||||
else:
|
||||
return _has_perm(r, "can_manage_winsvcs")
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from agents.models import Agent
|
||||
|
||||
|
||||
class ServicesSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Agent
|
||||
fields = (
|
||||
"hostname",
|
||||
"pk",
|
||||
"services",
|
||||
)
|
||||
@@ -5,28 +5,22 @@ from model_bakery import baker
|
||||
from agents.models import Agent
|
||||
from tacticalrmm.test import TacticalTestCase
|
||||
|
||||
base_url = "/services"
|
||||
|
||||
|
||||
class TestServiceViews(TacticalTestCase):
|
||||
def setUp(self):
|
||||
self.authenticate()
|
||||
self.setup_coresettings()
|
||||
|
||||
def test_default_services(self):
|
||||
url = "/services/defaultservices/"
|
||||
resp = self.client.get(url, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(type(resp.data), list)
|
||||
|
||||
self.check_not_authenticated("get", url)
|
||||
|
||||
@patch("agents.models.Agent.nats_cmd")
|
||||
def test_get_services(self, nats_cmd):
|
||||
# test a call where agent doesn't exist
|
||||
resp = self.client.get("/services/500/services/", format="json")
|
||||
resp = self.client.get("/services/500234hjk348982h/", format="json")
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
agent = baker.make_recipe("agents.agent_with_services")
|
||||
url = f"/services/{agent.pk}/services/"
|
||||
url = f"{base_url}/{agent.agent_id}/"
|
||||
|
||||
nats_return = [
|
||||
{
|
||||
@@ -69,16 +63,16 @@ class TestServiceViews(TacticalTestCase):
|
||||
|
||||
@patch("agents.models.Agent.nats_cmd")
|
||||
def test_service_action(self, nats_cmd):
|
||||
url = "/services/serviceaction/"
|
||||
|
||||
invalid_data = {"pk": 500, "sv_name": "AeLookupSvc", "sv_action": "restart"}
|
||||
data = {"sv_action": "restart"}
|
||||
# test a call where agent doesn't exist
|
||||
resp = self.client.post(url, invalid_data, format="json")
|
||||
resp = self.client.post(
|
||||
f"{base_url}/kjhj4hj4khj34h34j/AeLookupSvc/", data, format="json"
|
||||
)
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
agent = baker.make_recipe("agents.agent_with_services")
|
||||
|
||||
data = {"pk": agent.pk, "sv_name": "AeLookupSvc", "sv_action": "restart"}
|
||||
url = f"/services/{agent.agent_id}/AeLookupSvc/"
|
||||
|
||||
# test failed attempt
|
||||
nats_cmd.return_value = "timeout"
|
||||
@@ -107,7 +101,7 @@ class TestServiceViews(TacticalTestCase):
|
||||
def test_service_detail(self, nats_cmd):
|
||||
# test a call where agent doesn't exist
|
||||
resp = self.client.get(
|
||||
"/services/500/doesntexist/servicedetail/", format="json"
|
||||
f"{base_url}/34kjhj3h4jh3kjh34/service_name/", format="json"
|
||||
)
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
@@ -123,7 +117,7 @@ class TestServiceViews(TacticalTestCase):
|
||||
}
|
||||
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
url = f"/services/{agent.pk}/alg/servicedetail/"
|
||||
url = f"{base_url}/{agent.agent_id}/alg/"
|
||||
|
||||
# test failed attempt
|
||||
nats_cmd.return_value = "timeout"
|
||||
@@ -147,25 +141,25 @@ class TestServiceViews(TacticalTestCase):
|
||||
|
||||
@patch("agents.models.Agent.nats_cmd")
|
||||
def test_edit_service(self, nats_cmd):
|
||||
url = "/services/editservice/"
|
||||
agent = baker.make_recipe("agents.agent_with_services")
|
||||
url = f"{base_url}/{agent.agent_id}/AeLookupSvc/"
|
||||
|
||||
invalid_data = {"pk": 500, "sv_name": "AeLookupSvc", "edit_action": "autodelay"}
|
||||
data = {"startType": "autodelay"}
|
||||
# test a call where agent doesn't exist
|
||||
resp = self.client.post(url, invalid_data, format="json")
|
||||
resp = self.client.put(
|
||||
f"{base_url}/234kjh2k3hkj23h4kj3h4k3jh/service/", data, format="json"
|
||||
)
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
data = {"pk": agent.pk, "sv_name": "AeLookupSvc", "edit_action": "autodelay"}
|
||||
|
||||
# test timeout
|
||||
nats_cmd.return_value = "timeout"
|
||||
resp = self.client.post(url, data, format="json")
|
||||
resp = self.client.put(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
nats_cmd.reset_mock()
|
||||
|
||||
# test successful attempt autodelay
|
||||
nats_cmd.return_value = {"success": True, "errormsg": ""}
|
||||
resp = self.client.post(url, data, format="json")
|
||||
resp = self.client.put(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
nats_cmd.assert_called_with(
|
||||
{
|
||||
@@ -180,20 +174,61 @@ class TestServiceViews(TacticalTestCase):
|
||||
nats_cmd.reset_mock()
|
||||
|
||||
# test error message from agent
|
||||
data = {"pk": agent.pk, "sv_name": "AeLookupSvc", "edit_action": "auto"}
|
||||
data = {"startType": "auto"}
|
||||
nats_cmd.return_value = {
|
||||
"success": False,
|
||||
"errormsg": "The parameter is incorrect",
|
||||
}
|
||||
resp = self.client.post(url, data, format="json")
|
||||
resp = self.client.put(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
nats_cmd.reset_mock()
|
||||
|
||||
# test catch all
|
||||
data = {"pk": agent.pk, "sv_name": "AeLookupSvc", "edit_action": "auto"}
|
||||
nats_cmd.return_value = {"success": False, "errormsg": ""}
|
||||
resp = self.client.post(url, data, format="json")
|
||||
resp = self.client.put(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
self.assertEqual(resp.data, "Something went wrong")
|
||||
|
||||
self.check_not_authenticated("post", url)
|
||||
self.check_not_authenticated("put", url)
|
||||
|
||||
|
||||
class TestServicePermissions(TacticalTestCase):
|
||||
def setUp(self):
|
||||
self.setup_coresettings()
|
||||
self.client_setup()
|
||||
|
||||
@patch("agents.models.Agent.nats_cmd", return_value="ok")
|
||||
def test_services_permissions(self, nats_cmd):
|
||||
agent = baker.make_recipe("agents.agent_with_services")
|
||||
unauthorized_agent = baker.make_recipe("agents.agent_with_services")
|
||||
|
||||
test_data = [
|
||||
{"url": f"{base_url}/{agent.agent_id}/", "method": "get"},
|
||||
{"url": f"{base_url}/{agent.agent_id}/service_name/", "method": "get"},
|
||||
{"url": f"{base_url}/{agent.agent_id}/service_name/", "method": "post"},
|
||||
{"url": f"{base_url}/{agent.agent_id}/service_name/", "method": "put"},
|
||||
]
|
||||
|
||||
for data in test_data:
|
||||
# test superuser
|
||||
self.check_authorized_superuser(data["method"], data["url"])
|
||||
|
||||
# test user with no roles
|
||||
user = self.create_user_with_roles([])
|
||||
self.client.force_authenticate(user=user)
|
||||
|
||||
self.check_not_authorized(data["method"], data["url"])
|
||||
|
||||
# test with correct role
|
||||
user.role.can_manage_winsvcs = True
|
||||
user.role.save()
|
||||
|
||||
self.check_authorized(data["method"], data["url"])
|
||||
|
||||
# test limiting user to client
|
||||
user.role.can_view_clients.set([agent.client])
|
||||
self.check_authorized(data["method"], data["url"])
|
||||
|
||||
user.role.can_view_clients.set([unauthorized_agent.client])
|
||||
|
||||
self.client.logout()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user