Compare commits

...

934 Commits

Author SHA1 Message Date
wh1te909
00c5f1365a bump versions 2021-12-20 06:48:02 +00:00
wh1te909
f7d317328a update reqs 2021-12-20 06:48:02 +00:00
wh1te909
3ccd705225 bump backup script version 2021-12-20 06:48:02 +00:00
diskraider
9e439fffaa Change timeout method
The current timeout results in an error "ERROR: Input redirection is not supported, exiting the process immediately.".

Reusing the ping tool to act as a timeout resolves this error because the batch script is not producing a user interruptable timeout but will still produce a 4-5 second timeout.
2021-12-20 06:48:02 +00:00
wh1te909
859dc170e7 update uninstall params 2021-12-20 06:48:02 +00:00
silversword411
1932d8fad9 docs - backup and silent uninstall tweaks 2021-12-20 06:48:02 +00:00
wh1te909
0c814ae436 reduce ram reqs 2021-12-20 06:48:02 +00:00
sadnub
89313d8a37 make post_update_tasks run on init container start 2021-12-20 06:48:02 +00:00
silversword411
2b85722222 docs - mesh download multiple 2021-12-20 06:48:02 +00:00
David Randall
57e5b0188c Fixes #872: backup.sh does not have EOL
Add EOL to backup.sh so CRON doesn't fail.
2021-12-20 06:48:02 +00:00
silversword411
2d7c830e70 docs code signing emphasis 2021-12-20 06:48:02 +00:00
silversword411
ccaa1790a9 docs - sys req info 2021-12-20 06:48:02 +00:00
silversword411
f6531d905e docs cron backups 2021-12-20 06:48:02 +00:00
silversword411
64a31879d3 docs - video of updating server 2021-12-20 06:48:02 +00:00
silversword411
0c6a4b1ed2 script - tweak AUOptions revert 2021-12-20 06:48:02 +00:00
silversword411
67801f39fe docs - 2rd party Screenconnect AIO 2021-12-20 06:48:02 +00:00
silversword411
892a0d67bf docs updating install agent script 2021-12-20 06:48:02 +00:00
silversword411
9fc0b7d5cc script_wip 2021-12-20 06:48:02 +00:00
bc24fl
22a614ef54 Added Printer Restart Jobs Community Script 2021-12-20 06:48:02 +00:00
silversword411
cd257b8e4d docs faq log4j 2021-12-20 06:48:02 +00:00
silversword
fa1ee2ca14 docs - updating index 2021-12-20 06:48:02 +00:00
wh1te909
34ea1adde6 sorting fixes #857 2021-12-20 06:48:02 +00:00
wh1te909
41cf8abb1f update reqs 2021-12-20 06:48:02 +00:00
silversword411
c0ffec1a4c docs - howitallworks nats server service 2021-12-20 06:48:02 +00:00
bc24fl
65779b8eaf Added Sophos Endpoint Install Community Script 2021-12-20 06:48:02 +00:00
bc24fl
c47bdb2d56 Added Sophos Endpoint Install Community Script 2021-12-20 06:48:02 +00:00
Michael Maertzdorf
d47ae642e7 Create SECURITY.md 2021-12-20 06:48:02 +00:00
Michael Maertzdorf
39c4609cc6 Create devskim-analysis.yml 2021-12-20 06:48:02 +00:00
dependabot[bot]
3ebba02a10 Bump django from 3.2.9 to 3.2.10 in /api/tacticalrmm
Bumps [django](https://github.com/django/django) from 3.2.9 to 3.2.10.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/3.2.9...3.2.10)

---
updated-dependencies:
- dependency-name: django
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-12-20 06:48:02 +00:00
Michael Maertzdorf
4dc7a96e79 Create codeql-analysis.yml 2021-12-20 06:48:02 +00:00
silversword411
5a49a29110 docs - nginx proxy info 2021-12-20 06:48:02 +00:00
sadnub
1e2a56c5e9 Release 0.10.4 2021-12-10 21:59:35 -05:00
sadnub
8011773af4 bump versions 2021-12-10 19:12:45 -05:00
sadnub
ddc69c692e formatting 2021-12-10 19:09:52 -05:00
sadnub
df925c9744 fix script tests 2021-12-10 19:04:20 -05:00
sadnub
1726341aad remove script hashing since it was erroring out on some characters 2021-12-10 18:51:32 -05:00
sadnub
63b1ccc7a7 fix deleted community scripts not being removed from database 2021-12-10 18:50:51 -05:00
Dan
e80397c857 Merge pull request #847 from silversword411/develop
Community scripts - adding software install report, parameters to task scheduler, bluescreen report
2021-12-09 09:25:03 -08:00
silversword411
81aa7ca1a4 community scripts - user enable/disable 2021-12-09 01:44:40 -05:00
silversword411
f0f7695890 community script - Windows Update revert to MS Auto managed 2021-12-09 01:16:49 -05:00
silversword411
e7e8ce2f7a community script - adding chocolately list installed 2021-12-09 01:09:47 -05:00
silversword411
ba37a3f18d script library - task scheduler adding parameters 2021-12-09 01:00:41 -05:00
silversword411
60b11a7a5d community scripts - new user monitor add parameters 2021-12-09 00:56:13 -05:00
silversword411
29461c20a7 script library - Bluescreen Report 2021-12-09 00:50:43 -05:00
silversword411
2ff1f34543 Community scripts - adding software install report 2021-12-09 00:40:37 -05:00
wh1te909
b75d7f970f use getattr with a default for optional settings 2021-12-09 00:08:49 +00:00
wh1te909
204681f097 fix openfile limit with 1k+ agents 2021-12-09 00:06:33 +00:00
wh1te909
e239fe95a4 remove old checks from update script 2021-12-08 18:37:44 +00:00
Dan
0a101f061a Merge pull request #844 from silversword411/develop
adding docs tips n trick and fixing PR #833
2021-12-07 20:03:07 -08:00
silversword411
f112a17afa Fixing community scripts and docs from PR #833 2021-12-07 22:29:37 -05:00
silversword411
54658a66d2 docs - Adding tips n tricks 2021-12-07 22:12:44 -05:00
sadnub
6b8f5a76e4 Merge pull request #833 from r3die/develop
Splashtop 3rd party integration docs and script
2021-12-07 19:47:52 -05:00
Dan
623a5d338d Merge pull request #842 from silversword411/develop
Adding Repo to Help menu
2021-12-06 20:30:38 -08:00
silversword411
9c5565cfd5 Merge branch 'develop' of https://github.com/silversword411/tacticalrmm into develop 2021-12-06 22:50:12 -05:00
silversword411
722f2efaee Adding Github repo to Help menu 2021-12-06 22:49:45 -05:00
Dan
4928264204 Merge pull request #841 from silversword411/develop
docs update: mgmt commands
2021-12-04 01:14:20 -08:00
silversword411
12d62ddc2a docs - adding mgmt commands for docker 2021-12-03 11:13:44 -05:00
wh1te909
da54e97217 Release 0.10.3 2021-12-02 08:19:38 +00:00
wh1te909
9c0993dac8 bump version 2021-12-02 07:50:52 +00:00
wh1te909
175486b7c4 fix bug where reboot_required field was not being updated when agent didn't have a patch policy and setting was set to 'inherit' 2021-12-02 01:39:41 +00:00
wh1te909
4760a287f6 update docs 2021-12-01 19:53:37 +00:00
wh1te909
0237b48c87 update reqs 2021-12-01 19:37:53 +00:00
Dan
95c9f22e6c Merge pull request #837 from silversword411/develop
docs tweak
2021-12-01 09:31:22 -08:00
silversword411
9b001219d5 docs tweak 2021-12-01 12:27:52 -05:00
Dan
6ff15efc7b Merge pull request #835 from silversword411/develop
docs outbound firewall rules
2021-11-30 16:07:56 -08:00
silversword411
6fe1dccc7e docs outbound firewall rules 2021-11-30 18:15:49 -05:00
sadnub
1c80f6f3fa Don't allow script arg variable assignment to callable attributes. Fixes #726 2021-11-29 22:18:05 -05:00
sadnub
54d3177fdd also don't include callable attributes with variable substitutions on alert scripts 2021-11-29 22:15:07 -05:00
r3die
a24ad245d2 splashtop 4rd party integration docs and script
splashtop 4rd party integration docs and script
2021-11-29 15:20:29 -08:00
wh1te909
f38cfdcadf fix test script 2021-11-29 18:51:18 +00:00
Dan
92e4ad8ccd Merge pull request #830 from silversword411/develop
docs updates
2021-11-29 09:20:14 -08:00
silversword411
3f3ab088d2 docs - adding bulk delete 2021-11-29 09:47:38 -05:00
sadnub
2c2cbaa175 formatting 2021-11-28 21:08:41 -05:00
sadnub
911b6bf863 fix sorting process cpu percentage. Fixes #831 2021-11-28 21:07:09 -05:00
sadnub
31462cab64 fix tests and also check for the correct script hash 2021-11-28 20:59:21 -05:00
silversword411
1ee35da62d docs updates 2021-11-28 15:31:37 -05:00
sadnub
edf4815595 make script file encoding consistent. utf-8 2021-11-28 13:23:47 -05:00
sadnub
06ccee5d18 add script hash field and calculate hash on script changes. Also removed storing scripts in DB as base64 strings. Should fix #634 2021-11-28 13:23:10 -05:00
sadnub
d5ad85725f fix duplicate package in dev requirements 2021-11-27 23:26:44 -05:00
sadnub
4d5bddb413 rework script form and add syntax field 2021-11-27 22:59:18 -05:00
Dan
2f4da7c381 Merge pull request #829 from ssteeltm/develop
Update unsupported_scripts.md
2021-11-26 16:11:50 -08:00
Dan
8b845fce03 Merge pull request #826 from NiceGuyIT/docs-howitallworks-services
Document server services and configuration
2021-11-26 16:11:24 -08:00
Dan
9fd15c38a9 Merge pull request #825 from silversword411/develop
scripts and docs
2021-11-26 16:11:00 -08:00
silversword411
ec1573d01f Merge branch 'develop' of https://github.com/silversword411/tacticalrmm into develop 2021-11-26 18:05:02 -05:00
silversword411
92ec1cc9e7 docs - add howitallworks to index 2021-11-26 18:05:00 -05:00
Hugo Sampaio
8b2f9665ce Update unsupported_scripts.md
Added info about how I run rmm behind Apache Proxy
( discord Hugo )
2021-11-26 17:25:42 -03:00
silversword411
cb388a5a78 scripts - adding demo server scripts 2021-11-26 14:35:33 -05:00
David Randall
7f4389ae08 Docs: Server services
Document the server services and configuration.
2021-11-25 16:59:19 -05:00
silversword411
76d71beaa2 script_wip addition 2021-11-25 15:06:15 -05:00
silversword411
31bb9c2197 docs - tips and tricks add mesh connection logs 2021-11-25 08:59:21 -05:00
wh1te909
6a2cd5c45a reduce celery memory usage and optimize a query 2021-11-25 06:20:06 +00:00
Dan
520632514b Merge pull request #823 from silversword411/develop
docs - video embed #1 and getting started started
2021-11-24 15:25:46 -08:00
silversword411
f998b28d0b docs - numbering fix 2021-11-24 17:34:43 -05:00
silversword411
1a6587e9e6 Merge branch 'develop' of https://github.com/silversword411/tacticalrmm into develop 2021-11-24 17:13:09 -05:00
silversword411
9b4b729d19 undo vscode spellcheck 2021-11-24 17:12:58 -05:00
silversword411
e80345295e script library - adding security audit 2021-11-24 14:06:44 -05:00
silversword411
026c259a2e added vscode spellcheck, shouldn't go public 2021-11-24 11:32:04 -05:00
silversword411
63474c2269 community scripts - adding syntax to defender enable 2021-11-24 11:30:03 -05:00
silversword411
faa1a9312f scripts - adding parameter check 2021-11-24 11:25:15 -05:00
silversword411
23fa0726d5 docs - v1 of getting started guide 2021-11-24 10:10:46 -05:00
silversword411
22210eaf7d Merge branch 'wh1te909:develop' into develop 2021-11-23 23:40:47 -05:00
silversword411
dcd8bee676 docs - video embed 2021-11-23 23:40:29 -05:00
silversword411
06f0fa8f0e Revert "docs - video embed testing"
This reverts commit 6d0f9e2cd5.
2021-11-23 23:37:54 -05:00
silversword411
6d0f9e2cd5 docs - video embed testing 2021-11-23 23:33:25 -05:00
sadnub
732afdb65d move custom fields to tab in edit agent modal 2021-11-23 21:35:11 -05:00
sadnub
1a9e8742f7 remove the need to type agent name to delete agents in dashboard 2021-11-23 21:35:11 -05:00
sadnub
b8eda37339 Fix setting alert template when policy assignment changes 2021-11-23 21:35:11 -05:00
sadnub
5107db6169 add drf_spectacular to dev requirements 2021-11-23 21:35:11 -05:00
wh1te909
2c8f207454 add mgmt command to bulk delete agents 2021-11-22 20:26:10 +00:00
wh1te909
489bc9c3b3 optimize some queries 2021-11-22 17:24:48 +00:00
wh1te909
514713e883 don't log swagger 2021-11-22 17:23:38 +00:00
wh1te909
17cc0cd09c forgot to check core settings fixes #816 2021-11-22 17:18:58 +00:00
Dan
4475df1295 Merge pull request #815 from tremor021/develop
Update Defender script
2021-11-22 09:06:04 -08:00
Dan
fdad267cfd Merge pull request #814 from silversword411/develop
docs updates
2021-11-22 09:05:19 -08:00
silversword411
3684fc80f0 docs - spellchecking 2021-11-22 08:07:49 -05:00
silversword411
e97a5fef94 script library - adding syntax to tooltip helper 2021-11-22 00:14:19 -05:00
silversword411
de2972631f docs - tips about running scripts syntax 2021-11-21 23:02:47 -05:00
tremor021
e5b8fd67c8 Update Defender script 2021-11-22 02:14:11 +01:00
silversword411
5fade89e2d docs - fixing install and restore docs to eliminate confusion 2021-11-21 15:18:18 -05:00
wh1te909
2eefedadb3 Release 0.10.2 2021-11-21 02:24:29 +00:00
wh1te909
e63d7a0b8a bump version 2021-11-21 02:24:07 +00:00
wh1te909
2a1b1849fa fix nats-api not working in docker 2021-11-21 02:02:29 +00:00
wh1te909
0461cb7f19 update docs 2021-11-20 22:21:02 +00:00
Dan
0932e0be03 Merge pull request #811 from silversword411/develop
docs updates
2021-11-20 14:17:11 -08:00
silversword411
4638ac9474 docs - reiterating no root and backup 2021-11-20 12:59:42 -05:00
silversword411
d8d7255029 docs - filter tips 2021-11-20 12:50:10 -05:00
wh1te909
fa05276c3f black 2021-11-19 20:00:22 +00:00
silversword411
e50a5d51d8 docs - troubleshooting enhancements 2021-11-19 14:14:12 -05:00
sadnub
c03ba78587 make swagger views optional 2021-11-19 13:58:38 -05:00
wh1te909
ff07c69e7d Release 0.10.1 2021-11-19 17:41:12 +00:00
wh1te909
735b84b26d bump version 2021-11-19 17:39:14 +00:00
sadnub
8dd069ad67 push models.py file update for scripts 2021-11-19 12:13:20 -05:00
sadnub
1857e68003 change filename db field to not be required 2021-11-19 10:46:40 -05:00
wh1te909
ff2508382a Release 0.10.0 2021-11-19 08:37:39 +00:00
wh1te909
9cb952b116 bump version 2021-11-19 08:04:25 +00:00
wh1te909
105e8089bb trigger an agent update task after rmm update 2021-11-19 07:25:32 +00:00
wh1te909
730f37f247 add debian 11 support and update reqs 2021-11-19 06:58:18 +00:00
wh1te909
284716751f update docs for new service 2021-11-19 06:32:15 +00:00
sadnub
8d0db699bf remove dynamic agent options function 2021-11-18 21:11:53 -05:00
Dan
53cf1cae58 Merge pull request #807 from silversword411/develop
docs and script adds
2021-11-18 12:32:22 -08:00
silversword411
307e4719e0 wip script - user enable/disabling 2021-11-18 12:23:52 -05:00
silversword411
5effae787a Community scripts - Fixing Drive Volume check 2021-11-18 10:45:25 -05:00
silversword411
6532be0b52 docs - reverting content tabs 2021-11-18 10:05:01 -05:00
silversword411
fb225a5347 community scripts add - Win11 check 2021-11-18 05:24:22 -05:00
silversword411
b83830a45e docs moving position 2021-11-18 05:20:12 -05:00
wh1te909
ca28288c33 add missing onMounted 2021-11-18 08:42:26 +00:00
wh1te909
b6f8d9cb25 change drive color based on percent closes #802 2021-11-18 07:46:17 +00:00
Dan
9cad0f11e5 Merge pull request #803 from silversword411/develop
Scripts and docs
2021-11-17 11:24:29 -08:00
silversword411
807be08566 docs - adding how to invalidate all auth tokens 2021-11-17 10:43:52 -05:00
sadnub
67f6a985f8 increase font size on script editors and fix import error 2021-11-16 21:16:03 -05:00
sadnub
f87d54ae8d move imports for styles and select light or dark theme for editor depending on if dark mode is enabled 2021-11-16 20:45:42 -05:00
sadnub
d894bf7271 move to ace text editor. Fixes script line wrap issue and more features. Fixes #712 2021-11-16 20:19:46 -05:00
sadnub
56e0e5cace formatting 2021-11-15 21:17:28 -05:00
sadnub
685084e784 add agent counts to client/site tooltip. Closes #426 2021-11-15 21:16:18 -05:00
sadnub
cbeec5a973 swagger api documentation start 2021-11-15 17:50:59 -05:00
sadnub
3fff56bcd7 cleanup script manager and snippet modals and move agent select dropdown for test script to script form 2021-11-15 17:50:26 -05:00
silversword411
c504c23eec docs add mesh token recovery 2021-11-15 16:47:18 -05:00
silversword411
16dae5a655 docs Updating index and adding permissions and considerations for choosing install type 2021-11-15 15:42:02 -05:00
silversword411
e512c5ae7d Merge branch 'wh1te909:develop' into develop 2021-11-15 15:39:56 -05:00
silversword411
094078b928 scripts wip adding disk status 2021-11-15 15:26:07 -05:00
wh1te909
34fc3ff919 fix issue where emails/sms were not being sent if recipients in global settings were empty, even if they were present in an alert template recipients 2021-11-15 00:05:42 +00:00
wh1te909
4391f48e78 add some tests 2021-11-14 19:52:21 +00:00
wh1te909
775608a3c0 update reqs 2021-11-14 19:51:28 +00:00
Dan
b326228901 Merge pull request #800 from silversword411/develop
script library - fixing choco
2021-11-14 11:27:40 -08:00
silversword411
b2e98173a8 script library - fixing choco 2021-11-14 13:04:37 -05:00
wh1te909
65c9b7952c have task runs appear in history tab closes #716 2021-11-14 09:18:32 +00:00
wh1te909
b9dc9e7d62 speed up some views 2021-11-14 09:15:43 +00:00
Dan
ce178d0354 Merge pull request #799 from silversword411/develop
Community scripts: Adding syntax for tooltip
2021-11-14 00:54:15 -08:00
sadnub
a3ff6efebc remove nats-api from api dev image 2021-11-13 16:56:50 -05:00
wh1te909
6a9bc56723 update for new service 2021-11-13 21:30:01 +00:00
wh1te909
c9ac158d25 Merge branch 'develop' of https://github.com/wh1te909/tacticalrmm into develop 2021-11-13 20:18:18 +00:00
silversword411
4b937a0fe8 Community scripts: Adding syntax for tooltip 2021-11-13 14:10:05 -05:00
sadnub
405bf26ac5 formatting 2021-11-13 13:40:26 -05:00
sadnub
5dcda0e0a0 allow q-select slots in tactical-dropdown. Fix info icon on run script dialog 2021-11-13 13:39:38 -05:00
sadnub
83e9b60308 when filtering agents add category to the side of options 2021-11-13 12:55:09 -05:00
sadnub
10b40b4730 script syntax highlighting. Resolves #702 2021-11-13 12:55:09 -05:00
wh1te909
79d6d804ef stringify errors before saving to db 2021-11-13 08:31:45 +00:00
wh1te909
e9c7b6d8f8 fix tests 2021-11-13 01:25:25 +00:00
wh1te909
4fcfbfb3f4 more go rework 2021-11-13 00:45:28 +00:00
wh1te909
30cde14ed3 update go mod 2021-11-13 00:36:57 +00:00
wh1te909
cf76e6f538 remove deprecated endpoint, add another deprecation 2021-11-13 00:33:52 +00:00
wh1te909
d0f600ec8d filter_software now handled by agent 2021-11-13 00:32:44 +00:00
wh1te909
675f9e956f remove some celery tasks now handled by agent/go 2021-11-13 00:32:03 +00:00
wh1te909
381605a6bb remove tests 2021-11-13 00:31:06 +00:00
wh1te909
0fce66062b remove some utils now handled by agent 2021-11-13 00:30:45 +00:00
wh1te909
747cc9e5da remove tasks 2021-11-13 00:27:34 +00:00
sadnub
25a1b464da Fix block inheritance on client/site 2021-11-10 22:45:25 -05:00
Dan
3b6738b547 Merge pull request #798 from silversword411/develop
Wip script additions
2021-11-10 11:12:28 -08:00
silversword411
fc93e3e97f Merge branch 'wh1te909:develop' into develop 2021-11-10 11:01:34 -05:00
silversword411
0edbb13d48 scripts wip revert windows update to default settings 2021-11-10 11:00:44 -05:00
silversword411
673687341c scripts wip adding 2021-11-10 09:03:17 -05:00
wh1te909
3969208942 Release 0.9.2 2021-11-09 06:11:51 +00:00
wh1te909
3fa89b58df bump version 2021-11-09 06:11:33 +00:00
wh1te909
a43a9c8543 remove old wording 2021-11-09 05:54:17 +00:00
sadnub
45deda4dea fix saving agents 2021-11-08 22:25:07 -05:00
sadnub
6ec46f02a9 fix deleting automation task #791 2021-11-08 21:17:41 -05:00
sadnub
d643c17ff1 fix remote background height and adjust other items to fit to screen 2021-11-08 21:13:01 -05:00
wh1te909
e5de89c6b4 more loading fixes 2021-11-09 01:23:01 +00:00
wh1te909
c21e7c632d fix debug log loading 2021-11-09 01:06:11 +00:00
wh1te909
6ae771682a fix task loading fixes #790 2021-11-08 22:00:40 +00:00
wh1te909
bf2075b902 pending actions loading fixes #789 2021-11-08 21:56:57 +00:00
wh1te909
62ec8c8f76 Release 0.9.1 2021-11-07 21:01:10 +00:00
wh1te909
b84d4a99b8 bump version 2021-11-07 21:00:25 +00:00
Dan
cce9dfe585 Merge pull request #788 from silversword411/develop
adding wip scripts
2021-11-07 12:34:43 -08:00
wh1te909
166be395b9 fix email/sms 2021-11-07 20:33:58 +00:00
wh1te909
fa3f5f8d68 fix alerts still polling on logout 2021-11-07 20:11:59 +00:00
sadnub
2926b68c32 change permission_classes under class view instead of decorator 2021-11-07 13:12:45 -05:00
sadnub
a55f187958 fit task control to screen 2021-11-07 13:00:53 -05:00
sadnub
c76d263375 stop loading when no audit log permissions 2021-11-07 12:37:36 -05:00
silversword411
6740d97f8f Merge branch 'wh1te909:develop' into develop 2021-11-07 10:02:16 -05:00
sadnub
b079eebe79 add restricted users to the client/site that they are adding to the system 2021-11-07 07:56:12 -05:00
sadnub
363e48a1e8 formatting 2021-11-07 07:34:14 -05:00
sadnub
f60e4e3e4f fix email test sending 2021-11-07 07:31:45 -05:00
sadnub
1b02974efa fix core settings showing when permissions should restrict it 2021-11-07 07:24:46 -05:00
wh1te909
496abdd230 fix sorting 2021-11-07 09:39:04 +00:00
wh1te909
bc495d77d1 fix django admin render error 2021-11-07 09:38:08 +00:00
wh1te909
fb54d4bb64 Release 0.9.0 2021-11-07 06:48:12 +00:00
wh1te909
0786163dc3 bump versions 2021-11-07 06:44:50 +00:00
wh1te909
ed85611e75 remove old setting 2021-11-07 06:41:35 +00:00
wh1te909
86ebfce44a fix prop name 2021-11-07 04:30:17 +00:00
wh1te909
dae51cff51 black 2021-11-07 04:17:08 +00:00
wh1te909
358a2e7220 fix dashboard refresh 2021-11-07 03:53:29 +00:00
wh1te909
d45353e8c8 fix agent update 2021-11-07 03:37:30 +00:00
wh1te909
2f56e4e3a1 bump mesh 2021-11-07 00:37:07 +00:00
wh1te909
0e503f8273 remove a debug log message 2021-11-07 00:34:16 +00:00
wh1te909
876fe803f5 try again 2021-11-07 00:27:16 +00:00
wh1te909
6adb9678b6 please work 2021-11-07 00:24:58 +00:00
wh1te909
39bf7ba4a9 fix coverage 2021-11-07 00:20:38 +00:00
wh1te909
5da6e2ff99 black 2021-11-07 00:02:27 +00:00
sadnub
44603c41a2 alerts permissions and fixes 2021-11-06 19:54:44 -04:00
wh1te909
0feb982a73 add migrations for new timezones 2021-11-06 23:54:19 +00:00
Dan
d93cb32f2e Merge pull request #770 from cocorocho/develop
formatting
2021-11-06 16:52:26 -07:00
Dan
40c47eace2 Merge branch 'develop' into develop 2021-11-06 16:50:19 -07:00
wh1te909
509bdd879c update reqs 2021-11-06 23:48:56 +00:00
Dan
b98ebb6e9f Merge pull request #755 from sadnub/develop
Per site/client permissions and other refactoring
2021-11-06 16:25:49 -07:00
Dan
924ddecff0 Merge pull request #780 from saulens22/patch-1
Remove Test-License script check
2021-11-06 16:16:35 -07:00
wh1te909
ca64fd218d fix loading 2021-11-06 23:12:09 +00:00
wh1te909
9b12b55acd fix title 2021-11-06 23:08:54 +00:00
wh1te909
450239564a add loading 2021-11-06 23:05:08 +00:00
Saulius Kazokas
bb1cc62d2a Remove Test-License script check
Leftover from previous commit
2021-11-07 01:02:38 +02:00
wh1te909
b4875c1e2d black 2021-11-06 22:45:14 +00:00
wh1te909
a21440d663 optimize query 2021-11-06 22:41:51 +00:00
wh1te909
eb6836b63c reduce api calls 2021-11-06 22:39:21 +00:00
wh1te909
b39a2690c1 fix refresh, change icon size 2021-11-06 22:33:19 +00:00
silversword411
706902da1c wip script - user logon auditing 2021-11-06 15:30:51 -04:00
silversword411
d5104b5d27 Add veeam wip scripts 2021-11-06 15:23:16 -04:00
wh1te909
a13ae5c4b1 update middleware urls 2021-11-06 08:25:59 +00:00
wh1te909
a92d1d9958 fix pending actions 2021-11-06 08:11:43 +00:00
wh1te909
10852a9427 stop loading after uninstall 2021-11-05 22:34:20 +00:00
wh1te909
b757ce1e38 fix url action for agents 2021-11-05 21:15:59 +00:00
sadnub
91e75f3fa2 Merge branch 'develop' of https://github.com/sadnub/tacticalrmm into develop 2021-11-05 16:01:06 -04:00
sadnub
6c8e55eb2f fix alert exclusions and policy exclusion modals 2021-11-05 16:01:00 -04:00
wh1te909
f821f700fa improve history tab show command output 2021-11-05 19:32:24 +00:00
wh1te909
d76d24408f fix double click agent params 2021-11-05 18:13:25 +00:00
wh1te909
7ad85dfe1c remove debug 2021-11-05 18:04:48 +00:00
wh1te909
7d8be0a719 fix url 2021-11-05 17:56:23 +00:00
sadnub
bac15c18e4 clean up unused vuex methods. Fix double-click action on agent table. Fix deleting clients and site reloading tree 2021-11-05 13:21:43 -04:00
sadnub
2f266d39e6 stop unnecessary DB calls for superusers on custom queryset 2021-11-05 12:28:03 -04:00
wh1te909
5726d1fc52 fix run task 2021-11-05 06:36:51 +00:00
wh1te909
69aee1823e fix ping check output 2021-11-05 06:23:46 +00:00
wh1te909
e6a0ae5f57 fix refresh 2021-11-05 05:34:29 +00:00
wh1te909
e5df566c7a typo 2021-11-05 05:14:15 +00:00
sadnub
81e173b609 migration to convert agent_id field onAuditLog to use agent_id isn't of agent pk 2021-11-04 22:08:53 -04:00
sadnub
d0ebcc6606 implement alerts permissions 2021-11-04 21:45:36 -04:00
sadnub
99c3fcf42a implement list scripts permissions 2021-11-04 21:17:55 -04:00
sadnub
794666e7cc supress 403 error messages on get and patch requests 2021-11-04 21:01:11 -04:00
sadnub
45abe4955d add list roles to accounts views 2021-11-04 21:01:11 -04:00
wh1te909
7eed421c70 fix my last commit 2021-11-04 22:33:49 +00:00
wh1te909
69f7c397c2 fix sms attempting to send when not fully configured 2021-11-04 22:26:05 +00:00
wh1te909
d2d136e922 fix missing function call 2021-11-04 21:36:33 +00:00
sadnub
396e435ae0 fix pip symbol is url for agent_id 2021-11-04 16:43:32 -04:00
sadnub
45d8e9102a convert mesh username to lowercase before creating token 2021-11-04 16:25:20 -04:00
sadnub
12a51deffa add permissions to custom fields in core settings 2021-11-04 16:25:20 -04:00
sadnub
f2f69abec2 increase celery worker init timeout 2021-11-04 16:25:20 -04:00
sadnub
02b7f962e9 move to inject/provide to refresh dashboard in nested components 2021-11-04 16:25:20 -04:00
sadnub
eb813e6b22 convert hr to q-separator 2021-11-04 16:25:20 -04:00
sadnub
5ddc604341 rework deployments ui, implement client/site permisssions, and tests 2021-11-04 16:25:20 -04:00
sadnub
313e672e93 add new roles to ui 2021-11-04 16:25:20 -04:00
sadnub
ce77ad6de4 fixed reboot later modal and allowed only certain pending actions to be deleted 2021-11-04 16:25:20 -04:00
sadnub
bea22690b1 improve remote background tabs 2021-11-04 16:25:20 -04:00
sadnub
c9a52bd7d0 fix tests broken by url changes 2021-11-04 16:25:20 -04:00
sadnub
a244a341ec rework agent recovery and permissions 2021-11-04 16:25:20 -04:00
sadnub
2b47870032 fix patch scan and install in agent table 2021-11-04 16:25:20 -04:00
sadnub
de9e35ae6a rework pending action permissions and write tests 2021-11-04 16:25:20 -04:00
sadnub
1a6fec8ca9 allow installing approved updated from winupdates tab 2021-11-04 16:25:20 -04:00
sadnub
094054cd99 ui fixes 2021-11-04 16:25:20 -04:00
sadnub
f85b8a81f1 fix alert checkboxes with alert template applied 2021-11-04 16:25:20 -04:00
sadnub
a44eaebf7c add migrations for new roles 2021-11-04 16:25:20 -04:00
sadnub
f37b3c063e fix agent edit 2021-11-04 16:25:20 -04:00
sadnub
6e5d5a3b82 fix some prop errors in addautomatedtask and fix display of assigned checks and tasks in policy check/task tabs 2021-11-04 16:25:20 -04:00
sadnub
bf0562d619 fix automation components and rework some views and urls 2021-11-04 16:25:20 -04:00
sadnub
ecaa81be3c rework alerts urls. added new permissions for alert templates. Make sure certain urls were protected. 2021-11-04 16:25:20 -04:00
sadnub
d98ae48935 rework patch reset policy modal to comp api and take into account permissions 2021-11-04 16:25:20 -04:00
sadnub
f52a76b16c update quasar pacakges 2021-11-04 16:25:20 -04:00
sadnub
d421c27602 fix remove agent and ping 2021-11-04 16:25:20 -04:00
sadnub
70e4cd4de1 rework reboot later modal to comp api and fixed reboot now and later 2021-11-04 16:25:20 -04:00
sadnub
29767e9265 fix bulk actions 2021-11-04 16:25:20 -04:00
sadnub
46d4c7f96d fix more update, version, bulk, and installer views and take permissions into account. Also fixed tests 2021-11-04 16:25:20 -04:00
sadnub
161a6f3923 clients and sites permissions tests and fixes 2021-11-04 16:25:20 -04:00
sadnub
53e912341b fix some agent table menu actions 2021-11-04 16:25:19 -04:00
sadnub
19396ea11a fix debuglog, auditlog, api auth tests 2021-11-04 16:25:19 -04:00
sadnub
1d9a5e742b fix alerts tests 2021-11-04 16:25:19 -04:00
sadnub
e8dfdd03f7 rework policy serializers that have clients/sites/agents return to filter by agent permissions 2021-11-04 16:25:19 -04:00
sadnub
2f5b15dac7 rework filter_by_role to allow chaining from other querysets 2021-11-04 16:25:19 -04:00
sadnub
525e1f5136 formatting 2021-11-04 16:25:19 -04:00
sadnub
7d63d188af fix client view 2021-11-04 16:25:19 -04:00
sadnub
87889c12ea refactor clients and sites to take into account new client/site permissions 2021-11-04 16:25:19 -04:00
sadnub
53d023f5ee fix clients tests 2021-11-04 16:25:19 -04:00
sadnub
1877ab8c67 rework client manager and modals to composition api. Improved client/site delete 2021-11-04 16:25:19 -04:00
sadnub
72a5a8cab7 fix client tree not loading correct agents 2021-11-04 16:25:19 -04:00
sadnub
221e49a978 mmodify clients urls, rework perms, and fix views 2021-11-04 16:25:19 -04:00
sadnub
1a4c67d173 fix run script modal 2021-11-04 16:25:19 -04:00
sadnub
42fd23ece3 fix audit manager tab and modal 2021-11-04 16:25:19 -04:00
sadnub
3035c0712a fix debug log and change agent dropdown to use agent_id versus pk 2021-11-04 16:25:19 -04:00
sadnub
61315f8bfd add tests for autotasks permissions 2021-11-04 16:25:19 -04:00
sadnub
52683124d8 rework task tab 2021-11-04 16:25:19 -04:00
sadnub
1f77390366 agent per site/client permissions initial, uri updates, comp api rework 2021-11-04 16:25:19 -04:00
sadnub
322d492540 change dashboard hostname default in env.example to be consistent with docs. Log nginx container access/error logs to stdout/stderr 2021-11-04 16:25:19 -04:00
Dan
f977d8cca9 Merge pull request #774 from silversword411/develop
Scripts library update
2021-11-02 12:38:06 -07:00
silversword411
a9aedea2bd docs Linking to Traefik howto 2021-11-02 11:28:24 -04:00
silversword411
5560bbeecb scripts - Defender status adding full details 2021-10-29 06:48:41 -04:00
cocorocho
f226206703 change API key creation function 2021-10-25 23:03:53 +03:00
cocorocho
170687226d formatting 2021-10-25 22:41:03 +03:00
Dan
d56d3dc271 Merge pull request #764 from silversword411/develop
Inverting exit codes
2021-10-18 13:14:35 -07:00
silversword411
32a202aff4 Inverting exit codes 2021-10-18 16:01:29 -04:00
Dan
6ee75e6e60 Merge pull request #762 from silversword411/develop
Docs and scripts updates
2021-10-18 09:26:36 -07:00
Dan
13d74cae3b Merge pull request #757 from mattthhdp/develop
Add Traefikv2 reverse proxy
2021-10-18 09:26:17 -07:00
silversword411
88651916b0 docs fix tipsntricks pic order 2021-10-14 11:07:05 -04:00
silversword411
be12505d2f docs fix tipsntricks pic order 2021-10-14 11:01:00 -04:00
Jaune
23fcf3b045 add RMM.toml
forgot to add i file
2021-10-12 20:31:11 -04:00
Jaune
9e7459b204 Add Traefikv2 reverse proxy
Should give a good way to use 
traefikv2 with 1 minor modification
 to the install script (changing the port)
with 2fa support on the mesh side
2021-10-12 17:08:38 -04:00
silversword411
4f0eb1d566 docs fix formatting 2021-10-12 02:17:47 -04:00
silversword411
ce00481f47 docs fix typo and domain name reference consistency 2021-10-12 02:02:42 -04:00
silversword411
f596af90ba wip fixing downloadurl 2021-10-11 11:10:56 -04:00
wh1te909
5c74d1d021 Release 0.8.5 2021-10-10 22:24:30 +00:00
wh1te909
aff659b6b6 bump versions 2021-10-10 22:21:23 +00:00
wh1te909
58724d95fa no debian 11 for now 2021-10-10 21:21:51 +00:00
wh1te909
8d61fcd5c9 handle invalid tokens 2021-10-10 21:08:43 +00:00
wh1te909
3e1be53c36 update reqs 2021-10-08 06:18:15 +00:00
Dan
f3754588bd Merge pull request #743 from bc24fl/develop
Fixed sorting under Folder View in Script Manager
2021-10-07 22:42:33 -07:00
Dan
c4ffffeec8 Merge pull request #750 from silversword411/develop
docs and scripts updates
2021-10-07 22:35:24 -07:00
silversword411
5b69f6a358 scripts_wip adding dell command 2021-10-08 00:50:19 -04:00
silversword411
1af89a7447 Merge pull request #1 from silversword411/develop-wip
Develop wip
2021-10-08 00:47:37 -04:00
silversword411
90abd81035 docs Adding way to restrict admin access thru nginx 2021-10-08 00:45:20 -04:00
Dan
898824b13f Merge pull request #733 from isaacg123/patch-1
Minor cleanup tweaks
2021-10-07 21:29:56 -07:00
Dan
9d093aa7f8 Merge pull request #740 from bbrendon/patch-2
Update Win_Defender_QuickScan_Background.ps1
2021-10-07 21:28:26 -07:00
Irving
1770549f6c Fixed sorting under Folder View in Script Manager 2021-10-07 01:23:44 -04:00
silversword411
d21be77fd2 scripts_wip TRMM Agent Install script 2021-10-06 09:09:49 -04:00
silversword411
41a1c19877 docs adding cmd deploy script 2021-10-06 09:08:44 -04:00
bbrendon
9b6571ce68 Update Win_Defender_QuickScan_Background.ps1
I did some tests and don't see value in specifying a drive letter. `QuickScanAge` resets to 0 even without a drive letter.

Having a drive letter seems to also assume something that may not be true.
2021-10-04 12:29:41 -07:00
silversword411
88e98e4e35 docs adding HP link 2021-10-01 12:20:50 -04:00
silversword411
10c56ffbfa Fixing lenovo support link 2021-10-01 07:57:29 -04:00
silversword411
cb2c8d6f3c working on support questions 2021-10-01 07:22:08 -04:00
silversword411
ca62b850ce wip scripts adding 2021-10-01 07:17:04 -04:00
silversword411
5a75d4e140 docs troubleshooting tweaks 2021-10-01 07:15:45 -04:00
Dan
e0972b7c24 Merge pull request #737 from silversword411/develop
wip and docs updates
2021-09-30 12:17:46 -07:00
silversword411
0db497916d docs install tweaks 2021-09-30 00:32:53 -04:00
silversword411
23a0ad3c4e docs updating docker with LE info and improving headers 2021-09-29 22:41:11 -04:00
silversword411
2b4e1c4b67 docker add 2021-09-29 14:47:47 -04:00
silversword411
9b1b9244cf full chain cert docs 2021-09-29 13:56:33 -04:00
silversword411
ad570e9b16 wip and howitallworks tweaks 2021-09-28 14:30:34 -04:00
wh1te909
812ba6de62 add security policy closes #725 2021-09-27 05:55:51 +00:00
silversword411
8f97124adb docs troubleshooting tweak 2021-09-26 13:55:09 -04:00
wh1te909
28289838f9 change docs for uploading mesh agent 2021-09-25 23:47:15 +00:00
Dan
cca8a010c3 Merge pull request #736 from silversword411/develop
WIP works under the wire
2021-09-25 16:45:42 -07:00
wh1te909
91ab296692 update reqs 2021-09-25 23:23:03 +00:00
silversword411
ee6c9c4272 WIP works 2021-09-25 18:56:43 -04:00
isaacg123
21cd36fa92 Minor cleanup tweaks
https://evotec.xyz/the-curious-case-of-null-should-be-on-the-left-side-of-equality-comparisons-psscriptanalyzer/
2021-09-25 04:30:54 -05:00
sadnub
b1aafe3dbc Fix block_policy_inheritance not saving correctly when set in UI 2021-09-24 17:43:08 -04:00
Dan
5cd832de89 Merge pull request #711 from silversword411/develop
Docs tweaks
2021-09-24 13:48:28 -07:00
silversword411
24dd9d0518 WIP scripts adds and tweaks 2021-09-24 16:36:45 -04:00
sadnub
aab6ab810a formatting 2021-09-24 12:16:13 -04:00
sadnub
d1d6d5e71e fix sms message sending 2021-09-24 12:12:49 -04:00
sadnub
e67dd68522 formatting 2021-09-23 13:50:32 -04:00
sadnub
e25eae846d make two-factor prompt dialog persistent 2021-09-23 13:23:52 -04:00
sadnub
995eeaa455 remove console.log entries 2021-09-23 13:23:52 -04:00
sadnub
240c61b967 fix some audit log entries not typing to agent correctly 2021-09-23 13:23:52 -04:00
sadnub
2d8b0753b4 move upload mesh agent modal to core settings and rewrite to comp api 2021-09-23 13:23:52 -04:00
sadnub
44eab3de7f docker dev changes and nginx dev fix 2021-09-23 13:23:52 -04:00
sadnub
007be5bf95 Stop DatabaseError exception when update_fields hasn't been changed 2021-09-23 12:56:27 -04:00
silversword411
ee19c7c51f docs adding tidbits 2021-09-23 11:05:51 -04:00
sadnub
ce56afbdf9 Fix serialization error in view
Also moved policy processing to celery task
2021-09-23 11:02:45 -04:00
silversword411
51012695a1 docs adding mesh agent to trmm integration 2021-09-23 10:28:09 -04:00
silversword411
0eef2d2cc5 scripts fixing mt duplicati install script 2021-09-23 03:14:51 -04:00
silversword411
487f9f2815 docs adding install on synology docker. Thx ildrad 2021-09-21 12:48:49 -04:00
silversword411
d065adcd8e Adding contributing using browser to docs 2021-09-21 12:18:44 -04:00
silversword411
0d9a1dc5eb json args fix 2021-09-21 11:09:50 -04:00
silversword411
8f9ad15108 Preliminary release of script library parameters 2021-09-20 16:56:42 -04:00
silversword411
e538e9b843 requirements tweak 2021-09-20 12:45:30 -04:00
Dan
4a702b6813 Merge pull request #718 from Yamacore/develop
Fix nginx crash after netinstall
2021-09-17 19:24:24 -07:00
silversword411
1e6fd2c57a Docs updating automation policies 2021-09-17 11:46:25 -04:00
silversword411
600b959d89 Tweaking community scripts 2021-09-17 11:10:29 -04:00
Yamacore
b96de9eb13 Fix nginx crash after netinstall
debian 10 netinstall got "server_names_hash_bucket_size" option commented out this fixes it
2021-09-16 20:09:46 +02:00
silversword411
93be19b647 docs Adding network diagram from NiceGuyIT - NC 2021-09-15 13:53:39 -04:00
silversword411
74f45f6f1d docs faq tweaks 2021-09-13 18:33:23 -04:00
silversword411
54ba3d2888 docs tweaks 2021-09-13 16:25:55 -04:00
silversword411
65d5149f60 docs - More tweaks 2021-09-13 12:03:47 -04:00
silversword411
917ebb3771 docs - install tweaks 2021-09-13 11:47:24 -04:00
silversword411
7e66b1f545 refactoring server install doc, and other tweaks 2021-09-13 10:08:39 -04:00
Dan
05837dca35 Merge pull request #706 from subzdev/develop
add password plain text toggle
2021-09-12 14:56:02 -07:00
Dan
53be2ebe59 Merge pull request #698 from silversword411/develop
server troubleshooting script, docs cleanup, script library updates
2021-09-12 14:52:26 -07:00
silversword411
0341efcaea subzdev doc revamp 2021-09-11 12:08:34 -04:00
Bob
ec75210fd3 add password plain text toggle 2021-09-11 03:10:00 +00:00
silversword411
e6afe3e806 scripts Archiving old broken installer 2021-09-10 17:03:23 -04:00
silversword411
5aa46f068e scripts - duplicati updates 2021-09-10 16:58:29 -04:00
silversword411
a11a5b28bc cleanup docs 2021-09-10 15:23:12 -04:00
silversword411
907aa566ca docs again 2021-09-10 15:18:41 -04:00
silversword411
5c21f099a8 docs tweaks 2021-09-10 14:56:52 -04:00
silversword411
b91201ae3e docs Fix png 2021-09-10 14:05:22 -04:00
silversword411
56d7e19968 docs cleanup, adding new dev docs with subzdev 2021-09-10 13:46:13 -04:00
silversword411
cf91c6c90e troubleshooter adding waits 2021-09-10 07:31:50 -04:00
wh1te909
9011148adf Release 0.8.4 2021-09-09 19:14:11 +00:00
wh1te909
897d0590d2 bump version 2021-09-09 19:10:28 +00:00
wh1te909
33b33e8458 retry websocket on 1006 error 2021-09-09 19:07:00 +00:00
wh1te909
7758f5c187 add a file to ignore 2021-09-09 18:47:28 +00:00
silversword411
83d7a03ba4 adding cert checks 2021-09-09 12:56:16 -04:00
wh1te909
a9a0df9699 fix tests 2021-09-09 16:26:06 +00:00
silversword411
df44f8f5f8 Adding server troubleshooting script 2021-09-09 11:43:12 -04:00
wh1te909
216a9ed035 speed up some views 2021-09-09 06:50:30 +00:00
wh1te909
35d61b6a6c add missing trailing slashes fixes #43 2021-09-09 05:55:27 +00:00
wh1te909
5fb72cea53 add types to url 2021-09-09 05:54:34 +00:00
Dan
d54d021e9f Merge pull request #697 from silversword411/develop
Tweaks
2021-09-08 18:17:42 -07:00
silversword411
06e78311df Tweaks 2021-09-08 21:04:35 -04:00
Dan
df720f95ca Merge pull request #696 from silversword411/develop
Unsupported Officially...no we really mean it
2021-09-08 17:06:48 -07:00
Dan
00faff34d3 Merge pull request #695 from aaronstuder/patch-1
Update install_server.md
2021-09-08 17:06:35 -07:00
silversword411
2b5b3ea4f3 Unsupported Officially...no we really mean it 2021-09-08 18:38:40 -04:00
sadnub
95e608d0b4 fix agent saying that it was approving updates when it actually didn't 2021-09-08 17:37:02 -04:00
sadnub
1d55bf87dd fix audit and debug log not refreshing on agent change 2021-09-08 17:36:30 -04:00
aaronstuder
1220ce53eb Update install_server.md 2021-09-08 12:55:53 -04:00
sadnub
2006218f87 honor block_dashboard_login from the login 2fa verification view 2021-09-08 10:29:58 -04:00
sadnub
40f427a387 add trailing slash to missing urls. Potentially fixes #43 2021-09-08 10:28:54 -04:00
sadnub
445e95baed formatting 2021-09-08 10:27:42 -04:00
sadnub
67fbc9ad33 make installer user use the new block_dasboard_login property 2021-09-06 22:42:32 -04:00
sadnub
1253e9e465 formatting 2021-09-06 20:10:14 -04:00
sadnub
21069432e8 fix tests 2021-09-06 20:06:23 -04:00
sadnub
6facf6a324 fix nginx on docker dev 2021-09-06 12:54:37 -04:00
sadnub
7556197485 move policy processing on any agent changes to celery task 2021-09-06 11:47:07 -04:00
wh1te909
8dddd2d896 Release 0.8.3 2021-09-06 09:30:51 +00:00
wh1te909
f319c95c2b bump version 2021-09-06 09:10:00 +00:00
wh1te909
8e972b0907 add docs for api keys 2021-09-06 08:50:18 +00:00
sadnub
395e400215 fix docker build script 2021-09-05 23:52:33 -04:00
sadnub
3685e3111f fix docker prod spinup. Move api container to uwsgi 2021-09-05 23:49:10 -04:00
sadnub
7bb1c75dc6 add auditing to objects URLAction, KeyStore, CustomFields and also audit when url actions are run 2021-09-05 12:32:37 -04:00
sadnub
b20834929c formatting 2021-09-05 11:35:15 -04:00
sadnub
181891757e fix tasks with assigned checks being added to automation policy 2021-09-05 11:22:21 -04:00
wh1te909
b16feeae44 fix debug log 2021-09-05 08:45:41 +00:00
wh1te909
684e049f27 typo 2021-09-05 06:07:46 +00:00
wh1te909
8cebd901b2 update reqs 2021-09-05 01:40:25 +00:00
wh1te909
3c96beb8fb fix celery memory leak 2021-09-04 23:40:57 +00:00
Dan
8a46459cf9 Merge pull request #683 from silversword411/develop
wip script additions and docs updates
2021-09-04 15:46:31 -07:00
Dan
be5c3e9daa Merge pull request #673 from juaromu/docs-securing-nginx
Securing NGINX added to docs
2021-09-04 15:45:37 -07:00
wh1te909
e44453877c skip sw errors fixes #682 2021-09-04 22:23:35 +00:00
wh1te909
f772a4ec56 allow users to reset their own password/2fa fixes #686 2021-09-04 22:15:51 +00:00
wh1te909
44182ec683 fix render error if results are null 2021-09-03 06:29:27 +00:00
wh1te909
b9ab13fa53 hide status field under properly implemented 2021-09-03 06:28:27 +00:00
wh1te909
2ad6721c95 fix pipeline 2021-09-03 05:45:31 +00:00
wh1te909
b7d0604e62 first/last name optional 2021-09-03 05:35:54 +00:00
wh1te909
a7518b4b26 black 2021-09-03 05:34:44 +00:00
wh1te909
50613f5d3e add api auth in settings, removed from local_settings 2021-09-03 05:31:44 +00:00
sadnub
f814767703 add tests and some ui fixes 2021-09-02 23:52:26 -04:00
sadnub
4af86d6456 set alert template on new agents 2021-09-02 21:36:35 -04:00
sadnub
f0a4f00c2d fix properties and block user dashboard access if denied 2021-09-02 21:32:18 -04:00
sadnub
4321affddb allow for creating special tokens for api access and bypassing two factor auth 2021-09-02 21:10:23 -04:00
silversword411
926ed55b9b docs update - Authorized users 2021-09-02 11:28:05 -04:00
silversword411
2ebf308565 Merge branch 'develop' of https://github.com/silversword411/tacticalrmm into develop 2021-09-02 10:33:36 -04:00
silversword411
1c5e736dce wip script network scanner 2021-09-02 10:33:25 -04:00
silversword411
b591f9f5b7 MOAR wips 2021-09-02 08:39:03 -04:00
silversword411
9724882578 wip script for print check 2021-09-02 08:23:05 -04:00
silversword411
ddef2df101 Merge branch 'develop' of https://github.com/silversword411/tacticalrmm into develop 2021-09-02 08:11:21 -04:00
silversword411
8af69c4284 adding alternate ssl to unsupported docs 2021-09-02 07:55:33 -04:00
silversword411
6ebe1ab467 adding alternate ssl to unsupported docs 2021-09-02 07:39:44 -04:00
silversword411
24e4d9cf6d docs Making docker howto visible 2021-09-02 05:21:51 -04:00
silversword411
f35fa0aa58 Troubleshooting docs update 2021-09-01 18:50:18 -04:00
wh1te909
4942f262f1 Release 0.8.2 2021-09-01 07:18:21 +00:00
wh1te909
a20b1a973e bump version 2021-09-01 07:18:09 +00:00
wh1te909
eae5e00706 allow filtering by overdue #674 2021-09-01 06:26:55 +00:00
silversword411
403762d862 wip script additions 2021-08-31 22:45:53 -04:00
sadnub
5c92d4b454 fix bug were script args weren't being substituted when testing scripts 2021-08-31 20:33:36 -04:00
wh1te909
38179b9d38 Release 0.8.1 2021-08-31 06:51:20 +00:00
wh1te909
8f510dde5a bump versions 2021-08-31 06:35:29 +00:00
wh1te909
be42d56e37 fix 500 error when trying to test newly added script 2021-08-31 06:16:40 +00:00
Juan J. Romero
6294530fa3 Securing NGINX added to docs 2021-08-31 15:45:47 +10:00
sadnub
c5c8f5fab1 formatting 2021-08-30 22:32:16 -04:00
sadnub
3d41d79078 change directory for nats configuration file for DOCKER. Fix nats-api commands in dev containers 2021-08-30 22:17:21 -04:00
sadnub
3005061a11 formatting 2021-08-30 08:06:15 -04:00
sadnub
65ea46f457 strip whitespace before processing collector output 2021-08-30 07:42:54 -04:00
wh1te909
eca8f32570 Release 0.8.0 2021-08-30 06:32:39 +00:00
wh1te909
8d1ef19c61 bump version 2021-08-30 06:28:40 +00:00
wh1te909
71d87d866b change schedule 2021-08-30 05:49:09 +00:00
wh1te909
c4f88bdce7 update for new debug log 2021-08-30 03:45:35 +00:00
sadnub
f722a115b1 update alerting docs and add database maintenance page 2021-08-29 16:54:05 -04:00
sadnub
1583beea7b update script docs 2021-08-29 16:25:33 -04:00
wh1te909
5b388c587b update python 2021-08-29 08:19:35 +00:00
wh1te909
e254923167 update mesh/nats 2021-08-29 08:13:04 +00:00
wh1te909
b0dbdd7803 fix field 2021-08-29 07:16:09 +00:00
wh1te909
aa6ebe0122 fix pagination 2021-08-29 03:40:14 +00:00
wh1te909
c5f179bab8 update nats-api 2021-08-29 03:39:58 +00:00
sadnub
e65cb86638 rework script testing a bit. Fix mismatch object properties and props 2021-08-28 10:33:18 -04:00
wh1te909
a349998640 add watcher 2021-08-28 06:48:00 +00:00
wh1te909
43f60610b8 fix props 2021-08-28 06:36:03 +00:00
wh1te909
46d042087a fix row name 2021-08-28 06:32:50 +00:00
sadnub
ee214727f6 format agent history table 2021-08-28 01:00:17 -04:00
sadnub
b4c1ec55ec fix env.example 2021-08-27 22:12:25 -04:00
Dan
0fdd54f710 Merge pull request #664 from bc24fl/change-community-script-add-domain-rename-capability-and-refactor
Change Win_Rename_Computer.ps1 community script to add domain joined …
2021-08-25 15:21:55 -07:00
wh1te909
4f0cdeaec0 reduce nats max payload as it will be enforced in future nats update 2021-08-25 21:14:35 +00:00
wh1te909
e5cc38857c update quasar conf to support quasar app 3.1.0 (webpack-dev-server 4.0.0) 2021-08-25 21:13:05 +00:00
wh1te909
fe4b9d71c0 update reqs 2021-08-25 21:11:39 +00:00
wh1te909
5c1181e40e Merge branch 'develop' of https://github.com/wh1te909/tacticalrmm into develop 2021-08-23 04:25:14 +00:00
wh1te909
8b71832bc2 update reqs 2021-08-23 04:19:21 +00:00
Irving
8412ed6065 Change Win_Rename_Computer.ps1 community script to add domain joined computer rename functionality and refactor per standards. 2021-08-22 16:36:19 -04:00
Dan
207f6cdc7c Merge pull request #661 from bc24fl/fix-doc-typo-in-alert-page
Fixed typo in documentation alert page
2021-08-21 23:44:09 -07:00
Dan
b0b51f5730 Merge pull request #660 from silversword411/develop
Script library - uninstall software
2021-08-21 23:38:46 -07:00
wh1te909
def6833ef0 new pipeline agent 2021-08-21 15:26:25 +00:00
wh1te909
c528dd3de1 attempt to fix pipelines 2021-08-20 08:23:16 +00:00
wh1te909
544270e35d new pipeline agent 2021-08-20 07:35:02 +00:00
bc24fl
657e029fee Fixed typo in documentation alert page 2021-08-19 15:48:46 -04:00
silversword411
49469d7689 docs update - adding to docker instructions 2021-08-18 22:59:28 -04:00
silversword411
4f0dd452c8 docs troubleshooting tweaks 2021-08-18 22:39:19 -04:00
silversword411
3f741eab11 Script library - uninstall software 2021-08-18 12:01:41 -04:00
Dan
190368788f Merge pull request #654 from NiceGuyIT/develop
Bitdefender install script:  Improve error detection and logging
2021-08-11 23:54:14 -07:00
Dan
8306a3f566 Merge pull request #649 from silversword411/develop
Docs and scripts updates
2021-08-11 23:53:24 -07:00
silversword411
988c134c09 choco typo fixes 2021-08-03 00:24:14 -04:00
silversword411
af0a4d578b Community Script Replacing Choco upgrade script 2021-08-03 00:06:38 -04:00
sadnub
9bc0abc831 fix favorited community scripts showing up if community scripts are hidden. Fix delete script in Script Manager 2021-08-02 17:48:13 -04:00
David Randall
41410e99e7 Improve error detection and logging 2021-08-02 12:43:39 -04:00
David Randall
deae04d5ff Merge branch 'wh1te909:develop' into develop 2021-08-02 12:37:49 -04:00
David Randall
7d6eeffd66 Improve error detection and logging 2021-08-02 12:33:32 -04:00
sadnub
629858e095 log django 500 errors (for easier debugging) to new log file 2021-08-02 09:35:41 -04:00
sadnub
dfdb628347 change favorited script run on agent to opent he Run Script modal with the script and defaults populated 2021-08-02 09:34:17 -04:00
sadnub
6e48b28fc9 fix filterable dropdown and prepopulating select value 2021-08-02 09:33:24 -04:00
sadnub
3ba450e837 fix replace values function 2021-08-02 09:21:07 -04:00
sadnub
688ed93500 allow url actions to be run against clients and sites 2021-08-01 00:17:48 -04:00
sadnub
7268ba20a2 Finished script snippet feature 2021-07-31 15:22:31 -04:00
sadnub
63d9e73098 fix tests 2021-07-31 13:29:51 -04:00
sadnub
564c048f90 add missing migration 2021-07-31 13:07:48 -04:00
sadnub
5f801c74d5 allowed dismissing persistent modals on Esc press. allow filtering on certain scripts and agent dropdowns. moved other dropdowns to tactical dropdown. Fixes with bulk actions 2021-07-31 11:56:47 -04:00
sadnub
b405fbc09a handle a few more errors when auth token is expired 2021-07-31 11:54:28 -04:00
sadnub
7a64c2eb49 update quasar 2021-07-31 11:54:00 -04:00
sadnub
c93cbac3b1 rework bulk action modal. start running bulk actions on next agent checkin 2021-07-30 12:48:47 -04:00
sadnub
8b0f67b8a6 actually stop the unauthorized console errors wwith websocket connection 2021-07-30 12:46:15 -04:00
sadnub
0d96129f2d get dropdown filtering working on custom tactical dropdown component 2021-07-30 12:45:26 -04:00
sadnub
54ee12d2b3 rework script manager and modals to composition api. Start on script snippets 2021-07-29 19:41:32 -04:00
silversword411
92fc042103 Win 10 upgrade script removing license check 2021-07-29 00:50:17 -04:00
silversword411
9bb7016fa7 Win 10 upgrade script commenting 2021-07-28 16:45:12 -04:00
silversword411
3ad56feafb Merge branch 'develop' of https://github.com/silversword411/tacticalrmm into develop 2021-07-27 19:04:58 -04:00
silversword411
14d59c3dec Sorting alphabetical and fixing pic 2021-07-27 19:04:40 -04:00
silversword411
443f419770 wip script add 2021-07-27 19:04:40 -04:00
silversword411
ddbb58755e Docs updates 2021-07-27 19:04:39 -04:00
silversword411
524283b9ff adding db maintenance to docs 2021-07-27 19:04:39 -04:00
silversword411
fb178d2944 add wip script 2021-07-27 19:04:39 -04:00
silversword411
52f4ad9403 add library wifi password retrieval script 2021-07-27 19:04:38 -04:00
silversword411
ba0c08ef1f Merge branch 'develop' of https://github.com/silversword411/tacticalrmm into develop 2021-07-27 19:03:36 -04:00
silversword411
9e19b1e04c wip script add 2021-07-27 19:02:22 -04:00
silversword411
b2118201b1 Sorting alphabetical and fixing pic 2021-07-25 15:27:05 -04:00
sadnub
b4346aa056 formatting 2021-07-21 20:41:11 -04:00
sadnub
b599f05aab fix version 2021-07-21 20:35:57 -04:00
sadnub
93d78a0200 add ipware req 2021-07-21 20:33:42 -04:00
silversword411
449957b2eb Docs updates 2021-07-21 15:02:56 -04:00
sadnub
0a6d44bad3 Fixes #561 2021-07-21 14:48:59 -04:00
sadnub
17ceaaa503 allow skipping alert resolved/failure actions on types of alerts 2021-07-21 14:30:25 -04:00
sadnub
d70803b416 add audit log retention 2021-07-21 13:49:34 -04:00
sadnub
aa414d4702 fix auditing on models that override the save method. Added Alert Template anmd Role to auditable models 2021-07-21 13:33:15 -04:00
sadnub
f24e1b91ea stop ws from reconnecting on unauthorized error 2021-07-21 10:53:55 -04:00
sadnub
1df8163090 add role and alert template to audit logging 2021-07-21 00:28:51 -04:00
sadnub
659ddf6a45 fix docker build script 2021-07-20 23:11:15 -04:00
sadnub
e110068da4 add public IP logging to audit log and agent login tables 2021-07-20 23:10:51 -04:00
sadnub
c943f6f936 stop the ws connection from retrying when logging out or session is expired 2021-07-20 16:46:16 -04:00
silversword411
cb1fe7fe54 adding db maintenance to docs 2021-07-19 10:44:38 -04:00
silversword411
593f1f63cc add wip script 2021-07-19 10:35:54 -04:00
silversword411
66aa70cf75 Merge branch 'develop' of https://github.com/silversword411/tacticalrmm into develop 2021-07-18 21:08:18 -04:00
silversword411
304be99067 add library wifi password retrieval script 2021-07-18 21:08:04 -04:00
silversword411
9a01ec35f4 add library wifi password retrieval script 2021-07-18 21:04:16 -04:00
sadnub
bfa5b4fba5 allow persistent mesh config and fix mongodb password uri issue 2021-07-17 15:57:35 -04:00
Dan
d2f63ef353 Merge pull request #641 from silversword411/develop
Docs and scripts additions
2021-07-17 10:57:07 -07:00
Dan
50f334425e Merge pull request #640 from bern-spl/patch-1
Update README.md
2021-07-17 10:56:33 -07:00
silversword411
f78212073c fix json 2021-07-17 11:21:39 -04:00
silversword411
5c655f5a82 Adding grafana to docs 2021-07-17 11:06:02 -04:00
silversword411
6a6446bfcb Adding configuring email to docs 2021-07-17 10:48:22 -04:00
silversword411
b60a3a5e50 Adding scripts 2021-07-17 10:33:31 -04:00
Bernard Blundell
02ccbab8e5 Update README.md 2021-07-17 14:51:09 +01:00
wh1te909
023ff3f964 update bin [skip ci] 2021-07-17 07:16:38 +00:00
wh1te909
7c5e8df3b8 fix tests 2021-07-17 07:11:29 +00:00
wh1te909
56fdab260b add/refactor task 2021-07-17 06:59:21 +00:00
wh1te909
7cce49dc1a deprecate an endpoint 2021-07-17 06:40:45 +00:00
wh1te909
2dfaafb20b fix bug where sms attempting to be sent when not configured 2021-07-17 06:35:31 +00:00
wh1te909
6138a5bf54 move some funcs to go 2021-07-17 05:13:40 +00:00
wh1te909
828c67cc00 fix tests 2021-07-17 00:33:21 +00:00
wh1te909
e70cd44e18 add history to send command 2021-07-16 21:45:16 +00:00
wh1te909
efa5ac5edd more run script rework 2021-07-16 06:11:40 +00:00
wh1te909
788b11e759 add fields to agent history 2021-07-14 07:38:31 +00:00
wh1te909
d049d7a61f update reqs 2021-07-14 07:36:55 +00:00
Dan
075c833b58 Merge pull request #626 from sadnub/runscript-rework
Agent Tabs/Run Script WIP
2021-07-13 11:43:38 -07:00
Dan
e9309c2a96 Merge pull request #638 from silversword411/develop
Docs and scripts update
2021-07-12 22:24:12 -07:00
silversword411
a592d2b397 Adding scripts to library and WIP 2021-07-13 00:21:43 -04:00
silversword411
3ad1805ac0 tweak faq 2021-07-12 23:51:16 -04:00
Dan
dbc2bab698 Merge pull request #632 from silversword411/develop
script library and docs updates
2021-07-12 08:51:13 -07:00
silversword411
79eec5c299 Bitdefender GravityZone Docs 2021-07-11 14:10:10 -04:00
silversword411
7754b0c575 howitallworks tweaks 2021-07-11 13:55:37 -04:00
silversword411
be4289ce76 Docs update 2021-07-11 13:26:15 -04:00
silversword411
67f5226270 add BitDefender Gravity Zone Install script 2021-07-10 12:42:27 -04:00
sadnub
b6d77c581b fix styling 2021-07-09 21:13:35 -04:00
sadnub
d84bf47d04 added script cloning functionality 2021-07-09 18:47:28 -04:00
sadnub
aba3a7bb9e fix and add tests 2021-07-09 18:00:28 -04:00
sadnub
6281736d89 implement test script in script edit 2021-07-09 08:03:53 -04:00
sadnub
94d96f89d3 implement run script save to custom field and agent notes 2021-07-09 00:16:15 -04:00
sadnub
4b55f9dead add tests and minor fixes 2021-07-08 22:02:02 -04:00
sadnub
5c6dce94df fix broken tests 2021-07-08 13:02:50 -04:00
wh1te909
f7d8f9c7f5 fix mkdocs warning 2021-07-08 06:39:32 +00:00
Dan
053df24f9c Merge pull request #627 from silversword411/develop
docs update and script tweak
2021-07-07 23:33:00 -07:00
silversword411
1dc470e434 powershell upgrade 2021-07-07 22:17:11 -04:00
silversword411
cfd8773267 wip script add 2021-07-07 22:13:17 -04:00
silversword411
67045cf6c1 docs tweaks 2021-07-07 22:00:52 -04:00
sadnub
ddfb9e7239 run script rework start 2021-07-07 19:28:52 -04:00
sadnub
9f6eed5472 setup pruning tasks 2021-07-07 19:28:52 -04:00
sadnub
15a1e2ebcb add agent history 2021-07-07 19:28:52 -04:00
sadnub
fcfe450b07 finish debug and audit rework 2021-07-07 19:28:52 -04:00
sadnub
a69bbb3bc9 audit manager rework wip 2021-07-07 19:28:52 -04:00
sadnub
6d2559cfc1 debug log rework 2021-07-07 19:28:52 -04:00
sadnub
b3a62615f3 moved debug log to database. modified frontend to composition api. moved a few mixins. 2021-07-07 19:28:52 -04:00
sadnub
57f5cca1cb debug modal rework into comp api 2021-07-07 19:28:52 -04:00
sadnub
6b9851f540 new agent tabs wip 2021-07-07 19:28:52 -04:00
silversword411
36fd203a88 Updating which registry tree to query 2021-07-07 16:00:48 -04:00
Dan
3f5cb5d61c Merge pull request #623 from meuchels/develop
Fix SC collector script to work with windows 7
2021-07-07 00:43:56 -07:00
Samuel Meuchel
862fc6a946 add newline to end 2021-07-06 19:45:03 -05:00
Samuel Meuchel
92c386ac0e Fixed ScreenConnect Collector script for ps 2.0 2021-07-06 19:41:16 -05:00
Samuel Meuchel
98a11a3645 add this exclusion for your ScreenConnect Deployment script to work. 2021-07-06 11:25:17 -05:00
Dan
62be0ed936 Merge pull request #610 from meuchels/develop
Add TeamViewer Script and Integration Docs
2021-07-01 14:45:45 -07:00
Samuel Meuchel
b7de73fd8a removed args from script json. 2021-07-01 08:19:12 -05:00
Samuel Meuchel
e2413f1af2 Add AnyDesk script collector and Integration Docs. 2021-06-30 17:33:48 -05:00
Samuel Meuchel
0e77d575c4 Add TeamViewer Script and Integration Docs 2021-06-30 15:09:04 -05:00
wh1te909
ba42c5e367 Release 0.7.2 2021-06-30 06:53:33 +00:00
wh1te909
6a06734192 bump version 2021-06-30 06:53:16 +00:00
Dan
5e26a406b7 Merge pull request #606 from silversword411/develop
docs update
2021-06-29 23:49:14 -07:00
wh1te909
b6dd03138d rework agent installation auth token to have minimal perms 2021-06-30 06:46:07 +00:00
wh1te909
cf03ee03ee update quasar 2021-06-30 06:46:07 +00:00
silversword411
0e665b6bf0 doc remove incomplete 2021-06-29 23:42:02 -04:00
silversword411
e3d0de7313 consolidated into 3rdparty_screenconnect.md 2021-06-29 23:40:26 -04:00
silversword411
bcf3a543a1 merged into 3rdparty_screenconnect.md 2021-06-29 23:33:00 -04:00
silversword411
b27f17c74a fix case 2021-06-29 11:28:00 -04:00
silversword411
75d864771e Merge branch 'develop' of https://github.com/silversword411/tacticalrmm into develop 2021-06-29 11:24:34 -04:00
silversword411
6420060f2a docs index update 2021-06-29 11:22:52 -04:00
Dan
c149ae71b9 Merge pull request #602 from meuchels/develop
added a Connectwise Control Integration document.
2021-06-28 23:54:37 -07:00
Dan
3a49dd034c Merge pull request #600 from sdm216/develop
Update Win_Firewall_Check_Status.ps1
2021-06-28 23:53:47 -07:00
Dan
b26d7e82e3 Merge pull request #599 from silversword411/develop
docs tweaks
2021-06-28 23:53:02 -07:00
silversword411
415abdf0ce adding windows update info 2021-06-29 01:19:05 -04:00
silversword411
f7f6f6ecb2 Separating out screenconnect docs 2021-06-29 00:39:11 -04:00
meuchels
43d54f134a added a Connectwise Control Integration document. 2021-06-28 17:50:24 -05:00
silversword411
0d2606a13b Revert "code commenting"
This reverts commit ecccf39455.
2021-06-28 15:59:50 -04:00
silversword411
1deb10dc88 community scrip typo 2021-06-28 15:49:25 -04:00
sdm216
1236d55544 Update Win_Firewall_Check_Status.ps1 2021-06-28 14:48:22 -04:00
silversword411
ecccf39455 code commenting 2021-06-28 10:17:14 -04:00
silversword411
8e0316825a Merge branch 'develop' of https://github.com/silversword411/tacticalrmm into develop 2021-06-28 10:05:22 -04:00
silversword411
aa45fa87af Noting case sensitive for all {{}} references 2021-06-28 10:03:32 -04:00
wh1te909
71e78bd0c5 Release 0.7.1 2021-06-28 07:13:33 +00:00
wh1te909
4766477c58 bump version 2021-06-28 07:13:19 +00:00
wh1te909
d97e49ff2b add button to test SMS closes #590 2021-06-28 07:05:57 +00:00
wh1te909
6b9d775cb9 add hostname to email subject/body fixes #589 2021-06-28 06:07:17 +00:00
wh1te909
e521f580d7 make clearing search field when switching client/site optional closes #597 2021-06-28 05:21:07 +00:00
silversword411
25e7cf7db0 Merge branch 'develop' of https://github.com/silversword411/tacticalrmm into develop 2021-06-27 17:16:21 -04:00
silversword411
0cab33787d Noting case sensitive for all {{}} references 2021-06-27 17:15:25 -04:00
wh1te909
bc6faf817f Release 0.7.0 2021-06-27 06:58:48 +00:00
wh1te909
d46ae55863 bump versions 2021-06-27 06:58:06 +00:00
wh1te909
bbd900ab25 move checkin to go 2021-06-27 06:23:37 +00:00
Dan
129ae93e2b Merge pull request #596 from rfost52/develop
Submitting System Report Generator to Community Scripts
2021-06-26 21:58:23 -07:00
rfost52
44dd59fa3f Merge branch 'develop' of https://github.com/rfost52/tacticalrmm into develop 2021-06-26 22:31:00 -04:00
rfost52
ec4e7559b0 updated script header 2021-06-26 22:30:52 -04:00
rfost52
dce40611cf Merge branch 'wh1te909:develop' into develop 2021-06-26 22:17:31 -04:00
rfost52
e71b8546f9 Submitting System Report Generator to Community Scripts 2021-06-26 22:09:56 -04:00
wh1te909
f827348467 style changes 2021-06-27 01:15:47 +00:00
wh1te909
f3978343db cache some values to speed up agent table loading 2021-06-27 00:51:34 +00:00
wh1te909
2654a7ea70 remove extra param 2021-06-27 00:05:00 +00:00
wh1te909
1068bf4ef7 fix row highlight 2021-06-26 17:53:06 +00:00
Dan
e7fccc97cc Merge pull request #595 from rfost52/develop
Initial Parameterization of System Report WIP Script
2021-06-25 23:57:11 -07:00
Dan
733e289852 Merge pull request #592 from silversword411/develop
Docs tweaks
2021-06-25 23:56:44 -07:00
rfost52
29d71a104c include check for C:\Temp folder 2021-06-25 00:36:16 -04:00
rfost52
05200420ad Merge branch 'develop' of https://github.com/rfost52/tacticalrmm into develop 2021-06-24 23:53:26 -04:00
rfost52
eb762d4bfd Initial Parameterization of variables 2021-06-24 23:53:06 -04:00
silversword411
58ace9eda1 Adding wip scripts 2021-06-24 17:20:49 -04:00
sadnub
eeb2623be0 Merge pull request #516 from sadnub/quasar-update
Quasar update to v2
2021-06-24 13:48:47 -04:00
sadnub
cfa242c2fe update loading bar delay 2021-06-24 13:41:34 -04:00
sadnub
ec0441ccc2 fix collector dropdown in policy task edit 2021-06-24 13:41:34 -04:00
sadnub
ae2782a8fe update quasar to v2 release 2021-06-24 13:41:34 -04:00
sadnub
58ff570251 fix assets tab 2021-06-24 13:41:34 -04:00
sadnub
7b554b12c7 update packages 2021-06-24 13:41:34 -04:00
sadnub
58f7603d4f fix agent drowndown in audit manager 2021-06-24 13:41:34 -04:00
sadnub
8895994c54 update packages 2021-06-24 13:41:34 -04:00
sadnub
de8f7e36d5 fix q-checkboxes that need to trigger actions and replace @input with @update:model-value 2021-06-24 13:41:34 -04:00
sadnub
88d7a50265 refactor user administration without vuex 2021-06-24 13:41:34 -04:00
sadnub
21e19fc7e5 add keys back to v-fors 2021-06-24 13:41:34 -04:00
sadnub
faf4935a69 fix saving custom field values and change sites dropdown in edit agent modal 2021-06-24 13:41:34 -04:00
sadnub
71a1f9d74a update reqs and fix custom field values 2021-06-24 13:41:34 -04:00
sadnub
bd8d523e10 stop blinking when loading 2021-06-24 13:41:34 -04:00
sadnub
60cae0e3ac remove 'created' hooks from components and fix agent and script optino dropdowns 2021-06-24 13:41:34 -04:00
sadnub
5a342ac012 removed key from v-for. Fixed custom dropdowns. other fixes 2021-06-24 13:41:34 -04:00
sadnub
bb8767dfc3 fix darkmode and policy check and task tables 2021-06-24 13:41:34 -04:00
sadnub
fcb2779c15 update quasar 2021-06-24 13:41:34 -04:00
sadnub
77dd6c1f61 more fixes 2021-06-24 13:41:34 -04:00
sadnub
8118eef300 upgrade to quasar v2 and vue3 initial 2021-06-24 13:41:34 -04:00
silversword411
802d1489fe adding to howitallworks 2021-06-24 02:42:41 -04:00
silversword411
443a029185 Merge branch 'develop' of https://github.com/silversword411/tacticalrmm into develop 2021-06-24 02:00:51 -04:00
silversword411
4ee508fdd0 Docs tweaks 2021-06-24 01:55:50 -04:00
wh1te909
aa5608f7e8 fix custom field args in bulk script fixes #591 2021-06-24 01:34:14 +00:00
wh1te909
cc472b4613 update celery 2021-06-24 01:32:07 +00:00
wh1te909
764b945ddc fix pipelines 2 2021-06-22 06:51:44 +00:00
wh1te909
fd2206ce4c fix pipelines 2021-06-22 06:47:17 +00:00
Dan
48c0ac9f00 Merge pull request #588 from rfost52/develop
Moving Win_AD_Join_Computer.ps1 from WIP scripts to Community Scripts
2021-06-21 23:38:18 -07:00
silversword411
84eb4fe9ed Merge branch 'develop' of https://github.com/silversword411/tacticalrmm into develop 2021-06-21 11:35:04 -04:00
silversword411
4a5428812c Docs tweaks 2021-06-21 11:34:10 -04:00
silversword411
023f98a89d Docs tweaks 2021-06-21 11:32:56 -04:00
rfost52
66893dd0c1 Update Win_AD_Join_Computer.ps1 2021-06-19 20:50:56 -04:00
rfost52
25a6666e35 Adding AD PC Join to Listings 2021-06-19 20:47:11 -04:00
rfost52
19d75309b5 Merge branch 'develop' of https://github.com/rfost52/tacticalrmm into develop 2021-06-19 20:21:21 -04:00
rfost52
11110d65c1 Adding to Community Scripts
Moving from WIP Scripts to Community Scripts after successful testing.
2021-06-19 20:21:11 -04:00
Dan
a348f58fe2 Merge pull request #585 from rfost52/develop
First rework of Join to AD PowerShell WIP Script
2021-06-19 11:41:52 -07:00
rfost52
13851dd976 Added new line at end of code 2021-06-18 23:25:15 -04:00
rfost52
2ec37c5da9 1st Code rework with parameterization 2021-06-18 22:57:23 -04:00
rfost52
8c127160de Updated synopsis and description 2021-06-18 22:51:21 -04:00
rfost52
2af820de9a Update Win_AD_Join_Computer.ps1
Parameters, error checking with exit codes
2021-06-18 22:43:26 -04:00
Dan
55fb0bb3a0 Merge pull request #584 from silversword411/develop
community script updates
2021-06-18 10:58:00 -07:00
silversword411
9f9ecc521f community script updates 2021-06-17 15:27:40 -04:00
Dan
dfd01df5ba Merge pull request #581 from silversword411/develop
Adding docs
2021-06-16 22:55:18 -07:00
silversword411
474090698c Merge branch 'wh1te909:develop' into develop 2021-06-17 01:00:40 -04:00
silversword411
6b71cdeea4 Merge branch 'develop' of https://github.com/silversword411/tacticalrmm into develop 2021-06-17 00:53:58 -04:00
wh1te909
581e974236 add view setting perms closes #569 2021-06-17 04:36:34 +00:00
wh1te909
ba3c3a42ce add missing mypy types 2021-06-17 04:35:51 +00:00
silversword411
c8bc5671c5 adding all possible script variables to docs 2021-06-17 00:34:11 -04:00
wh1te909
ff9401a040 make failing tasks fail client tree closes #571 2021-06-17 03:51:20 +00:00
wh1te909
5e1bc1989f update reqs 2021-06-17 03:50:00 +00:00
wh1te909
a1dc91cd7d fix typo in docs #580 2021-06-16 16:46:24 +00:00
sadnub
99f2772bb3 Fixes #577 2021-06-14 20:27:41 -04:00
sadnub
e5d0e42655 fix agent policies not updating when monitoring mode is changed 2021-06-14 20:18:56 -04:00
Dan
2c914cc374 Merge pull request #576 from bradhawkins85/patch-19
Update installer.ps1
2021-06-14 09:45:13 -07:00
Dan
9bceb62381 Merge pull request #575 from nextgi/zak-develop
Updates to Devcontainer and Added #467
2021-06-14 09:44:58 -07:00
Zak
de7518a800 Added new community script
New script for auto documenting ADDS.
2021-06-13 17:56:44 -07:00
bradhawkins85
304fb63453 Update installer.ps1
Fix spelling errors
2021-06-13 17:22:13 +10:00
Zak
0f7ef60ca0 Added #467
Added QTooltip to the label of the QItem in the QTree.
2021-06-12 20:50:59 -07:00
Zak
07c74e4641 Updated devcontainer
Prior it was statically set to use a specific range of IPs. I changed this so it could be set via environment variables. Also, NATS port 4222 is a reserved port for Hyper-V. I updated this so it could be set in env variables as well.
2021-06-12 20:49:10 -07:00
wh1te909
de7f325cfb fix redis appendonly backup/restore 2021-06-13 00:10:58 +00:00
wh1te909
42cdf70cb4 Release 0.6.15 2021-06-12 20:41:19 +00:00
wh1te909
6beb6be131 bump version 2021-06-12 20:40:54 +00:00
wh1te909
fa4fc2a708 only parse script args for script checks 2021-06-12 20:24:51 +00:00
wh1te909
2db9758260 fix custom fields in script checks #568 2021-06-12 19:41:49 +00:00
wh1te909
715982e40a Release 0.6.14 2021-06-11 04:41:48 +00:00
wh1te909
d00cd4453a bump versions 2021-06-11 04:40:57 +00:00
wh1te909
429c08c24a fix width on q-file caused by recent quasar update 2021-06-11 03:58:57 +00:00
wh1te909
6a71490e20 update reqs 2021-06-11 02:40:22 +00:00
Dan
9bceda0646 Merge pull request #562 from diekinderwelt/nginx_enable_ipv6
enable ipv6 in nginx config
2021-06-10 18:59:34 -07:00
Dan
a1027a6773 Merge pull request #565 from silversword411/develop
Docs Update - adding design and tipsntricks
2021-06-10 18:59:12 -07:00
silversword411
302d4b75f9 formatting fix 2021-06-08 15:39:43 -04:00
silversword411
5f6ee0e883 Docs Update - adding design and tipsntricks 2021-06-08 14:45:02 -04:00
Silvio
27f9720de1 enable ipv6 in nginx config
Signed-off-by: Silvio <silvio.zimmer@die-kinderwelt.com>
2021-06-08 11:43:55 +02:00
sadnub
22aa3fdbbc fix bug with policy copy and task that triggers on check failure. Fix check history tests 2021-06-06 23:19:07 -04:00
sadnub
069ecdd33f apply redis configuration after restore 2021-06-06 22:58:32 -04:00
sadnub
dd545ae933 catch an exception that a celery task could potentially throw and configure automation task retries 2021-06-06 22:55:47 -04:00
sadnub
6650b705c4 configure redis to use an appendonly file for celery task reliability 2021-06-06 22:54:52 -04:00
sadnub
59b0350289 fix duplicate tasks when there is an assigned check 2021-06-06 22:54:06 -04:00
sadnub
1ad159f820 remove foreign key from checkhistory to make mass check deletes reliable. (This will not migrate check history data) 2021-06-06 22:53:11 -04:00
Dan
0bf42190e9 Merge pull request #544 from bbrendon/patch-1
check for proper OS support
2021-05-30 23:10:21 -07:00
bbrendon
d2fa836232 check for proper OS support 2021-05-30 10:39:08 -07:00
Dan
c387774093 Merge pull request #543 from bbrendon/develop
fixed an edge case and warning notes
2021-05-29 22:39:52 -07:00
bbrendon
e99736ba3c fixed an edge case and warning notes 2021-05-29 19:25:53 -07:00
wh1te909
16cb54fcc9 fix multiline output not working for automation task 2021-05-29 18:47:09 +00:00
wh1te909
5aa15c51ec Release 0.6.13 2021-05-29 07:35:29 +00:00
wh1te909
a8aedd9cf3 bump version 2021-05-29 07:35:10 +00:00
wh1te909
b851b632bc fix agent_outages_task async error 2021-05-29 07:26:10 +00:00
wh1te909
541e07fb65 Release 0.6.12 2021-05-29 05:16:37 +00:00
wh1te909
6ad16a897d bump versions 2021-05-29 05:15:26 +00:00
wh1te909
72f1053a93 change interval 2021-05-29 04:49:17 +00:00
sadnub
fb15a2762c allow saving multiple script output in custom fields #533 2021-05-28 23:52:23 -04:00
wh1te909
9165248b91 update go/codec 2021-05-29 03:20:12 +00:00
sadnub
add18b29db fix agent dropdown 2021-05-28 22:59:44 -04:00
wh1te909
1971653548 bump nats/mesh 2021-05-29 02:53:16 +00:00
wh1te909
392cd64d7b hide settings in hosted 2021-05-29 02:20:07 +00:00
wh1te909
b5affbb7c8 change function name 2021-05-29 02:18:57 +00:00
wh1te909
71d1206277 more checks rework 2021-05-29 01:37:20 +00:00
wh1te909
26e6a8c409 update reqs 2021-05-28 18:12:32 +00:00
wh1te909
eb54fae11a more checks rework 2021-05-28 17:54:57 +00:00
wh1te909
ee773e5966 remove deprecated func 2021-05-28 17:54:14 +00:00
wh1te909
7218ccdba8 start checks rework 2021-05-27 07:16:06 +00:00
wh1te909
332400e48a autogrow text field fixes #533 2021-05-27 07:09:40 +00:00
Dan
ad1a5d3702 Merge pull request #534 from silversword411/develop
Script library and docs tweaks
2021-05-26 23:59:08 -07:00
silversword411
3006b4184d Docs update on regular patching 2021-05-26 21:36:28 -04:00
silversword411
84eb84a080 Script library adding comments 2021-05-26 10:19:30 -04:00
sadnub
60beea548b Allow clearing resolved/failure actions in alert template 2021-05-24 22:18:12 -04:00
Dan
5f9c149e59 Merge pull request #528 from bbrendon/develop
updated timeouts and fixed one script
2021-05-21 18:36:07 -07:00
bbrendon
53367c6f04 update timeouts on some scripts 2021-05-21 18:01:16 -07:00
bbrendon
d7f817ee44 syntax error fix. 2021-05-21 17:56:53 -07:00
Dan
d33a87da54 Merge pull request #526 from silversword411/develop
script library - Screenconnect collector
2021-05-20 20:13:51 -07:00
silversword411
3aebfb12b7 Merge branch 'develop' of https://github.com/silversword411/tacticalrmm into develop 2021-05-20 21:50:10 -04:00
silversword411
1d6c55ffa6 Script library - screenconnect collector 2021-05-20 21:49:01 -04:00
Dan
5e7080aac3 Merge pull request #522 from silversword411/develop
Docs Example and wip tweaks
2021-05-20 18:37:33 -07:00
silversword411
fad739bc01 Updating script delegated folders 2021-05-20 10:10:59 -04:00
silversword411
c6b7f23884 Adding URL Action Example to docs 2021-05-19 02:46:51 -04:00
silversword411
a6f7e446de tweaking wip scripts 2021-05-18 23:22:45 -04:00
wh1te909
89d95d3ae1 Release 0.6.11 2021-05-19 03:08:29 +00:00
wh1te909
764208698f bump version 2021-05-19 03:04:06 +00:00
Dan
57129cf934 Merge pull request #521 from agit8or/develop
Create Win_Shortcut_Creator.ps1
2021-05-18 18:10:33 -07:00
Dan
aae1a842d5 Merge pull request #519 from silversword411/develop
add script to wip
2021-05-18 18:10:03 -07:00
agit8or
623f35aec7 Create Win_Shortcut_Creator2.ps1 2021-05-18 13:05:46 -04:00
agit8or
870bf842cf Create Win_Shortcut_Creator.ps1 2021-05-18 13:00:26 -04:00
silversword411
07f2d7dd5c wip additions for printers 2021-05-18 02:00:55 -04:00
silversword411
f223f2edc5 Merge branch 'wh1te909:develop' into develop 2021-05-17 22:47:22 -04:00
wh1te909
e848a9a577 fix tests 2021-05-17 06:45:43 +00:00
wh1te909
7569d98e07 fix task args fixes #514 2021-05-17 06:01:28 +00:00
wh1te909
596dee2f24 update docs 2021-05-15 08:07:30 +00:00
wh1te909
9970403964 Release 0.6.10 2021-05-15 07:52:35 +00:00
wh1te909
07a88ae00d bump versions 2021-05-15 07:51:44 +00:00
wh1te909
5475b4d287 typo 2021-05-15 02:20:33 +00:00
sadnub
6631dcfd3e Fix custom check run interval. Fixes #473 2021-05-14 21:37:49 -04:00
sadnub
0dd3f337f3 Add Client and Site categories for agent select options. Fixes #499 2021-05-14 20:27:32 -04:00
silversword411
8eb27b5875 Merge branch 'wh1te909:develop' into develop 2021-05-14 19:03:42 -04:00
sadnub
2d1863031c fix default custom field value not being used if blank value is present on model. Fixes #501 2021-05-14 18:48:49 -04:00
sadnub
9feb76ca81 fix tests 2021-05-14 18:19:57 -04:00
sadnub
993e8f4ab3 sort script categories prior to formating script options #506 2021-05-14 18:08:51 -04:00
sadnub
e08ae95d4f Fix alignment issue #512 2021-05-14 18:08:51 -04:00
sadnub
15359e8846 ws wip 2021-05-14 18:08:51 -04:00
silversword411
d1457b312b wip addition create shortcut to URL 2021-05-14 17:50:50 -04:00
silversword411
c9dd2af196 Merge branch 'wh1te909:develop' into develop 2021-05-14 14:41:12 -04:00
wh1te909
564ef4e688 feat: add clear faults #484 2021-05-14 04:54:59 +00:00
wh1te909
a33e6e8bb5 move token refresh before local settings import to allow overriding #503 2021-05-14 01:47:25 +00:00
Dan
cf34f33f04 Merge pull request #507 from silversword411/develop
Script library and docs updates
2021-05-13 12:50:21 -07:00
silversword411
827cfe4e8f Merge branch 'wh1te909:develop' into develop 2021-05-13 13:44:45 -04:00
silversword411
2ce1c2383c Merge branch 'develop' of https://github.com/silversword411/tacticalrmm into develop 2021-05-13 13:38:51 -04:00
silversword411
6fc0a665ae script library docs - volunteers needed 2021-05-13 13:36:33 -04:00
silversword411
4f16d01263 script library - sn collector 2021-05-13 12:37:10 -04:00
sadnub
67cc37354a Evaluate policies on exclusion changes. Fixes #500 2021-05-12 18:17:03 -04:00
silversword411
e388243ef4 renaming wips 2021-05-12 11:32:16 -04:00
silversword411
3dc92763c7 Script library add 2021-05-12 11:25:22 -04:00
Dan
dfe97dd466 Merge pull request #493 from silversword411/develop
Adding comment headers to wip1
2021-05-12 00:36:57 -07:00
wh1te909
2803cee29b Release 0.6.9 2021-05-12 07:08:41 +00:00
wh1te909
3a03020e54 bump versions 2021-05-12 07:07:51 +00:00
wh1te909
64443cc703 fix link 2021-05-12 06:46:51 +00:00
wh1te909
4d1aa6ed18 fix 404 2021-05-12 06:29:36 +00:00
wh1te909
84837e88d2 update reqs 2021-05-12 05:53:09 +00:00
wh1te909
ff49c936ea fix tests 2021-05-12 05:52:36 +00:00
wh1te909
e6e0901329 add optional installer arg for custom mesh dir #487 2021-05-12 03:32:03 +00:00
silversword411
23b6284b51 Adding comment headers to wip2 2021-05-11 22:55:01 -04:00
silversword411
33dfbcbe32 Adding comment headers to wip1 2021-05-11 22:53:37 -04:00
wh1te909
700c23d537 fix sorting #491 2021-05-12 02:28:00 +00:00
wh1te909
369fac9e38 clear search when switching client tree #492 2021-05-12 01:43:11 +00:00
wh1te909
2229eb1167 add role perms 2021-05-11 17:42:43 +00:00
wh1te909
a3dec841b6 get more accurate model for lenovo #490 2021-05-11 17:15:21 +00:00
wh1te909
b17620bdb6 refactor perms into roles 2021-05-11 07:10:18 +00:00
sadnub
f39cd5ae2f make the policy automated tasks check assignment work correctly and add tests 2021-05-10 20:35:38 -04:00
sadnub
83a19e005b exclude autotask creation on agent when policy is being copied 2021-05-10 18:21:25 -04:00
sadnub
a9dd01b0c8 rework alert template form into a stepper. Add better docs for Alert Templates 2021-05-08 23:40:09 -04:00
wh1te909
eb59afa1d1 isort 2021-05-08 17:28:29 +00:00
wh1te909
2adcfce9d0 fix tests 2021-05-08 17:27:01 +00:00
wh1te909
314ab9b304 fix migrations 2021-05-08 17:16:43 +00:00
wh1te909
8576fb82c7 merge permissions 2021-05-08 17:05:52 +00:00
wh1te909
0f95a6bb2f add permissions #162 2021-05-08 17:02:23 +00:00
sadnub
ad5104567d formatting 2021-05-07 18:03:08 -04:00
sadnub
ece68ba1d5 remove import 2021-05-07 17:58:50 -04:00
sadnub
acccd3a586 add url action docs 2021-05-07 17:53:55 -04:00
sadnub
8ebef1c1ca fix editing error in preferences 2021-05-07 12:12:58 -04:00
sadnub
28abc0d5ed allow setting a url action as agent dblclick action 2021-05-07 11:45:55 -04:00
sadnub
1efe25d3ec finish url actions with tests 2021-05-07 10:22:37 -04:00
sadnub
c40e4f8e4b url actions ui 2021-05-07 10:22:37 -04:00
Dan
baca84092d Merge pull request #479 from silversword411/develop
Updating docs - unsupported scripts
2021-05-06 10:06:37 -07:00
silversword411
346d4da059 Updating docs - unsupported scripts 2021-05-05 16:23:05 -04:00
wh1te909
ade64d6c0a Release 0.6.8 2021-05-05 17:07:19 +00:00
wh1te909
8204bdfc5f bump versions 2021-05-05 17:06:57 +00:00
wh1te909
1a9bb3e986 fix update script 2021-05-05 07:59:23 +00:00
wh1te909
49356479e5 fix update script 2021-05-05 07:58:30 +00:00
wh1te909
c44e9a7292 Release 0.6.7 2021-05-05 07:27:54 +00:00
wh1te909
21771a593f bump versions 2021-05-05 07:25:59 +00:00
wh1te909
84458dfc4c add agent proxy docs 2021-05-05 06:55:48 +00:00
wh1te909
5835632dab add button to force code signing 2021-05-05 06:50:25 +00:00
Dan
67aa7229ef Merge pull request #475 from silversword411/develop
Adding docs regarding HAProxy
2021-05-04 20:23:55 -07:00
silversword411
b72dc3ed3a Adding docs regarding HAProxy 2021-05-04 22:57:33 -04:00
wh1te909
0f93d4a5bd improve wording 2021-05-05 02:18:21 +00:00
wh1te909
106320b035 nats 2.2.2 2021-05-05 02:04:03 +00:00
wh1te909
63951705cd update reqs 2021-05-05 02:03:11 +00:00
Dan
a8d56921d5 Merge pull request #472 from silversword411/develop
Tweaking patches pane
2021-05-04 19:01:32 -07:00
sadnub
10bc133cf1 fix other checks getting deleted when deleting a policy check 2021-05-04 20:01:44 -04:00
silversword411
adeb5b35c9 Tweaking patches pane
Co-authored-by: sadnub <sadnub@users.noreply.github.com> using Live Share
2021-05-04 15:43:40 -04:00
Dan
589ff46ea5 Merge pull request #471 from silversword411/develop
script library addition
2021-05-04 11:07:11 -07:00
silversword411
656fcb9fe7 script library - adding tcp reset script 2021-05-04 13:18:43 -04:00
silversword411
1cb9353006 Revert "script library - adding tcp reset script"
This reverts commit 659846ed88.
2021-05-04 13:16:07 -04:00
silversword411
57bf16ba07 Merge branch 'develop' of https://github.com/silversword411/tacticalrmm into develop 2021-05-04 10:03:48 -04:00
silversword411
659846ed88 script library - adding tcp reset script 2021-05-04 10:02:58 -04:00
silversword411
25894044e0 script library - adding outlook delegated folders 2021-05-04 10:02:58 -04:00
silversword411
e7a0826beb tweaking script docs 2021-05-04 10:02:57 -04:00
silversword411
1f7ddee23b script library - adding tcp reset script 2021-05-04 10:02:21 -04:00
Dan
7e186730db Merge pull request #470 from bradhawkins85/patch-17
Update Win_ScreenConnectAIO.ps1
2021-05-03 23:51:03 -07:00
Dan
6713a50208 Merge branch 'develop' into patch-17 2021-05-03 23:50:54 -07:00
Dan
7c9d8fcfec Merge pull request #469 from bradhawkins85/patch-18
Update community_scripts.json
2021-05-03 23:49:45 -07:00
Dan
33bfc8cfe8 Merge pull request #466 from InsaneTechnologies/develop
Add in Client and Site variables
2021-05-03 23:49:35 -07:00
wh1te909
ca735bc14a fix ui for custom fields with very long text 2021-05-04 06:47:53 +00:00
bradhawkins85
4ba748a18b Update community_scripts.json
Add variables to include client name and site name to install in correct groups in ScreenConnect
2021-05-04 16:19:44 +10:00
bradhawkins85
f1845106f8 Update Win_ScreenConnectAIO.ps1
Include client name and site name in URL to add agent to correct group in ScreenConnect
2021-05-04 16:17:52 +10:00
David Rudduck
67e7156c4b Create Alert_MSTeams.ps1
Very raw MS Teams alert script
2021-05-04 11:47:09 +10:00
silversword411
4a476adebf Merge branch 'develop' of https://github.com/silversword411/tacticalrmm into develop 2021-05-03 18:22:33 -04:00
silversword411
918798f8cc script library - adding outlook delegated folders 2021-05-03 18:20:38 -04:00
silversword411
5a3f868866 tweaking script docs 2021-05-03 18:05:27 -04:00
silversword411
feea2c6396 tweaking script docs 2021-05-03 14:15:21 -04:00
Dan
707b4c46d9 Merge pull request #464 from silversword411/develop
tweaking docs and adding scripts
2021-05-03 07:56:01 -07:00
David Rudduck
89ca39fc2b Update Win_ScreenConnectAIO.ps1 2021-05-03 11:31:49 +10:00
David Rudduck
204281b12d Merge pull request #1 from InsaneTechnologies/scripts-screenconnect-1-1
Update Win_ScreenConnectAIO.ps1
2021-05-03 11:30:30 +10:00
David Rudduck
a8538a7e95 Update Win_ScreenConnectAIO.ps1
added support for `-company {{client.name}} -site {{site.name}}` command line arguments. 

This results in ScreenConnect adding those fields to the agent so it's easier to filter down.
2021-05-03 11:29:48 +10:00
silversword411
dee1b471e9 tweaking script docs 2021-05-02 20:03:09 -04:00
silversword411
aa04e9b01f Script - display message to user tweak 2021-05-02 11:54:51 -04:00
silversword411
350f0dc604 Standardized Comments for scripts 2021-05-02 11:52:47 -04:00
silversword411
6021f2efd6 Add wip script 2021-05-02 11:42:00 -04:00
wh1te909
51838ec25a retry uninstall a few times 2021-05-02 08:45:19 +00:00
wh1te909
54768a121e add exact datetime of next agent update cycle in pending actions #457 2021-05-01 07:11:12 +00:00
wh1te909
8ff72cdca3 fix cors exception msg 2021-05-01 06:20:51 +00:00
sadnub
2cb53ad06b error handling and axios changes 2021-04-30 18:35:56 -04:00
sadnub
b8349de31d add additional check in delete policy task test 2021-04-30 18:35:56 -04:00
wh1te909
d7e11af7f8 fix speedtest.py 2021-04-30 07:18:13 +00:00
wh1te909
dd8d39e698 Release 0.6.6 2021-04-30 07:05:04 +00:00
wh1te909
afb1316daa bump versions 2021-04-30 07:01:22 +00:00
wh1te909
04d7017536 rework ping checks #444 2021-04-30 06:32:21 +00:00
wh1te909
6a1c75b060 add help toolbar #452 2021-04-30 06:01:22 +00:00
Dan
5c94611f3b Merge pull request #456 from silversword411/develop
WIP it, WIP it good: and script library stuff
2021-04-29 18:08:07 -07:00
silversword411
4e5676e80f adding the wip 2021-04-29 11:45:32 -04:00
wh1te909
c96d688a9c add alert if new trmm version available #453 2021-04-29 08:12:44 +00:00
silversword411
804242e9a5 Merge branch 'develop' of https://github.com/silversword411/tacticalrmm into develop 2021-04-28 22:50:47 -04:00
silversword411
0ec9760b17 Adding to docker 2021-04-28 22:49:49 -04:00
Dan
d481ae3da4 Merge pull request #443 from bradhawkins85/patch-16
Update Win_ScreenConnectAIO.ps1
2021-04-28 09:04:43 -07:00
silversword411
4742c14fc1 Rename temp script 2021-04-28 11:12:18 -04:00
bradhawkins85
509b0d501b Update Win_ScreenConnectAIO.ps1
Updated script notes regarding quoting around variables.
2021-04-28 10:10:18 +10:00
silversword411
d4c9b04d4e Hidden Script Library todo list 2021-04-27 13:11:30 -04:00
silversword411
16fb4d331b script library adding msi install ref script 2021-04-27 13:07:14 -04:00
silversword411
e9e5bf31a7 script library adding file copy script 2021-04-27 12:50:01 -04:00
wh1te909
221418120e Release 0.6.5 2021-04-27 16:20:25 +00:00
wh1te909
46f852e26e bump version 2021-04-27 16:20:08 +00:00
sadnub
4234cf0a31 fix policy task deletion 2021-04-27 12:12:04 -04:00
wh1te909
7f3daea648 Release 0.6.4 2021-04-27 15:36:49 +00:00
wh1te909
2eb16c82f4 bump version 2021-04-27 15:36:38 +00:00
sadnub
e00b2ce591 add test for check deletes 2021-04-27 11:04:06 -04:00
sadnub
d71e1311ca fix deleting checks 2021-04-27 10:58:23 -04:00
sadnub
2cf16963e3 fix custom fields on policy tasks 2021-04-27 10:51:29 -04:00
wh1te909
10bf7b7fb4 update restore docs 2021-04-27 06:18:15 +00:00
665 changed files with 46687 additions and 36431 deletions

View File

@@ -26,3 +26,6 @@ POSTGRES_PASS=postgrespass
APP_PORT=80
API_PORT=80
HTTP_PROTOCOL=https
DOCKER_NETWORK=172.21.0.0/24
DOCKER_NGINX_IP=172.21.0.20
NATS_PORTS=4222:4222

View File

@@ -1,4 +1,4 @@
FROM python:3.9.2-slim
FROM python:3.9.9-slim
ENV TACTICAL_DIR /opt/tactical
ENV TACTICAL_READY_FILE ${TACTICAL_DIR}/tmp/tactical.ready
@@ -13,12 +13,13 @@ EXPOSE 8000 8383 8005
RUN groupadd -g 1000 tactical && \
useradd -u 1000 -g 1000 tactical
# Copy Dev python reqs
COPY ./requirements.txt /
# Copy dev python reqs
COPY .devcontainer/requirements.txt /
# Copy Docker Entrypoint
COPY ./entrypoint.sh /
# Copy docker entrypoint.sh
COPY .devcontainer/entrypoint.sh /
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
WORKDIR ${WORKSPACE_DIR}/api/tacticalrmm

View File

@@ -6,8 +6,8 @@ services:
image: api-dev
restart: always
build:
context: .
dockerfile: ./api.dockerfile
context: ..
dockerfile: .devcontainer/api.dockerfile
command: ["tactical-api"]
environment:
API_PORT: ${API_PORT}
@@ -46,7 +46,7 @@ services:
API_PORT: ${API_PORT}
DEV: 1
ports:
- "4222:4222"
- "${NATS_PORTS}"
volumes:
- tactical-data-dev:/opt/tactical
- ..:/workspace:cached
@@ -67,7 +67,7 @@ services:
MESH_PASS: ${MESH_PASS}
MONGODB_USER: ${MONGODB_USER}
MONGODB_PASSWORD: ${MONGODB_PASSWORD}
NGINX_HOST_IP: 172.21.0.20
NGINX_HOST_IP: ${DOCKER_NGINX_IP}
networks:
dev:
aliases:
@@ -115,7 +115,10 @@ services:
redis-dev:
container_name: trmm-redis-dev
restart: always
command: redis-server --appendonly yes
image: redis:6.0-alpine
volumes:
- redis-data-dev:/data
networks:
dev:
aliases:
@@ -124,9 +127,6 @@ services:
init-dev:
container_name: trmm-init-dev
image: api-dev
build:
context: .
dockerfile: ./api.dockerfile
restart: on-failure
command: ["tactical-init-dev"]
environment:
@@ -153,9 +153,6 @@ services:
celery-dev:
container_name: trmm-celery-dev
image: api-dev
build:
context: .
dockerfile: ./api.dockerfile
command: ["tactical-celery-dev"]
restart: always
networks:
@@ -171,9 +168,6 @@ services:
celerybeat-dev:
container_name: trmm-celerybeat-dev
image: api-dev
build:
context: .
dockerfile: ./api.dockerfile
command: ["tactical-celerybeat-dev"]
restart: always
networks:
@@ -189,9 +183,6 @@ services:
websockets-dev:
container_name: trmm-websockets-dev
image: api-dev
build:
context: .
dockerfile: ./api.dockerfile
command: ["tactical-websockets-dev"]
restart: always
networks:
@@ -218,9 +209,10 @@ services:
CERT_PRIV_KEY: ${CERT_PRIV_KEY}
APP_PORT: ${APP_PORT}
API_PORT: ${API_PORT}
DEV: 1
networks:
dev:
ipv4_address: 172.21.0.20
ipv4_address: ${DOCKER_NGINX_IP}
ports:
- "80:80"
- "443:443"
@@ -231,9 +223,6 @@ services:
container_name: trmm-mkdocs-dev
image: api-dev
restart: always
build:
context: .
dockerfile: ./api.dockerfile
command: ["tactical-mkdocs-dev"]
ports:
- "8005:8005"
@@ -247,6 +236,7 @@ volumes:
postgres-data-dev:
mongo-dev-data:
mesh-data-dev:
redis-data-dev:
networks:
dev:
@@ -254,4 +244,4 @@ networks:
ipam:
driver: default
config:
- subnet: 172.21.0.0/24
- subnet: ${DOCKER_NETWORK}

View File

@@ -78,24 +78,6 @@ DATABASES = {
}
}
REST_FRAMEWORK = {
'DATETIME_FORMAT': '%b-%d-%Y - %H:%M',
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
),
'DEFAULT_AUTHENTICATION_CLASSES': (
'knox.auth.TokenAuthentication',
),
}
if not DEBUG:
REST_FRAMEWORK.update({
'DEFAULT_RENDERER_CLASSES': (
'rest_framework.renderers.JSONRenderer',
)
})
MESH_USERNAME = '${MESH_USER}'
MESH_SITE = 'https://${MESH_HOST}'
MESH_TOKEN_KEY = '${MESH_TOKEN}'
@@ -114,6 +96,8 @@ 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
echo "from accounts.models import User; User.objects.create_superuser('${TRMM_USER}', 'admin@example.com', '${TRMM_PASS}') if not User.objects.filter(username='${TRMM_USER}').exists() else 0;" | python manage.py shell

View File

@@ -2,6 +2,8 @@
asyncio-nats-client
celery
channels
channels_redis
django-ipware
Django
django-cors-headers
django-rest-knox
@@ -33,3 +35,4 @@ Pygments
mypy
pysnooper
isort
drf_spectacular

70
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@@ -0,0 +1,70 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ develop ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ develop ]
schedule:
- cron: '19 14 * * 6'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'go', 'javascript', 'python' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://git.io/codeql-language-support
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

34
.github/workflows/devskim-analysis.yml vendored Normal file
View File

@@ -0,0 +1,34 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
name: DevSkim
on:
push:
branches: [ develop ]
pull_request:
branches: [ develop ]
schedule:
- cron: '19 5 * * 0'
jobs:
lint:
name: DevSkim
runs-on: ubuntu-20.04
permissions:
actions: read
contents: read
security-events: write
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Run DevSkim scanner
uses: microsoft/DevSkim-Action@v1
- name: Upload DevSkim scan results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v1
with:
sarif_file: devskim-results.sarif

4
.gitignore vendored
View File

@@ -47,3 +47,7 @@ docs/.vuepress/dist
nats-rmm.conf
.mypy_cache
docs/site/
reset_db.sh
run_go_cmd.py
nats-api.conf

View File

@@ -9,7 +9,7 @@ Tactical RMM is a remote monitoring & management tool for Windows computers, bui
It uses an [agent](https://github.com/wh1te909/rmmagent) written in golang and integrates with [MeshCentral](https://github.com/Ylianst/MeshCentral)
# [LIVE DEMO](https://rmm.tacticalrmm.io/)
Demo database resets every hour. Alot of features are disabled for obvious reasons due to the nature of this app.
Demo database resets every hour. A lot of features are disabled for obvious reasons due to the nature of this app.
### [Discord Chat](https://discord.gg/upGTkWp)
@@ -35,4 +35,4 @@ Demo database resets every hour. Alot of features are disabled for obvious reaso
## Installation / Backup / Restore / Usage
### Refer to the [documentation](https://wh1te909.github.io/tacticalrmm/)
### Refer to the [documentation](https://wh1te909.github.io/tacticalrmm/)

19
SECURITY.md Normal file
View File

@@ -0,0 +1,19 @@
# Security Policy
## Supported Versions
Use this section to tell people about which versions of your project are
currently being supported with security updates.
| Version | Supported |
| ------- | ------------------ |
| 0.10.4 | :white_check_mark: |
| < 0.10.4| :x: |
## Reporting a Vulnerability
Use this section to tell people how to report a vulnerability.
Tell them where to go, how often they can expect to get an update on a
reported vulnerability, what to expect if the vulnerability is accepted or
declined, etc.

View File

@@ -1,7 +1,8 @@
from django.contrib import admin
from rest_framework.authtoken.admin import TokenAdmin
from .models import User
from .models import User, Role
admin.site.register(User)
TokenAdmin.raw_id_fields = ("user",)
admin.site.register(Role)

View File

@@ -0,0 +1,19 @@
import uuid
from django.core.management.base import BaseCommand
from accounts.models import User
class Command(BaseCommand):
help = "Creates the installer user"
def handle(self, *args, **kwargs):
if User.objects.filter(is_installer_user=True).exists():
return
User.objects.create_user( # type: ignore
username=uuid.uuid4().hex,
is_installer_user=True,
password=User.objects.make_random_password(60), # type: ignore
block_dashboard_login=True,
)

View File

@@ -0,0 +1,25 @@
# Generated by Django 3.2.1 on 2021-05-07 15:26
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0022_urlaction'),
('accounts', '0015_user_loading_bar_color'),
]
operations = [
migrations.AddField(
model_name='user',
name='url_action',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='user', to='core.urlaction'),
),
migrations.AlterField(
model_name='user',
name='agent_dblclick_action',
field=models.CharField(choices=[('editagent', 'Edit Agent'), ('takecontrol', 'Take Control'), ('remotebg', 'Remote Background'), ('urlaction', 'URL Action')], default='editagent', max_length=50),
),
]

View File

@@ -0,0 +1,173 @@
# Generated by Django 3.2.1 on 2021-05-08 17:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0016_auto_20210507_1526'),
]
operations = [
migrations.AddField(
model_name='user',
name='can_code_sign',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_do_server_maint',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_edit_agent',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_edit_core_settings',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_install_agents',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_manage_accounts',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_manage_alerts',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_manage_automation_policies',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_manage_autotasks',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_manage_checks',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_manage_clients',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_manage_deployments',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_manage_notes',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_manage_pendingactions',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_manage_procs',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_manage_scripts',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_manage_sites',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_manage_software',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_manage_winsvcs',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_manage_winupdates',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_reboot_agents',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_run_autotasks',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_run_bulk',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_run_checks',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_run_scripts',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_send_cmd',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_uninstall_agents',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_update_agents',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_use_mesh',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_view_auditlogs',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_view_debuglogs',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='user',
name='can_view_eventlogs',
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,181 @@
# Generated by Django 3.2.1 on 2021-05-11 02:33
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0017_auto_20210508_1716'),
]
operations = [
migrations.CreateModel(
name='Role',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, unique=True)),
('is_superuser', models.BooleanField(default=False)),
('can_use_mesh', models.BooleanField(default=False)),
('can_uninstall_agents', models.BooleanField(default=False)),
('can_update_agents', models.BooleanField(default=False)),
('can_edit_agent', models.BooleanField(default=False)),
('can_manage_procs', models.BooleanField(default=False)),
('can_view_eventlogs', models.BooleanField(default=False)),
('can_send_cmd', models.BooleanField(default=False)),
('can_reboot_agents', models.BooleanField(default=False)),
('can_install_agents', models.BooleanField(default=False)),
('can_run_scripts', models.BooleanField(default=False)),
('can_run_bulk', models.BooleanField(default=False)),
('can_manage_notes', models.BooleanField(default=False)),
('can_edit_core_settings', models.BooleanField(default=False)),
('can_do_server_maint', models.BooleanField(default=False)),
('can_code_sign', models.BooleanField(default=False)),
('can_manage_checks', models.BooleanField(default=False)),
('can_run_checks', models.BooleanField(default=False)),
('can_manage_clients', models.BooleanField(default=False)),
('can_manage_sites', models.BooleanField(default=False)),
('can_manage_deployments', models.BooleanField(default=False)),
('can_manage_automation_policies', models.BooleanField(default=False)),
('can_manage_autotasks', models.BooleanField(default=False)),
('can_run_autotasks', models.BooleanField(default=False)),
('can_view_auditlogs', models.BooleanField(default=False)),
('can_manage_pendingactions', models.BooleanField(default=False)),
('can_view_debuglogs', models.BooleanField(default=False)),
('can_manage_scripts', models.BooleanField(default=False)),
('can_manage_alerts', models.BooleanField(default=False)),
('can_manage_winsvcs', models.BooleanField(default=False)),
('can_manage_software', models.BooleanField(default=False)),
('can_manage_winupdates', models.BooleanField(default=False)),
('can_manage_accounts', models.BooleanField(default=False)),
],
),
migrations.RemoveField(
model_name='user',
name='can_code_sign',
),
migrations.RemoveField(
model_name='user',
name='can_do_server_maint',
),
migrations.RemoveField(
model_name='user',
name='can_edit_agent',
),
migrations.RemoveField(
model_name='user',
name='can_edit_core_settings',
),
migrations.RemoveField(
model_name='user',
name='can_install_agents',
),
migrations.RemoveField(
model_name='user',
name='can_manage_accounts',
),
migrations.RemoveField(
model_name='user',
name='can_manage_alerts',
),
migrations.RemoveField(
model_name='user',
name='can_manage_automation_policies',
),
migrations.RemoveField(
model_name='user',
name='can_manage_autotasks',
),
migrations.RemoveField(
model_name='user',
name='can_manage_checks',
),
migrations.RemoveField(
model_name='user',
name='can_manage_clients',
),
migrations.RemoveField(
model_name='user',
name='can_manage_deployments',
),
migrations.RemoveField(
model_name='user',
name='can_manage_notes',
),
migrations.RemoveField(
model_name='user',
name='can_manage_pendingactions',
),
migrations.RemoveField(
model_name='user',
name='can_manage_procs',
),
migrations.RemoveField(
model_name='user',
name='can_manage_scripts',
),
migrations.RemoveField(
model_name='user',
name='can_manage_sites',
),
migrations.RemoveField(
model_name='user',
name='can_manage_software',
),
migrations.RemoveField(
model_name='user',
name='can_manage_winsvcs',
),
migrations.RemoveField(
model_name='user',
name='can_manage_winupdates',
),
migrations.RemoveField(
model_name='user',
name='can_reboot_agents',
),
migrations.RemoveField(
model_name='user',
name='can_run_autotasks',
),
migrations.RemoveField(
model_name='user',
name='can_run_bulk',
),
migrations.RemoveField(
model_name='user',
name='can_run_checks',
),
migrations.RemoveField(
model_name='user',
name='can_run_scripts',
),
migrations.RemoveField(
model_name='user',
name='can_send_cmd',
),
migrations.RemoveField(
model_name='user',
name='can_uninstall_agents',
),
migrations.RemoveField(
model_name='user',
name='can_update_agents',
),
migrations.RemoveField(
model_name='user',
name='can_use_mesh',
),
migrations.RemoveField(
model_name='user',
name='can_view_auditlogs',
),
migrations.RemoveField(
model_name='user',
name='can_view_debuglogs',
),
migrations.RemoveField(
model_name='user',
name='can_view_eventlogs',
),
]

View File

@@ -0,0 +1,25 @@
# Generated by Django 3.2.1 on 2021-05-11 02:33
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("accounts", "0018_auto_20210511_0233"),
]
operations = [
migrations.AddField(
model_name="user",
name="role",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="roles",
to="accounts.role",
),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.1 on 2021-05-11 17:37
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0019_user_role'),
]
operations = [
migrations.AddField(
model_name='role',
name='can_manage_roles',
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.4 on 2021-06-17 04:29
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0020_role_can_manage_roles'),
]
operations = [
migrations.AddField(
model_name='role',
name='can_view_core_settings',
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.4 on 2021-06-28 05:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0021_role_can_view_core_settings'),
]
operations = [
migrations.AddField(
model_name='user',
name='clear_search_when_switching',
field=models.BooleanField(default=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.4 on 2021-06-30 03:22
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0022_user_clear_search_when_switching'),
]
operations = [
migrations.AddField(
model_name='user',
name='is_installer_user',
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.1 on 2021-07-20 20:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0023_user_is_installer_user'),
]
operations = [
migrations.AddField(
model_name='user',
name='last_login_ip',
field=models.GenericIPAddressField(blank=True, default=None, null=True),
),
]

View File

@@ -0,0 +1,33 @@
# Generated by Django 3.2.1 on 2021-07-21 04:24
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0024_user_last_login_ip'),
]
operations = [
migrations.AddField(
model_name='role',
name='created_by',
field=models.CharField(blank=True, max_length=100, null=True),
),
migrations.AddField(
model_name='role',
name='created_time',
field=models.DateTimeField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='role',
name='modified_by',
field=models.CharField(blank=True, max_length=100, null=True),
),
migrations.AddField(
model_name='role',
name='modified_time',
field=models.DateTimeField(auto_now=True, null=True),
),
]

View File

@@ -0,0 +1,34 @@
# Generated by Django 3.2.6 on 2021-09-01 12:47
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0025_auto_20210721_0424'),
]
operations = [
migrations.CreateModel(
name='APIKey',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_by', models.CharField(blank=True, max_length=100, null=True)),
('created_time', models.DateTimeField(auto_now_add=True, null=True)),
('modified_by', models.CharField(blank=True, max_length=100, null=True)),
('modified_time', models.DateTimeField(auto_now=True, null=True)),
('name', models.CharField(max_length=25, unique=True)),
('key', models.CharField(blank=True, max_length=48, unique=True)),
('expiration', models.DateTimeField(blank=True, default=None, null=True)),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='role',
name='can_manage_api_keys',
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,25 @@
# Generated by Django 3.2.6 on 2021-09-03 00:54
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('accounts', '0026_auto_20210901_1247'),
]
operations = [
migrations.AddField(
model_name='apikey',
name='user',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='api_key', to='accounts.user'),
preserve_default=False,
),
migrations.AddField(
model_name='user',
name='block_dashboard_login',
field=models.BooleanField(default=False),
),
]

View 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'),
),
]

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.db.models.fields import CharField, DateTimeField
from logs.models import BaseAuditModel
@@ -7,6 +8,7 @@ AGENT_DBLCLICK_CHOICES = [
("editagent", "Edit Agent"),
("takecontrol", "Take Control"),
("remotebg", "Remote Background"),
("urlaction", "URL Action"),
]
AGENT_TBL_TAB_CHOICES = [
@@ -23,12 +25,20 @@ CLIENT_TREE_SORT_CHOICES = [
class User(AbstractUser, BaseAuditModel):
is_active = models.BooleanField(default=True)
block_dashboard_login = models.BooleanField(default=False)
totp_key = models.CharField(max_length=50, null=True, blank=True)
dark_mode = models.BooleanField(default=True)
show_community_scripts = models.BooleanField(default=True)
agent_dblclick_action = models.CharField(
max_length=50, choices=AGENT_DBLCLICK_CHOICES, default="editagent"
)
url_action = models.ForeignKey(
"core.URLAction",
related_name="user",
null=True,
blank=True,
on_delete=models.SET_NULL,
)
default_agent_tbl_tab = models.CharField(
max_length=50, choices=AGENT_TBL_TAB_CHOICES, default="server"
)
@@ -38,6 +48,9 @@ class User(AbstractUser, BaseAuditModel):
)
client_tree_splitter = models.PositiveIntegerField(default=11)
loading_bar_color = models.CharField(max_length=255, default="red")
clear_search_when_switching = models.BooleanField(default=True)
is_installer_user = models.BooleanField(default=False)
last_login_ip = models.GenericIPAddressField(default=None, blank=True, null=True)
agent = models.OneToOneField(
"agents.Agent",
@@ -47,9 +60,141 @@ class User(AbstractUser, BaseAuditModel):
on_delete=models.CASCADE,
)
role = models.ForeignKey(
"accounts.Role",
null=True,
blank=True,
related_name="users",
on_delete=models.SET_NULL,
)
@staticmethod
def serialize(user):
# serializes the task and returns json
from .serializers import UserSerializer
return UserSerializer(user).data
class Role(BaseAuditModel):
name = models.CharField(max_length=255, unique=True)
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)
can_edit_agent = models.BooleanField(default=False)
can_manage_procs = models.BooleanField(default=False)
can_view_eventlogs = models.BooleanField(default=False)
can_send_cmd = models.BooleanField(default=False)
can_reboot_agents = models.BooleanField(default=False)
can_install_agents = models.BooleanField(default=False)
can_run_scripts = models.BooleanField(default=False)
can_run_bulk = models.BooleanField(default=False)
can_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):
return self.name
@staticmethod
def serialize(role):
# serializes the agent and returns json
from .serializers import RoleAuditSerializer
return RoleAuditSerializer(role).data
class APIKey(BaseAuditModel):
name = CharField(unique=True, max_length=25)
key = CharField(unique=True, blank=True, max_length=48)
expiration = DateTimeField(blank=True, null=True, default=None)
user = models.ForeignKey(
"accounts.User",
related_name="api_key",
on_delete=models.CASCADE,
)
@staticmethod
def serialize(apikey):
from .serializers import APIKeyAuditSerializer
return APIKeyAuditSerializer(apikey).data

View File

@@ -0,0 +1,43 @@
from rest_framework import permissions
from tacticalrmm.permissions import _has_perm
class AccountsPerms(permissions.BasePermission):
def has_permission(self, r, view):
if r.method == "GET":
return _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/"]
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
return _has_perm(r, "can_manage_accounts")
class RolesPerms(permissions.BasePermission):
def has_permission(self, r, view):
if r.method == "GET":
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")

View File

@@ -1,7 +1,11 @@
import pyotp
from rest_framework.serializers import ModelSerializer, SerializerMethodField
from rest_framework.serializers import (
ModelSerializer,
SerializerMethodField,
ReadOnlyField,
)
from .models import User
from .models import APIKey, User, Role
class UserUISerializer(ModelSerializer):
@@ -11,17 +15,20 @@ class UserUISerializer(ModelSerializer):
"dark_mode",
"show_community_scripts",
"agent_dblclick_action",
"url_action",
"default_agent_tbl_tab",
"client_tree_sort",
"client_tree_splitter",
"loading_bar_color",
"clear_search_when_switching",
"block_dashboard_login",
]
class UserSerializer(ModelSerializer):
class Meta:
model = User
fields = (
fields = [
"id",
"username",
"first_name",
@@ -29,7 +36,10 @@ class UserSerializer(ModelSerializer):
"email",
"is_active",
"last_login",
)
"last_login_ip",
"role",
"block_dashboard_login",
]
class TOTPSetupSerializer(ModelSerializer):
@@ -48,3 +58,41 @@ class TOTPSetupSerializer(ModelSerializer):
return pyotp.totp.TOTP(obj.totp_key).provisioning_uri(
obj.username, issuer_name="Tactical RMM"
)
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:
model = Role
fields = "__all__"
class APIKeySerializer(ModelSerializer):
username = ReadOnlyField(source="user.username")
class Meta:
model = APIKey
fields = "__all__"
class APIKeyAuditSerializer(ModelSerializer):
username = ReadOnlyField(source="user.username")
class Meta:
model = APIKey
fields = [
"name",
"username",
"expiration",
]

View File

@@ -1,10 +1,12 @@
from unittest.mock import patch
from django.test import override_settings
from accounts.models import User
from model_bakery import baker, seq
from accounts.models import User, APIKey
from tacticalrmm.test import TacticalTestCase
from accounts.serializers import APIKeySerializer
class TestAccounts(TacticalTestCase):
def setUp(self):
@@ -25,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()
@@ -39,6 +41,12 @@ class TestAccounts(TacticalTestCase):
self.assertEqual(r.status_code, 200)
self.assertEqual(r.data, "ok")
# test user set to block dashboard logins
self.bob.block_dashboard_login = True
self.bob.save()
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 400)
@patch("pyotp.TOTP.verify")
def test_login_view(self, mock_verify):
url = "/login/"
@@ -53,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"}
@@ -280,6 +288,7 @@ class TestUserAction(TacticalTestCase):
"client_tree_sort": "alpha",
"client_tree_splitter": 14,
"loading_bar_color": "green",
"clear_search_when_switching": False,
}
r = self.client.patch(url, data, format="json")
self.assertEqual(r.status_code, 200)
@@ -287,6 +296,68 @@ class TestUserAction(TacticalTestCase):
self.check_not_authenticated("patch", url)
class TestAPIKeyViews(TacticalTestCase):
def setUp(self):
self.setup_coresettings()
self.authenticate()
def test_get_api_keys(self):
url = "/accounts/apikeys/"
apikeys = baker.make("accounts.APIKey", key=seq("APIKEY"), _quantity=3)
serializer = APIKeySerializer(apikeys, many=True)
resp = self.client.get(url, format="json")
self.assertEqual(resp.status_code, 200)
self.assertEqual(serializer.data, resp.data) # type: ignore
self.check_not_authenticated("get", url)
def test_add_api_keys(self):
url = "/accounts/apikeys/"
user = baker.make("accounts.User")
data = {"name": "Name", "user": user.id, "expiration": None}
resp = self.client.post(url, data, format="json")
self.assertEqual(resp.status_code, 200)
self.assertTrue(APIKey.objects.filter(name="Name").exists())
self.assertTrue(APIKey.objects.get(name="Name").key)
self.check_not_authenticated("post", url)
def test_modify_api_key(self):
# test a call where api key doesn't exist
resp = self.client.put("/accounts/apikeys/500/", format="json")
self.assertEqual(resp.status_code, 404)
apikey = baker.make("accounts.APIKey", name="Test")
url = f"/accounts/apikeys/{apikey.pk}/" # type: ignore
data = {"name": "New Name"} # type: ignore
resp = self.client.put(url, data, format="json")
self.assertEqual(resp.status_code, 200)
apikey = APIKey.objects.get(pk=apikey.pk) # type: ignore
self.assertEquals(apikey.name, "New Name")
self.check_not_authenticated("put", url)
def test_delete_api_key(self):
# test a call where api key doesn't exist
resp = self.client.delete("/accounts/apikeys/500/", format="json")
self.assertEqual(resp.status_code, 404)
# test delete api key
apikey = baker.make("accounts.APIKey")
url = f"/accounts/apikeys/{apikey.pk}/" # type: ignore
resp = self.client.delete(url, format="json")
self.assertEqual(resp.status_code, 200)
self.assertFalse(APIKey.objects.filter(pk=apikey.pk).exists()) # type: ignore
self.check_not_authenticated("delete", url)
class TestTOTPSetup(TacticalTestCase):
def setUp(self):
self.authenticate()
@@ -312,3 +383,29 @@ class TestTOTPSetup(TacticalTestCase):
r = self.client.post(url)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.data, "totp token already set")
class TestAPIAuthentication(TacticalTestCase):
def setUp(self):
# create User and associate to API Key
self.user = User.objects.create(username="api_user", is_superuser=True)
self.api_key = APIKey.objects.create(
name="Test Token", key="123456", user=self.user
)
self.client_setup()
def test_api_auth(self):
url = "/clients/"
# auth should fail if no header set
self.check_not_authenticated("get", url)
# invalid api key in header should return code 400
self.client.credentials(HTTP_X_API_KEY="000000")
r = self.client.get(url, format="json")
self.assertEqual(r.status_code, 401)
# valid api key in header should return code 200
self.client.credentials(HTTP_X_API_KEY="123456")
r = self.client.get(url, format="json")
self.assertEqual(r.status_code, 200)

View File

@@ -9,4 +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("roles/", views.GetAddRoles.as_view()),
path("roles/<int:pk>/", views.GetUpdateDeleteRole.as_view()),
path("apikeys/", views.GetAddAPIKeys.as_view()),
path("apikeys/<int:pk>/", views.GetUpdateDeleteAPIKey.as_view()),
]

View File

@@ -3,18 +3,25 @@ from django.conf import settings
from django.contrib.auth import login
from django.db import IntegrityError
from django.shortcuts import get_object_or_404
from ipware import get_client_ip
from knox.views import LoginView as KnoxLoginView
from logs.models import AuditLog
from rest_framework import status
from rest_framework.authtoken.serializers import AuthTokenSerializer
from rest_framework.permissions import AllowAny
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from logs.models import AuditLog
from tacticalrmm.utils import notify_error
from .models import User
from .serializers import TOTPSetupSerializer, UserSerializer, UserUISerializer
from .models import APIKey, Role, User
from .permissions import APIKeyPerms, AccountsPerms, RolesPerms
from .serializers import (
APIKeySerializer,
RoleSerializer,
TOTPSetupSerializer,
UserSerializer,
UserUISerializer,
)
def _is_root_user(request, user) -> bool:
@@ -34,11 +41,16 @@ class CheckCreds(KnoxLoginView):
# check credentials
serializer = AuthTokenSerializer(data=request.data)
if not serializer.is_valid():
AuditLog.audit_user_failed_login(request.data["username"])
return Response("bad credentials", status=status.HTTP_400_BAD_REQUEST)
AuditLog.audit_user_failed_login(
request.data["username"], debug_info={"ip": request._client_ip}
)
return notify_error("Bad credentials")
user = serializer.validated_data["user"]
if user.block_dashboard_login:
return notify_error("Bad credentials")
# if totp token not set modify response to notify frontend
if not user.totp_key:
login(request, user)
@@ -60,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)
@@ -70,16 +85,35 @@ class LoginView(KnoxLoginView):
if valid:
login(request, user)
AuditLog.audit_user_login_successful(request.data["username"])
# save ip information
client_ip, is_routable = get_client_ip(request)
user.last_login_ip = client_ip
user.save()
AuditLog.audit_user_login_successful(
request.data["username"], debug_info={"ip": request._client_ip}
)
return super(LoginView, self).post(request, format=None)
else:
AuditLog.audit_user_failed_twofactor(request.data["username"])
return Response("bad credentials", status=status.HTTP_400_BAD_REQUEST)
AuditLog.audit_user_failed_twofactor(
request.data["username"], debug_info={"ip": request._client_ip}
)
return notify_error("Bad credentials")
class GetAddUsers(APIView):
permission_classes = [IsAuthenticated, AccountsPerms]
def get(self, request):
users = User.objects.filter(agent=None)
search = request.GET.get("search", None)
if search:
users = User.objects.filter(agent=None, is_installer_user=False).filter(
username__icontains=search
)
else:
users = User.objects.filter(agent=None, is_installer_user=False)
return Response(UserSerializer(users, many=True).data)
@@ -96,15 +130,21 @@ class GetAddUsers(APIView):
f"ERROR: User {request.data['username']} already exists!"
)
user.first_name = request.data["first_name"]
user.last_name = request.data["last_name"]
# Can be changed once permissions and groups are introduced
user.is_superuser = True
if "first_name" in request.data.keys():
user.first_name = request.data["first_name"]
if "last_name" in request.data.keys():
user.last_name = request.data["last_name"]
if "role" in request.data.keys() and isinstance(request.data["role"], int):
role = get_object_or_404(Role, pk=request.data["role"])
user.role = role
user.save()
return Response(user.username)
class GetUpdateDeleteUser(APIView):
permission_classes = [IsAuthenticated, AccountsPerms]
def get(self, request, pk):
user = get_object_or_404(User, pk=pk)
@@ -133,7 +173,7 @@ class GetUpdateDeleteUser(APIView):
class UserActions(APIView):
permission_classes = [IsAuthenticated, AccountsPerms]
# reset password
def post(self, request):
user = get_object_or_404(User, pk=request.data["id"])
@@ -182,3 +222,76 @@ class UserUI(APIView):
serializer.is_valid(raise_exception=True)
serializer.save()
return Response("ok")
class GetAddRoles(APIView):
permission_classes = [IsAuthenticated, RolesPerms]
def get(self, request):
roles = Role.objects.all()
return Response(RoleSerializer(roles, many=True).data)
def post(self, request):
serializer = RoleSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response("Role was added")
class GetUpdateDeleteRole(APIView):
permission_classes = [IsAuthenticated, RolesPerms]
def get(self, request, pk):
role = get_object_or_404(Role, pk=pk)
return Response(RoleSerializer(role).data)
def put(self, request, pk):
role = get_object_or_404(Role, pk=pk)
serializer = RoleSerializer(instance=role, data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response("Role was edited")
def delete(self, request, pk):
role = get_object_or_404(Role, pk=pk)
role.delete()
return Response("Role was removed")
class GetAddAPIKeys(APIView):
permission_classes = [IsAuthenticated, APIKeyPerms]
def get(self, request):
apikeys = APIKey.objects.all()
return Response(APIKeySerializer(apikeys, many=True).data)
def post(self, request):
# generate a random API Key
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()
return Response("The API Key was added")
class GetUpdateDeleteAPIKey(APIView):
permission_classes = [IsAuthenticated, APIKeyPerms]
def put(self, request, pk):
apikey = get_object_or_404(APIKey, pk=pk)
# remove API key is present in request data
if "key" in request.data.keys():
request.data.pop("key")
serializer = APIKeySerializer(instance=apikey, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response("The API Key was edited")
def delete(self, request, pk):
apikey = get_object_or_404(APIKey, pk=pk)
apikey.delete()
return Response("The API Key was deleted")

View File

@@ -1,8 +1,9 @@
from django.contrib import admin
from .models import Agent, AgentCustomField, Note, RecoveryAction
from .models import Agent, AgentCustomField, Note, RecoveryAction, AgentHistory
admin.site.register(Agent)
admin.site.register(RecoveryAction)
admin.site.register(Note)
admin.site.register(AgentCustomField)
admin.site.register(AgentHistory)

View File

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

View File

@@ -0,0 +1,81 @@
import asyncio
from django.core.management.base import BaseCommand
from django.utils import timezone as djangotime
from packaging import version as pyver
from agents.models import Agent
from tacticalrmm.utils import AGENT_DEFER, reload_nats
class Command(BaseCommand):
help = "Delete old agents"
def add_arguments(self, parser):
parser.add_argument(
"--days",
type=int,
help="Delete agents that have not checked in for this many days",
)
parser.add_argument(
"--agentver",
type=str,
help="Delete agents that equal to or less than this version",
)
parser.add_argument(
"--delete",
action="store_true",
help="This will delete agents",
)
def handle(self, *args, **kwargs):
days = kwargs["days"]
agentver = kwargs["agentver"]
delete = kwargs["delete"]
if not days and not agentver:
self.stdout.write(
self.style.ERROR("Must have at least one parameter: days or agentver")
)
return
q = Agent.objects.defer(*AGENT_DEFER)
agents = []
if days:
overdue = djangotime.now() - djangotime.timedelta(days=days)
agents = [i for i in q if i.last_seen < overdue]
if agentver:
agents = [i for i in q if pyver.parse(i.version) <= pyver.parse(agentver)]
if not agents:
self.stdout.write(self.style.ERROR("No agents matched"))
return
deleted_count = 0
for agent in agents:
s = f"{agent.hostname} | Version {agent.version} | Last Seen {agent.last_seen} | {agent.client} > {agent.site}"
if delete:
s = "Deleting " + s
self.stdout.write(self.style.SUCCESS(s))
asyncio.run(agent.nats_cmd({"func": "uninstall"}, wait=False))
try:
agent.delete()
except Exception as e:
err = f"Failed to delete agent {agent.hostname}: {str(e)}"
self.stdout.write(self.style.ERROR(err))
else:
deleted_count += 1
else:
self.stdout.write(self.style.WARNING(s))
if delete:
reload_nats()
self.stdout.write(self.style.SUCCESS(f"Deleted {deleted_count} agents"))
else:
self.stdout.write(
self.style.SUCCESS(
"The above agents would be deleted. Run again with --delete to actually delete them."
)
)

View File

@@ -0,0 +1,25 @@
from django.conf import settings
from django.core.management.base import BaseCommand
from packaging import version as pyver
from agents.models import Agent
from core.models import CoreSettings
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):
core = CoreSettings.objects.first()
if not core.agent_auto_update: # type: ignore
return
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)

View File

@@ -0,0 +1,23 @@
# Generated by Django 3.2.4 on 2021-06-27 00:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('agents', '0036_agent_block_policy_inheritance'),
]
operations = [
migrations.AddField(
model_name='agent',
name='has_patches_pending',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='agent',
name='pending_actions_count',
field=models.PositiveIntegerField(default=0),
),
]

View File

@@ -0,0 +1,27 @@
# Generated by Django 3.2.1 on 2021-07-06 02:01
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('agents', '0037_auto_20210627_0014'),
]
operations = [
migrations.CreateModel(
name='AgentHistory',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('time', models.DateTimeField(auto_now_add=True)),
('type', models.CharField(choices=[('task_run', 'Task Run'), ('script_run', 'Script Run'), ('cmd_run', 'CMD Run')], default='cmd_run', max_length=50)),
('command', models.TextField(blank=True, null=True)),
('status', models.CharField(choices=[('success', 'Success'), ('failure', 'Failure')], default='success', max_length=50)),
('username', models.CharField(default='system', max_length=50)),
('results', models.TextField(blank=True, null=True)),
('agent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='history', to='agents.agent')),
],
),
]

View File

@@ -0,0 +1,25 @@
# Generated by Django 3.2.5 on 2021-07-14 07:38
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('scripts', '0008_script_guid'),
('agents', '0038_agenthistory'),
]
operations = [
migrations.AddField(
model_name='agenthistory',
name='script',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='history', to='scripts.script'),
),
migrations.AddField(
model_name='agenthistory',
name='script_results',
field=models.JSONField(blank=True, null=True),
),
]

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

View File

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

View File

@@ -16,17 +16,18 @@ from django.conf import settings
from django.contrib.postgres.fields import ArrayField
from django.db import models
from django.utils import timezone as djangotime
from loguru import logger
from nats.aio.client import Client as NATS
from nats.aio.errors import ErrTimeout
from packaging import version as pyver
from core.models import TZ_CHOICES, CoreSettings
from logs.models import BaseAuditModel
logger.configure(**settings.LOG_CONFIG)
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)
@@ -35,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)
@@ -64,6 +65,8 @@ class Agent(BaseAuditModel):
)
maintenance_mode = models.BooleanField(default=False)
block_policy_inheritance = models.BooleanField(default=False)
pending_actions_count = models.PositiveIntegerField(default=0)
has_patches_pending = models.BooleanField(default=False)
alert_template = models.ForeignKey(
"alerts.AlertTemplate",
related_name="agents",
@@ -87,22 +90,24 @@ class Agent(BaseAuditModel):
)
def save(self, *args, **kwargs):
from automation.tasks import generate_agent_checks_task
# get old agent if exists
old_agent = type(self).objects.get(pk=self.pk) if self.pk else None
super(BaseAuditModel, self).save(*args, **kwargs)
old_agent = Agent.objects.get(pk=self.pk) if self.pk else None
super(Agent, self).save(old_model=old_agent, *args, **kwargs)
# check if new agent has been created
# or check if policy have changed on agent
# or if site has changed on agent and if so generate-policies
# or if site has changed on agent and if so generate policies
# or if agent was changed from server or workstation
if (
not old_agent
or (old_agent and old_agent.policy != self.policy)
or (old_agent.site != self.site)
or (old_agent.monitoring_type != self.monitoring_type)
or (old_agent.block_policy_inheritance != self.block_policy_inheritance)
):
self.generate_checks_from_policies()
self.generate_tasks_from_policies()
generate_agent_checks_task.delay(agents=[self.pk], create_tasks=True)
def __str__(self):
return self.hostname
@@ -119,7 +124,7 @@ class Agent(BaseAuditModel):
else:
from core.models import CoreSettings
return CoreSettings.objects.first().default_time_zone
return CoreSettings.objects.first().default_time_zone # type: ignore
@property
def arch(self):
@@ -161,10 +166,6 @@ class Agent(BaseAuditModel):
else:
return "offline"
@property
def has_patches_pending(self):
return self.winupdates.filter(action="approve").filter(installed=False).exists() # type: ignore
@property
def checks(self):
total, passing, failing, warning, info = 0, 0, 0, 0, 0
@@ -263,6 +264,11 @@ class Agent(BaseAuditModel):
make = [x["Manufacturer"] for x in mobo if "Manufacturer" in x][0]
model = [x["Product"] for x in mobo if "Product" in x][0]
if make.lower() == "lenovo":
sysfam = [x["SystemFamily"] for x in comp_sys if "SystemFamily" in x][0]
if "to be filled" not in sysfam.lower():
model = sysfam
return f"{make} {model}"
except:
pass
@@ -320,6 +326,7 @@ class Agent(BaseAuditModel):
full: bool = False,
wait: bool = False,
run_on_any: bool = False,
history_pk: int = 0,
) -> Any:
from scripts.models import Script
@@ -338,6 +345,9 @@ class Agent(BaseAuditModel):
},
}
if history_pk != 0 and pyver.parse(self.version) >= pyver.parse("1.6.0"):
data["id"] = history_pk
running_agent = self
if run_on_any:
nats_ping = {"func": "ping"}
@@ -440,8 +450,8 @@ class Agent(BaseAuditModel):
# if patch policy still doesn't exist check default policy
elif (
core_settings.server_policy
and core_settings.server_policy.winupdatepolicy.exists()
core_settings.server_policy # type: ignore
and core_settings.server_policy.winupdatepolicy.exists() # type: ignore
):
# make sure agent site and client are not blocking inheritance
if (
@@ -449,7 +459,7 @@ class Agent(BaseAuditModel):
and not site.block_policy_inheritance
and not site.client.block_policy_inheritance
):
patch_policy = core_settings.server_policy.winupdatepolicy.get()
patch_policy = core_settings.server_policy.winupdatepolicy.get() # type: ignore
elif self.monitoring_type == "workstation":
# check agent policy first which should override client or site policy
@@ -478,8 +488,8 @@ class Agent(BaseAuditModel):
# if patch policy still doesn't exist check default policy
elif (
core_settings.workstation_policy
and core_settings.workstation_policy.winupdatepolicy.exists()
core_settings.workstation_policy # type: ignore
and core_settings.workstation_policy.winupdatepolicy.exists() # type: ignore
):
# make sure agent site and client are not blocking inheritance
if (
@@ -488,7 +498,7 @@ class Agent(BaseAuditModel):
and not site.client.block_policy_inheritance
):
patch_policy = (
core_settings.workstation_policy.winupdatepolicy.get()
core_settings.workstation_policy.winupdatepolicy.get() # type: ignore
)
# if policy still doesn't exist return the agent patch policy
@@ -603,35 +613,35 @@ class Agent(BaseAuditModel):
# check if alert template is applied globally and return
if (
core.alert_template
and core.alert_template.is_active
core.alert_template # type: ignore
and core.alert_template.is_active # type: ignore
and not self.block_policy_inheritance
and not site.block_policy_inheritance
and not client.block_policy_inheritance
):
templates.append(core.alert_template)
templates.append(core.alert_template) # type: ignore
# if agent is a workstation, check if policy with alert template is assigned to the site, client, or core
if (
self.monitoring_type == "server"
and core.server_policy
and core.server_policy.alert_template
and core.server_policy.alert_template.is_active
and core.server_policy # type: ignore
and core.server_policy.alert_template # type: ignore
and core.server_policy.alert_template.is_active # type: ignore
and not self.block_policy_inheritance
and not site.block_policy_inheritance
and not client.block_policy_inheritance
):
templates.append(core.server_policy.alert_template)
templates.append(core.server_policy.alert_template) # type: ignore
if (
self.monitoring_type == "workstation"
and core.workstation_policy
and core.workstation_policy.alert_template
and core.workstation_policy.alert_template.is_active
and core.workstation_policy # type: ignore
and core.workstation_policy.alert_template # type: ignore
and core.workstation_policy.alert_template.is_active # type: ignore
and not self.block_policy_inheritance
and not site.block_policy_inheritance
and not client.block_policy_inheritance
):
templates.append(core.workstation_policy.alert_template)
templates.append(core.workstation_policy.alert_template) # type: ignore
# go through the templates and return the first one that isn't excluded
for template in templates:
@@ -692,7 +702,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)
@@ -734,8 +744,8 @@ class Agent(BaseAuditModel):
try:
ret = msgpack.loads(msg.data) # type: ignore
except Exception as e:
logger.error(e)
ret = str(e)
DebugLog.error(agent=self, log_type="agent_issues", message=ret)
await nc.close()
return ret
@@ -747,12 +757,9 @@ class Agent(BaseAuditModel):
@staticmethod
def serialize(agent):
# serializes the agent and returns json
from .serializers import AgentEditSerializer
from .serializers import AgentAuditSerializer
ret = AgentEditSerializer(agent).data
del ret["all_timezones"]
del ret["client"]
return ret
return AgentAuditSerializer(agent).data
def delete_superseded_updates(self):
try:
@@ -767,7 +774,7 @@ class Agent(BaseAuditModel):
# skip if no version info is available therefore nothing to parse
try:
vers = [
re.search(r"\(Version(.*?)\)", i).group(1).strip()
re.search(r"\(Version(.*?)\)", i).group(1).strip() # type: ignore
for i in titles
]
sorted_vers = sorted(vers, key=LooseVersion)
@@ -802,7 +809,7 @@ class Agent(BaseAuditModel):
from core.models import CoreSettings
CORE = CoreSettings.objects.first()
CORE.send_mail(
CORE.send_mail( # type: ignore
f"{self.client.name}, {self.site.name}, {self.hostname} - data overdue",
(
f"Data has not been received from client {self.client.name}, "
@@ -817,7 +824,7 @@ class Agent(BaseAuditModel):
from core.models import CoreSettings
CORE = CoreSettings.objects.first()
CORE.send_mail(
CORE.send_mail( # type: ignore
f"{self.client.name}, {self.site.name}, {self.hostname} - data received",
(
f"Data has been received from client {self.client.name}, "
@@ -832,7 +839,7 @@ class Agent(BaseAuditModel):
from core.models import CoreSettings
CORE = CoreSettings.objects.first()
CORE.send_sms(
CORE.send_sms( # type: ignore
f"{self.client.name}, {self.site.name}, {self.hostname} - data overdue",
alert_template=self.alert_template,
)
@@ -841,7 +848,7 @@ class Agent(BaseAuditModel):
from core.models import CoreSettings
CORE = CoreSettings.objects.first()
CORE.send_sms(
CORE.send_sms( # type: ignore
f"{self.client.name}, {self.site.name}, {self.hostname} - data received",
alert_template=self.alert_template,
)
@@ -857,6 +864,8 @@ RECOVERY_CHOICES = [
class RecoveryAction(models.Model):
objects = PermissionQuerySet.as_manager()
agent = models.ForeignKey(
Agent,
related_name="recoveryactions",
@@ -871,6 +880,8 @@ class RecoveryAction(models.Model):
class Note(models.Model):
objects = PermissionQuerySet.as_manager()
agent = models.ForeignKey(
Agent,
related_name="notes",
@@ -891,6 +902,8 @@ class Note(models.Model):
class AgentCustomField(models.Model):
objects = PermissionQuerySet.as_manager()
agent = models.ForeignKey(
Agent,
related_name="custom_fields",
@@ -923,3 +936,59 @@ class AgentCustomField(models.Model):
return self.bool_value
else:
return self.string_value
def save_to_field(self, value):
if self.field.type in [
"text",
"number",
"single",
"datetime",
]:
self.string_value = value
self.save()
elif self.field.type == "multiple":
self.multiple_value = value.split(",")
self.save()
elif self.field.type == "checkbox":
self.bool_value = bool(value)
self.save()
AGENT_HISTORY_TYPES = (
("task_run", "Task Run"),
("script_run", "Script Run"),
("cmd_run", "CMD Run"),
)
AGENT_HISTORY_STATUS = (("success", "Success"), ("failure", "Failure"))
class AgentHistory(models.Model):
objects = PermissionQuerySet.as_manager()
agent = models.ForeignKey(
Agent,
related_name="history",
on_delete=models.CASCADE,
)
time = models.DateTimeField(auto_now_add=True)
type = models.CharField(
max_length=50, choices=AGENT_HISTORY_TYPES, default="cmd_run"
)
command = models.TextField(null=True, blank=True)
status = models.CharField(
max_length=50, choices=AGENT_HISTORY_STATUS, default="success"
)
username = models.CharField(max_length=255, default="system")
results = models.TextField(null=True, blank=True)
script = models.ForeignKey(
"scripts.Script",
null=True,
blank=True,
related_name="history",
on_delete=models.SET_NULL,
)
script_results = models.JSONField(null=True, blank=True)
def __str__(self):
return f"{self.agent.hostname} - {self.type}"

View File

@@ -0,0 +1,123 @@
from rest_framework import permissions
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") and _has_perm_on_agent(
r.user, view.kwargs["agent_id"]
)
class UpdateAgentPerms(permissions.BasePermission):
def has_permission(self, r, view):
return _has_perm(r, "can_update_agents")
class PingAgentPerms(permissions.BasePermission):
def has_permission(self, r, view):
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") 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") 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") 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") and _has_perm_on_agent(
r.user, view.kwargs["agent_id"]
)
class InstallAgentPerms(permissions.BasePermission):
def has_permission(self, r, view):
return _has_perm(r, "can_install_agents")
class RunScriptPerms(permissions.BasePermission):
def has_permission(self, r, view):
return _has_perm(r, "can_run_scripts") and _has_perm_on_agent(
r.user, view.kwargs["agent_id"]
)
class AgentNotesPerms(permissions.BasePermission):
def has_permission(self, r, view):
# 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")

View File

@@ -1,15 +1,30 @@
import pytz
from rest_framework import serializers
from clients.serializers import ClientSerializer
from winupdate.serializers import WinUpdatePolicySerializer
from .models import Agent, AgentCustomField, Note
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
patches_pending = serializers.ReadOnlyField(source="has_patches_pending")
winupdatepolicy = WinUpdatePolicySerializer(many=True, read_only=True)
status = serializers.ReadOnlyField()
cpu_model = serializers.ReadOnlyField()
@@ -20,33 +35,21 @@ 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):
patches_pending = serializers.ReadOnlyField(source="has_patches_pending")
pending_actions = serializers.SerializerMethodField()
status = serializers.ReadOnlyField()
checks = serializers.ReadOnlyField()
last_seen = serializers.SerializerMethodField()
@@ -69,9 +72,6 @@ class AgentTableSerializer(serializers.ModelSerializer):
"always_alert": obj.alert_template.agent_always_alert,
}
def get_pending_actions(self, obj):
return obj.pendingactions.filter(status="pending").count()
def get_last_seen(self, obj) -> str:
if obj.time_zone is not None:
agent_tz = pytz.timezone(obj.time_zone)
@@ -94,17 +94,16 @@ class AgentTableSerializer(serializers.ModelSerializer):
class Meta:
model = Agent
fields = [
"id",
"agent_id",
"alert_template",
"hostname",
"agent_id",
"site_name",
"client_name",
"monitoring_type",
"description",
"needs_reboot",
"patches_pending",
"pending_actions",
"has_patches_pending",
"pending_actions_count",
"status",
"overdue_text_alert",
"overdue_email_alert",
@@ -121,63 +120,7 @@ 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",
"all_timezones",
"winupdatepolicy",
"policy",
"custom_fields",
]
class WinAgentSerializer(serializers.ModelSerializer):
# for the windows agent
patches_pending = serializers.ReadOnlyField(source="has_patches_pending")
winupdatepolicy = WinUpdatePolicySerializer(many=True, read_only=True)
status = serializers.ReadOnlyField()
class Meta:
model = Agent
fields = "__all__"
@@ -190,24 +133,38 @@ 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__"
fields = ("pk", "entry_time", "agent", "user", "note", "username", "agent_id")
extra_kwargs = {"agent": {"write_only": True}, "user": {"write_only": True}}
class NotesSerializer(serializers.ModelSerializer):
notes = NoteSerializer(many=True, read_only=True)
class AgentHistorySerializer(serializers.ModelSerializer):
time = serializers.SerializerMethodField(read_only=True)
script_name = serializers.ReadOnlyField(source="script.name")
class Meta:
model = AgentHistory
fields = "__all__"
def get_time(self, history):
tz = self.context["default_tz"]
return history.time.astimezone(tz).strftime("%m %d %Y %H:%M:%S")
class AgentAuditSerializer(serializers.ModelSerializer):
class Meta:
model = Agent
fields = ["hostname", "pk", "notes"]
exclude = ["disks", "services", "wmi_detail"]

View File

@@ -1,66 +1,60 @@
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 CoreSettings
from django.conf import settings
from django.utils import timezone as djangotime
from loguru import logger
from logs.models import DebugLog, PendingAction
from packaging import version as pyver
from agents.models import Agent
from core.models import CodeSignToken, CoreSettings
from logs.models import PendingAction
from scripts.models import Script
from tacticalrmm.celery import app
from tacticalrmm.utils import run_nats_api_cmd
logger.configure(**settings.LOG_CONFIG)
from agents.models import Agent
from agents.utils import get_winagent_url
from tacticalrmm.utils import AGENT_DEFER
def agent_update(pk: int, codesigntoken: str = None) -> 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"
# skip if we can't determine the arch
if agent.arch is None:
logger.warning(
f"Unable to determine arch on {agent.hostname}. Skipping agent update."
DebugLog.warning(
agent=agent,
log_type="agent_issues",
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
url = get_winagent_url(agent.arch)
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
if agent.pendingactions.filter(
action_type="agentupdate", status="pending"
).exists():
agent.pendingactions.filter(
if not force:
if agent.pendingactions.filter(
action_type="agentupdate", status="pending"
).delete()
).exists():
agent.pendingactions.filter(
action_type="agentupdate", status="pending"
).delete()
PendingAction.objects.create(
agent=agent,
action_type="agentupdate",
details={
"url": url,
"version": version,
"inno": inno,
},
)
PendingAction.objects.create(
agent=agent,
action_type="agentupdate",
details={
"url": url,
"version": version,
"inno": inno,
},
)
nats_data = {
"func": "agentupdate",
@@ -75,16 +69,21 @@ def agent_update(pk: int, codesigntoken: str = None) -> str:
@app.task
def send_agent_update_task(pks: list[int]) -> None:
try:
codesigntoken = CodeSignToken.objects.first().token
except:
codesigntoken = None
chunks = (pks[i : i + 30] for i in range(0, len(pks), 30))
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, codesigntoken)
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(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 agent_id in chunk:
agent_update(agent_id)
sleep(0.05)
sleep(4)
@@ -92,25 +91,20 @@ def send_agent_update_task(pks: list[int]) -> None:
@app.task
def auto_self_agent_update_task() -> None:
core = CoreSettings.objects.first()
if not core.agent_auto_update:
if not core.agent_auto_update: # type:ignore
return
try:
codesigntoken = CodeSignToken.objects.first().token
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)
@@ -195,6 +189,7 @@ def agent_outages_task() -> None:
agents = Agent.objects.only(
"pk",
"agent_id",
"last_seen",
"offline_time",
"overdue_time",
@@ -215,14 +210,24 @@ def run_script_email_results_task(
nats_timeout: int,
emails: list[str],
args: list[str] = [],
history_pk: int = 0,
):
agent = Agent.objects.get(pk=agentpk)
script = Script.objects.get(pk=scriptpk)
r = agent.run_script(
scriptpk=script.pk, args=args, full=True, timeout=nats_timeout, wait=True
scriptpk=script.pk,
args=args,
full=True,
timeout=nats_timeout,
wait=True,
history_pk=history_pk,
)
if r == "timeout":
logger.error(f"{agent.hostname} timed out running script.")
DebugLog.error(
agent=agent,
log_type="scripting",
message=f"{agent.hostname}({agent.pk}) timed out running script.",
)
return
CORE = CoreSettings.objects.first()
@@ -238,43 +243,108 @@ def run_script_email_results_task(
msg = EmailMessage()
msg["Subject"] = subject
msg["From"] = CORE.smtp_from_email
msg["From"] = CORE.smtp_from_email # type:ignore
if emails:
msg["To"] = ", ".join(emails)
else:
msg["To"] = ", ".join(CORE.email_alert_recipients)
msg["To"] = ", ".join(CORE.email_alert_recipients) # type:ignore
msg.set_content(body)
try:
with smtplib.SMTP(CORE.smtp_host, CORE.smtp_port, timeout=20) as server:
if CORE.smtp_requires_auth:
with smtplib.SMTP(
CORE.smtp_host, CORE.smtp_port, timeout=20 # type:ignore
) as server: # type:ignore
if CORE.smtp_requires_auth: # type:ignore
server.ehlo()
server.starttls()
server.login(CORE.smtp_host_user, CORE.smtp_host_password)
server.login(
CORE.smtp_host_user, CORE.smtp_host_password # type:ignore
) # type:ignore
server.send_message(msg)
server.quit()
else:
server.send_message(msg)
server.quit()
except Exception as e:
logger.error(e)
DebugLog.error(message=str(e))
@app.task
def monitor_agents_task() -> None:
agents = Agent.objects.only(
"pk", "agent_id", "last_seen", "overdue_time", "offline_time"
def clear_faults_task(older_than_days: int) -> None:
# https://github.com/wh1te909/tacticalrmm/issues/484
agents = Agent.objects.exclude(last_seen__isnull=True).filter(
last_seen__lt=djangotime.now() - djangotime.timedelta(days=older_than_days)
)
ids = [i.agent_id for i in agents if i.status != "online"]
run_nats_api_cmd("monitor", ids)
for agent in agents:
if agent.agentchecks.exists():
for check in agent.agentchecks.all():
# reset check status
check.status = "passing"
check.save(update_fields=["status"])
if check.alert.filter(resolved=False).exists():
check.alert.get(resolved=False).resolve()
# reset overdue alerts
agent.overdue_email_alert = False
agent.overdue_text_alert = False
agent.overdue_dashboard_alert = False
agent.save(
update_fields=[
"overdue_email_alert",
"overdue_text_alert",
"overdue_dashboard_alert",
]
)
@app.task
def 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)
def prune_agent_history(older_than_days: int) -> str:
from .models import AgentHistory
AgentHistory.objects.filter(
time__lt=djangotime.now() - djangotime.timedelta(days=older_than_days)
).delete()
return "ok"
@app.task
def handle_agents_task() -> None:
q = Agent.objects.defer(*AGENT_DEFER)
agents = [
i
for i in q
if pyver.parse(i.version) >= pyver.parse("1.6.0") and i.status == "online"
]
for agent in agents:
# change agent update pending status to completed if agent has just updated
if (
pyver.parse(agent.version) == pyver.parse(settings.LATEST_AGENT_VER)
and agent.pendingactions.filter(
action_type="agentupdate", status="pending"
).exists()
):
agent.pendingactions.filter(
action_type="agentupdate", status="pending"
).update(status="completed")
# 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()
# handles any alerting actions
if Alert.objects.filter(agent=agent, resolved=False).exists():
try:
Alert.handle_alert_resolve(agent)
except:
continue

File diff suppressed because it is too large Load Diff

View File

@@ -1,32 +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()),
]

View File

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

View File

@@ -3,44 +3,247 @@ import datetime as dt
import os
import random
import string
import time
from django.conf import settings
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from loguru import logger
from django.db.models import Q
from packaging import version as pyver
from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.exceptions import PermissionDenied
from core.models import CoreSettings
from logs.models import AuditLog, PendingAction
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
from .models import Agent, AgentCustomField, Note, RecoveryAction, AgentHistory
from .permissions import (
AgentHistoryPerms,
AgentPerms,
EvtLogPerms,
InstallAgentPerms,
RecoverAgentPerms,
AgentNotesPerms,
ManageProcPerms,
MeshPerms,
RebootAgentPerms,
RunBulkPerms,
RunScriptPerms,
SendCMDPerms,
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
logger.configure(**settings.LOG_CONFIG)
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)
@api_view()
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],
@@ -50,145 +253,49 @@ 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()
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"
r = asyncio.run(agent.nats_cmd({"func": "ping"}, timeout=5))
if r == "pong":
status = "online"
attempts = 0
while 1:
r = asyncio.run(agent.nats_cmd({"func": "ping"}, timeout=2))
if r == "pong":
status = "online"
break
else:
attempts += 1
time.sleep(1)
if attempts >= 5:
break
return Response({"name": agent.hostname, "status": status})
@api_view(["DELETE"])
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"])
def edit_agent(request):
agent = get_object_or_404(Agent, pk=request.data["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("ok")
@api_view()
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}"
)
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"
terminal = f"{core.mesh_site}/?login={token}&gotonode={agent.mesh_node_id}&viewmode=12&hide=31"
file = f"{core.mesh_site}/?login={token}&gotonode={agent.mesh_node_id}&viewmode=13&hide=31"
AuditLog.audit_mesh_session(username=request.user.username, hostname=agent.hostname)
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()
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()
def get_event_log(request, pk, logtype, days):
agent = get_object_or_404(Agent, pk=pk)
@api_view(["GET"])
@permission_classes([IsAuthenticated, EvtLogPerms])
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,
@@ -198,15 +305,16 @@ 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)
@api_view(["POST"])
def send_raw_cmd(request):
agent = get_object_or_404(Agent, pk=request.data["pk"])
@permission_classes([IsAuthenticated, SendCMDPerms])
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",
@@ -216,6 +324,16 @@ def send_raw_cmd(request):
"shell": request.data["shell"],
},
}
if pyver.parse(agent.version) >= pyver.parse("1.6.0"):
hist = AgentHistory.objects.create(
agent=agent,
type="cmd_run",
command=request.data["cmd"],
username=request.user.username[:50],
)
data["id"] = hist.pk
r = asyncio.run(agent.nats_cmd(data, timeout=timeout + 2))
if r == "timeout":
@@ -223,86 +341,20 @@ def send_raw_cmd(request):
AuditLog.audit_raw_command(
username=request.user.username,
hostname=agent.hostname,
agent=agent,
cmd=request.data["cmd"],
shell=request.data["shell"],
debug_info={"ip": request._client_ip},
)
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",
)
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")
@@ -310,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")
@@ -352,8 +404,10 @@ class Reboot(APIView):
@api_view(["POST"])
@permission_classes([IsAuthenticated, InstallAgentPerms])
def install_agent(request):
from knox.models import AuthToken
from accounts.models import User
from agents.utils import get_winagent_url
@@ -362,25 +416,34 @@ 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"
)
download_url = get_winagent_url(arch)
installer_user = User.objects.filter(is_installer_user=True).first()
_, token = AuthToken.objects.create(
user=request.user, expiry=dt.timedelta(hours=request.data["expires"])
user=installer_user, expiry=dt.timedelta(hours=request.data["expires"])
)
if request.data["installMethod"] == "exe":
@@ -469,7 +532,7 @@ def install_agent(request):
try:
os.remove(ps1)
except Exception as e:
logger.error(str(e))
DebugLog.error(message=str(e))
with open(ps1, "w") as f:
f.write(text)
@@ -487,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
@@ -524,28 +588,44 @@ def recover(request):
@api_view(["POST"])
def run_script(request):
agent = get_object_or_404(Agent, pk=request.data["pk"])
script = get_object_or_404(Script, pk=request.data["scriptPK"])
@permission_classes([IsAuthenticated, RunScriptPerms])
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"]
req_timeout = int(request.data["timeout"]) + 3
AuditLog.audit_script_run(
username=request.user.username,
hostname=agent.hostname,
agent=agent,
script=script.name,
debug_info={"ip": request._client_ip},
)
history_pk = 0
if pyver.parse(agent.version) >= pyver.parse("1.6.0"):
hist = AgentHistory.objects.create(
agent=agent,
type="script_run",
script=script,
username=request.user.username[:50],
)
history_pk = hist.pk
if output == "wait":
r = agent.run_script(
scriptpk=script.pk, args=args, timeout=req_timeout, wait=True
scriptpk=script.pk,
args=args,
timeout=req_timeout,
wait=True,
history_pk=history_pk,
)
return Response(r)
elif output == "email":
emails = (
[] if request.data["emailmode"] == "default" else request.data["emails"]
[] if request.data["emailMode"] == "default" else request.data["emails"]
)
run_script_email_results_task.delay(
agentpk=agent.pk,
@@ -554,23 +634,55 @@ def run_script(request):
emails=emails,
args=args,
)
elif output == "collector":
from core.models import CustomField
r = agent.run_script(
scriptpk=script.pk,
args=args,
timeout=req_timeout,
wait=True,
history_pk=history_pk,
)
custom_field = CustomField.objects.get(pk=request.data["custom_field"])
if custom_field.model == "agent":
field = custom_field.get_or_create_field_value(agent)
elif custom_field.model == "client":
field = custom_field.get_or_create_field_value(agent.client)
elif custom_field.model == "site":
field = custom_field.get_or_create_field_value(agent.site)
else:
return notify_error("Custom Field was invalid")
value = (
r.strip()
if request.data["save_all_output"]
else r.strip().split("\n")[-1].strip()
)
field.save_to_field(value)
return Response(r)
elif output == "note":
r = agent.run_script(
scriptpk=script.pk,
args=args,
timeout=req_timeout,
wait=True,
history_pk=history_pk,
)
Note.objects.create(agent=agent, user=request.user, note=r)
return Response(r)
else:
agent.run_script(scriptpk=script.pk, args=args, timeout=req_timeout)
agent.run_script(
scriptpk=script.pk, args=args, timeout=req_timeout, history_pk=history_pk
)
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=45))
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"
@@ -593,49 +705,94 @@ 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, 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!")
@api_view(["POST"])
@permission_classes([IsAuthenticated, RunBulkPerms])
def bulk(request):
if request.data["target"] == "agents" and not request.data["agentPKs"]:
if request.data["target"] == "agents" and not request.data["agents"]:
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["agentPKs"])
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")
@@ -646,60 +803,107 @@ def bulk(request):
agents: list[int] = [agent.pk for agent in q]
AuditLog.audit_bulk_action(request.user, request.data["mode"], request.data)
if not agents:
return notify_error("No agents where found meeting the selected criteria")
AuditLog.audit_bulk_action(
request.user,
request.data["mode"],
request.data,
debug_info={"ip": request._client_ip},
)
if request.data["mode"] == "command":
handle_bulk_command_task.delay(
agents, request.data["cmd"], request.data["shell"], request.data["timeout"]
agents,
request.data["cmd"],
request.data["shell"],
request.data["timeout"],
request.user.username[:50],
run_on_offline=request.data["offlineAgents"],
)
return Response(f"Command will now be run on {len(agents)} agents")
elif request.data["mode"] == "script":
script = get_object_or_404(Script, pk=request.data["scriptPK"])
script = get_object_or_404(Script, pk=request.data["script"])
handle_bulk_script_task.delay(
script.pk, agents, request.data["args"], request.data["timeout"]
script.pk,
agents,
request.data["args"],
request.data["timeout"],
request.user.username[:50],
)
return Response(f"{script.name} will now be run on {len(agents)} agents")
elif request.data["mode"] == "install":
bulk_install_updates_task.delay(agents)
return Response(
f"Pending updates will now be installed on {len(agents)} agents"
)
elif request.data["mode"] == "scan":
bulk_check_for_updates_task.delay(agents)
return Response(f"Patch status scan will now run on {len(agents)} agents")
elif request.data["mode"] == "patch":
if request.data["patchMode"] == "install":
bulk_install_updates_task.delay(agents)
return Response(
f"Pending updates will now be installed on {len(agents)} agents"
)
elif request.data["patchMode"] == "scan":
bulk_check_for_updates_task.delay(agents)
return Response(f"Patch status scan will now run on {len(agents)} agents")
return notify_error("Something went wrong")
@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):
permission_classes = [IsAuthenticated, AgentHistoryPerms]
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)

View File

@@ -0,0 +1,33 @@
# Generated by Django 3.2.1 on 2021-07-21 04:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('alerts', '0006_auto_20210217_1736'),
]
operations = [
migrations.AddField(
model_name='alerttemplate',
name='created_by',
field=models.CharField(blank=True, max_length=100, null=True),
),
migrations.AddField(
model_name='alerttemplate',
name='created_time',
field=models.DateTimeField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='alerttemplate',
name='modified_by',
field=models.CharField(blank=True, max_length=100, null=True),
),
migrations.AddField(
model_name='alerttemplate',
name='modified_time',
field=models.DateTimeField(auto_now=True, null=True),
),
]

View File

@@ -0,0 +1,28 @@
# Generated by Django 3.2.1 on 2021-07-21 17:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('alerts', '0007_auto_20210721_0423'),
]
operations = [
migrations.AddField(
model_name='alerttemplate',
name='agent_script_actions',
field=models.BooleanField(blank=True, default=None, null=True),
),
migrations.AddField(
model_name='alerttemplate',
name='check_script_actions',
field=models.BooleanField(blank=True, default=None, null=True),
),
migrations.AddField(
model_name='alerttemplate',
name='task_script_actions',
field=models.BooleanField(blank=True, default=None, null=True),
),
]

View File

@@ -0,0 +1,28 @@
# Generated by Django 3.2.1 on 2021-07-21 18:10
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('alerts', '0008_auto_20210721_1757'),
]
operations = [
migrations.AlterField(
model_name='alerttemplate',
name='agent_script_actions',
field=models.BooleanField(blank=True, default=True, null=True),
),
migrations.AlterField(
model_name='alerttemplate',
name='check_script_actions',
field=models.BooleanField(blank=True, default=True, null=True),
),
migrations.AlterField(
model_name='alerttemplate',
name='task_script_actions',
field=models.BooleanField(blank=True, default=True, null=True),
),
]

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

View File

@@ -3,19 +3,19 @@ from __future__ import annotations
import re
from typing import TYPE_CHECKING, Union
from django.conf import settings
from django.contrib.postgres.fields import ArrayField
from django.db import models
from django.db.models.fields import BooleanField, PositiveIntegerField
from django.utils import timezone as djangotime
from loguru import logger
from logs.models import BaseAuditModel, DebugLog
from tacticalrmm.models import PermissionQuerySet
if TYPE_CHECKING:
from agents.models import Agent
from autotasks.models import AutomatedTask
from checks.models import Check
logger.configure(**settings.LOG_CONFIG)
SEVERITY_CHOICES = [
("info", "Informational"),
@@ -32,6 +32,8 @@ ALERT_TYPE_CHOICES = [
class Alert(models.Model):
objects = PermissionQuerySet.as_manager()
agent = models.ForeignKey(
"agents.Agent",
related_name="agent",
@@ -173,6 +175,7 @@ class Alert(models.Model):
always_email = alert_template.agent_always_email
always_text = alert_template.agent_always_text
alert_interval = alert_template.agent_periodic_alert_days
run_script_action = alert_template.agent_script_actions
if instance.should_create_alert(alert_template):
alert = cls.create_or_return_availability_alert(instance)
@@ -209,6 +212,7 @@ class Alert(models.Model):
always_email = alert_template.check_always_email
always_text = alert_template.check_always_text
alert_interval = alert_template.check_periodic_alert_days
run_script_action = alert_template.check_script_actions
if instance.should_create_alert(alert_template):
alert = cls.create_or_return_check_alert(instance)
@@ -242,6 +246,7 @@ class Alert(models.Model):
always_email = alert_template.task_always_email
always_text = alert_template.task_always_text
alert_interval = alert_template.task_periodic_alert_days
run_script_action = alert_template.task_script_actions
if instance.should_create_alert(alert_template):
alert = cls.create_or_return_task_alert(instance)
@@ -295,7 +300,7 @@ class Alert(models.Model):
text_task.delay(pk=alert.pk, alert_interval=alert_interval)
# check if any scripts should be run
if alert_template and alert_template.action and not alert.action_run:
if alert_template and alert_template.action and run_script_action and not alert.action_run: # type: ignore
r = agent.run_script(
scriptpk=alert_template.action.pk,
args=alert.parse_script_args(alert_template.action_args),
@@ -314,8 +319,10 @@ class Alert(models.Model):
alert.action_run = djangotime.now()
alert.save()
else:
logger.error(
f"Failure action: {alert_template.action.name} failed to run on any agent for {agent.hostname} failure alert"
DebugLog.error(
agent=agent,
log_type="scripting",
message=f"Failure action: {alert_template.action.name} failed to run on any agent for {agent.hostname}({agent.pk}) failure alert",
)
@classmethod
@@ -345,6 +352,7 @@ class Alert(models.Model):
if alert_template:
email_on_resolved = alert_template.agent_email_on_resolved
text_on_resolved = alert_template.agent_text_on_resolved
run_script_action = alert_template.agent_script_actions
elif isinstance(instance, Check):
from checks.tasks import (
@@ -363,6 +371,7 @@ class Alert(models.Model):
if alert_template:
email_on_resolved = alert_template.check_email_on_resolved
text_on_resolved = alert_template.check_text_on_resolved
run_script_action = alert_template.check_script_actions
elif isinstance(instance, AutomatedTask):
from autotasks.tasks import (
@@ -381,6 +390,7 @@ class Alert(models.Model):
if alert_template:
email_on_resolved = alert_template.task_email_on_resolved
text_on_resolved = alert_template.task_text_on_resolved
run_script_action = alert_template.task_script_actions
else:
return
@@ -403,6 +413,7 @@ class Alert(models.Model):
if (
alert_template
and alert_template.resolved_action
and run_script_action # type: ignore
and not alert.resolved_action_run
):
r = agent.run_script(
@@ -425,8 +436,10 @@ class Alert(models.Model):
alert.resolved_action_run = djangotime.now()
alert.save()
else:
logger.error(
f"Resolved action: {alert_template.action.name} failed to run on any agent for {agent.hostname} resolved alert"
DebugLog.error(
agent=agent,
log_type="scripting",
message=f"Resolved action: {alert_template.action.name} failed to run on any agent for {agent.hostname}({agent.pk}) resolved alert",
)
def parse_script_args(self, args: list[str]):
@@ -443,15 +456,16 @@ class Alert(models.Model):
if match:
name = match.group(1)
if hasattr(self, name):
value = getattr(self, name)
# check if attr exists and isn't a function
if hasattr(self, name) and not callable(getattr(self, name)):
value = f"'{getattr(self, name)}'"
else:
continue
try:
temp_args.append(re.sub("\\{\\{.*\\}\\}", "'" + value + "'", arg)) # type: ignore
temp_args.append(re.sub("\\{\\{.*\\}\\}", value, arg)) # type: ignore
except Exception as e:
logger.error(e)
DebugLog.error(log_type="scripting", message=str(e))
continue
else:
@@ -460,7 +474,7 @@ class Alert(models.Model):
return temp_args
class AlertTemplate(models.Model):
class AlertTemplate(BaseAuditModel):
name = models.CharField(max_length=100)
is_active = models.BooleanField(default=True)
@@ -517,6 +531,7 @@ class AlertTemplate(models.Model):
agent_always_text = BooleanField(null=True, blank=True, default=None)
agent_always_alert = BooleanField(null=True, blank=True, default=None)
agent_periodic_alert_days = PositiveIntegerField(blank=True, null=True, default=0)
agent_script_actions = BooleanField(null=True, blank=True, default=True)
# check alert settings
check_email_alert_severity = ArrayField(
@@ -540,6 +555,7 @@ class AlertTemplate(models.Model):
check_always_text = BooleanField(null=True, blank=True, default=None)
check_always_alert = BooleanField(null=True, blank=True, default=None)
check_periodic_alert_days = PositiveIntegerField(blank=True, null=True, default=0)
check_script_actions = BooleanField(null=True, blank=True, default=True)
# task alert settings
task_email_alert_severity = ArrayField(
@@ -563,6 +579,7 @@ class AlertTemplate(models.Model):
task_always_text = BooleanField(null=True, blank=True, default=None)
task_always_alert = BooleanField(null=True, blank=True, default=None)
task_periodic_alert_days = PositiveIntegerField(blank=True, null=True, default=0)
task_script_actions = BooleanField(null=True, blank=True, default=True)
# exclusion settings
exclude_workstations = BooleanField(null=True, blank=True, default=False)
@@ -581,6 +598,13 @@ class AlertTemplate(models.Model):
def __str__(self):
return self.name
@staticmethod
def serialize(alert_template):
# serializes the agent and returns json
from .serializers import AlertTemplateAuditSerializer
return AlertTemplateAuditSerializer(alert_template).data
@property
def has_agent_settings(self) -> bool:
return (

View File

@@ -0,0 +1,55 @@
from django.shortcuts import get_object_or_404
from rest_framework import permissions
from tacticalrmm.permissions import _has_perm, _has_perm_on_agent
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":
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")
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")

View File

@@ -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,9 +113,15 @@ 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
fields = "__all__"
class AlertTemplateAuditSerializer(ModelSerializer):
class Meta:
model = AlertTemplate
fields = "__all__"

View File

@@ -1,11 +1,10 @@
from django.utils import timezone as djangotime
from alerts.models import Alert
from tacticalrmm.celery import app
@app.task
def unsnooze_alerts() -> str:
from .models import Alert
Alert.objects.filter(snoozed=True, snooze_until__lte=djangotime.now()).update(
snoozed=False, snooze_until=None
@@ -22,3 +21,14 @@ def cache_agents_alert_template():
agent.set_alert_template()
return "ok"
@app.task
def prune_resolved_alerts(older_than_days: int) -> str:
from .models import Alert
Alert.objects.filter(resolved=True).filter(
alert_time__lt=djangotime.now() - djangotime.timedelta(days=older_than_days)
).delete()
return "ok"

View File

@@ -1,14 +1,15 @@
from datetime import datetime, timedelta
from unittest.mock import patch
from itertools import cycle
from core.models import CoreSettings
from django.conf import settings
from django.utils import timezone as djangotime
from model_bakery import baker, seq
from tacticalrmm.test import TacticalTestCase
from alerts.tasks import cache_agents_alert_template
from autotasks.models import AutomatedTask
from core.models import CoreSettings
from tacticalrmm.test import TacticalTestCase
from agents.tasks import handle_agents_task
from .models import Alert, AlertTemplate
from .serializers import (
@@ -17,6 +18,8 @@ from .serializers import (
AlertTemplateSerializer,
)
base_url = "/alerts"
class TestAlertsViews(TacticalTestCase):
def setUp(self):
@@ -24,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")
@@ -117,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 = {
@@ -134,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)
@@ -150,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")
@@ -168,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)
@@ -195,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)
@@ -243,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")
@@ -255,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",
@@ -268,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)
@@ -284,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,
@@ -309,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)
@@ -330,10 +331,10 @@ class TestAlertsViews(TacticalTestCase):
baker.make("clients.Site", alert_template=alert_template, _quantity=3)
baker.make("automation.Policy", alert_template=alert_template)
core = CoreSettings.objects.first()
core.alert_template = alert_template
core.save()
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)
@@ -403,16 +404,16 @@ class TestAlertTasks(TacticalTestCase):
# assign first Alert Template as to a policy and apply it as default
policy.alert_template = alert_templates[0] # type: ignore
policy.save() # type: ignore
core.workstation_policy = policy
core.server_policy = policy
core.save()
core.workstation_policy = policy # type: ignore
core.server_policy = policy # type: ignore
core.save() # type: ignore
self.assertEquals(server.set_alert_template().pk, alert_templates[0].pk) # type: ignore
self.assertEquals(workstation.set_alert_template().pk, alert_templates[0].pk) # type: ignore
# assign second Alert Template to as default alert template
core.alert_template = alert_templates[1] # type: ignore
core.save()
core.save() # type: ignore
self.assertEquals(workstation.set_alert_template().pk, alert_templates[1].pk) # type: ignore
self.assertEquals(server.set_alert_template().pk, alert_templates[1].pk) # type: ignore
@@ -514,6 +515,7 @@ class TestAlertTasks(TacticalTestCase):
agent_recovery_email_task,
agent_recovery_sms_task,
)
from alerts.models import Alert
agent_dashboard_alert = baker.make_recipe("agents.overdue_agent")
@@ -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
@@ -727,7 +718,6 @@ class TestAlertTasks(TacticalTestCase):
send_email,
sleep,
):
from alerts.tasks import cache_agents_alert_template
from checks.models import Check
from checks.tasks import (
handle_check_email_alert_task,
@@ -736,6 +726,8 @@ class TestAlertTasks(TacticalTestCase):
handle_resolved_check_sms_alert_task,
)
from alerts.tasks import cache_agents_alert_template
# create test data
agent = baker.make_recipe("agents.agent")
agent_no_settings = baker.make_recipe("agents.agent")
@@ -1011,7 +1003,6 @@ class TestAlertTasks(TacticalTestCase):
send_email,
sleep,
):
from alerts.tasks import cache_agents_alert_template
from autotasks.models import AutomatedTask
from autotasks.tasks import (
handle_resolved_task_email_alert,
@@ -1020,6 +1011,8 @@ class TestAlertTasks(TacticalTestCase):
handle_task_sms_alert,
)
from alerts.tasks import cache_agents_alert_template
# create test data
agent = baker.make_recipe("agents.agent")
agent_no_settings = baker.make_recipe("agents.agent")
@@ -1272,17 +1265,17 @@ class TestAlertTasks(TacticalTestCase):
)
core = CoreSettings.objects.first()
core.smtp_host = "test.test.com"
core.smtp_port = 587
core.smtp_recipients = ["recipient@test.com"]
core.twilio_account_sid = "test"
core.twilio_auth_token = "1234123412341234"
core.sms_alert_recipients = ["+1234567890"]
core.smtp_host = "test.test.com" # type: ignore
core.smtp_port = 587 # type: ignore
core.smtp_recipients = ["recipient@test.com"] # type: ignore
core.twilio_account_sid = "test" # type: ignore
core.twilio_auth_token = "1234123412341234" # type: ignore
core.sms_alert_recipients = ["+1234567890"] # type: ignore
# test sending email with alert template settings
core.send_mail("Test", "Test", alert_template=alert_template)
core.send_mail("Test", "Test", alert_template=alert_template) # type: ignore
core.send_sms("Test", alert_template=alert_template)
core.send_sms("Test", alert_template=alert_template) # type: ignore
@patch("agents.models.Agent.nats_cmd")
@patch("agents.tasks.agent_outage_sms_task.delay")
@@ -1315,6 +1308,7 @@ class TestAlertTasks(TacticalTestCase):
"alerts.AlertTemplate",
is_active=True,
agent_always_alert=True,
agent_script_actions=False,
action=failure_action,
action_timeout=30,
resolved_action=resolved_action,
@@ -1328,6 +1322,14 @@ class TestAlertTasks(TacticalTestCase):
agent_outages_task()
# should not have been called since agent_script_actions is set to False
nats_cmd.assert_not_called()
alert_template.agent_script_actions = True # type: ignore
alert_template.save() # type: ignore
agent_outages_task()
# this is what data should be
data = {
"func": "runscriptfull",
@@ -1340,14 +1342,6 @@ class TestAlertTasks(TacticalTestCase):
nats_cmd.reset_mock()
# Setup cmd mock
success = {
"retcode": 0,
"stdout": "success!",
"stderr": "",
"execution_time": 5.0000,
}
nats_cmd.side_effect = ["pong", success]
# make sure script run results were stored
@@ -1361,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 = {
@@ -1387,3 +1373,199 @@ class TestAlertTasks(TacticalTestCase):
self.assertEqual(alert.resolved_action_execution_time, "5.0000")
self.assertEqual(alert.resolved_action_stdout, "success!")
self.assertEqual(alert.resolved_action_stderr, "")
def test_parse_script_args(self):
alert = baker.make("alerts.Alert")
args = ["-Parameter", "-Another {{alert.id}}"]
# test default value
self.assertEqual(
["-Parameter", f"-Another '{alert.id}'"], # type: ignore
alert.parse_script_args(args=args), # type: ignore
)
def test_prune_resolved_alerts(self):
from .tasks import prune_resolved_alerts
# setup data
resolved_alerts = baker.make(
"alerts.Alert",
resolved=True,
_quantity=25,
)
alerts = baker.make(
"alerts.Alert",
resolved=False,
_quantity=25,
)
days = 0
for alert in resolved_alerts: # type: ignore
alert.alert_time = djangotime.now() - djangotime.timedelta(days=days)
alert.save()
days = days + 5
days = 0
for alert in alerts: # type: ignore
alert.alert_time = djangotime.now() - djangotime.timedelta(days=days)
alert.save()
days = days + 5
# delete AgentHistory older than 30 days
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)

View File

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

View File

@@ -3,12 +3,14 @@ from datetime import datetime as dt
from django.db.models import Q
from django.shortcuts import get_object_or_404
from django.utils import timezone as djangotime
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from tacticalrmm.utils import notify_error
from .models import Alert, AlertTemplate
from .permissions import AlertPerms, AlertTemplatePerms
from .serializers import (
AlertSerializer,
AlertTemplateRelationSerializer,
@@ -18,6 +20,8 @@ from .tasks import cache_agents_alert_template
class GetAddAlerts(APIView):
permission_classes = [IsAuthenticated, AlertPerms]
def patch(self, request):
# top 10 alerts for dashboard icon
@@ -88,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)
@@ -97,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):
@@ -109,9 +114,10 @@ class GetAddAlerts(APIView):
class GetUpdateDeleteAlert(APIView):
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):
@@ -163,6 +169,8 @@ class GetUpdateDeleteAlert(APIView):
class BulkAlerts(APIView):
permission_classes = [IsAuthenticated, AlertPerms]
def post(self, request):
if request.data["bulk_action"] == "resolve":
Alert.objects.filter(id__in=request.data["alerts"]).update(
@@ -185,9 +193,10 @@ class BulkAlerts(APIView):
class GetAddAlertTemplates(APIView):
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):
@@ -202,6 +211,8 @@ class GetAddAlertTemplates(APIView):
class GetUpdateDeleteAlertTemplate(APIView):
permission_classes = [IsAuthenticated, AlertTemplatePerms]
def get(self, request, pk):
alert_template = get_object_or_404(AlertTemplate, pk=pk)
@@ -231,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)

View File

@@ -5,8 +5,8 @@ from unittest.mock import patch
from django.conf import settings
from django.utils import timezone as djangotime
from model_bakery import baker
from autotasks.models import AutomatedTask
from autotasks.models import AutomatedTask
from tacticalrmm.test import TacticalTestCase
@@ -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"
@@ -213,7 +177,8 @@ class TestAPIv3(TacticalTestCase):
# setup data
agent = baker.make_recipe("agents.agent")
task = baker.make("autotasks.AutomatedTask", agent=agent)
script = baker.make_recipe("scripts.script")
task = baker.make("autotasks.AutomatedTask", agent=agent, script=script)
url = f"/api/v3/{task.pk}/{agent.agent_id}/taskrunner/" # type: ignore

View File

@@ -20,4 +20,5 @@ urlpatterns = [
path("superseded/", views.SupersededWinUpdate.as_view()),
path("<int:pk>/chocoresult/", views.ChocoResult.as_view()),
path("<str:agentid>/recovery/", views.AgentRecovery.as_view()),
path("<int:pk>/<str:agentid>/histresult/", views.AgentHistoryResult.as_view()),
]

View File

@@ -6,7 +6,6 @@ from django.conf import settings
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from django.utils import timezone as djangotime
from loguru import logger
from packaging import version as pyver
from rest_framework.authentication import TokenAuthentication
from rest_framework.authtoken.models import Token
@@ -15,71 +14,29 @@ from rest_framework.response import Response
from rest_framework.views import APIView
from accounts.models import User
from agents.models import Agent, AgentCustomField
from agents.serializers import WinAgentSerializer
from agents.models import Agent, AgentHistory
from agents.serializers import WinAgentSerializer, AgentHistorySerializer
from autotasks.models import AutomatedTask
from autotasks.serializers import TaskGOGetSerializer, TaskRunnerPatchSerializer
from checks.models import Check
from checks.serializers import CheckRunnerGetSerializer
from checks.utils import bytes2human
from logs.models import PendingAction
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
logger.configure(**settings.LOG_CONFIG)
class CheckIn(APIView):
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
def patch(self, request):
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):
"""
!!! DEPRECATED AS OF AGENT 1.7.0 !!!
Endpoint be removed in a future release
"""
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
serializer = WinAgentSerializer(instance=agent, data=request.data, partial=True)
@@ -108,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:
@@ -167,22 +121,26 @@ class WinUpdates(APIView):
def put(self, request):
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
needs_reboot: bool = request.data["needs_reboot"]
agent.needs_reboot = needs_reboot
agent.save(update_fields=["needs_reboot"])
reboot_policy: str = agent.get_patch_policy().reboot_after_install
reboot = False
if reboot_policy == "always":
reboot = True
if request.data["needs_reboot"]:
if reboot_policy == "required":
reboot = True
elif reboot_policy == "never":
agent.needs_reboot = True
agent.save(update_fields=["needs_reboot"])
elif needs_reboot and reboot_policy == "required":
reboot = True
if reboot:
asyncio.run(agent.nats_cmd({"func": "rebootnow"}, wait=False))
logger.info(f"{agent.hostname} is rebooting after updates were installed.")
DebugLog.info(
agent=agent,
log_type="windows_updates",
message=f"{agent.hostname} is rebooting after updates were installed.",
)
agent.delete_superseded_updates()
return Response("ok")
@@ -304,10 +262,11 @@ class CheckRunner(APIView):
< djangotime.now()
- djangotime.timedelta(seconds=check.run_interval)
)
# if check interval isn't set, make sure the agent's check interval has passed before running
)
# if check interval isn't set, make sure the agent's check interval has passed before running
or (
check.last_run
not check.run_interval
and check.last_run
< djangotime.now() - djangotime.timedelta(seconds=agent.check_interval)
)
]
@@ -320,11 +279,16 @@ class CheckRunner(APIView):
def patch(self, request):
check = get_object_or_404(Check, pk=request.data["id"])
if pyver.parse(check.agent.version) < pyver.parse("1.5.7"):
return notify_error("unsupported")
check.last_run = djangotime.now()
check.save(update_fields=["last_run"])
status = check.handle_checkv2(request.data)
status = check.handle_check(request.data)
if status == "failing" and check.assignedtask.exists(): # type: ignore
check.handle_assigned_task()
return Response(status)
return Response("ok")
class CheckRunnerInterval(APIView):
@@ -344,13 +308,12 @@ class TaskRunner(APIView):
permission_classes = [IsAuthenticated]
def get(self, request, pk, agentid):
agent = get_object_or_404(Agent, agent_id=agentid)
_ = get_object_or_404(Agent, agent_id=agentid)
task = get_object_or_404(AutomatedTask, pk=pk)
return Response(TaskGOGetSerializer(task).data)
def patch(self, request, pk, agentid):
from alerts.models import Alert
from logs.models import AuditLog
agent = get_object_or_404(Agent, agent_id=agentid)
task = get_object_or_404(AutomatedTask, pk=pk)
@@ -361,33 +324,18 @@ 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:
if AgentCustomField.objects.filter(
field=task.custom_field, agent=task.agent
).exists():
agent_field = AgentCustomField.objects.get(
field=task.custom_field, agent=task.agent
)
else:
agent_field = AgentCustomField.objects.create(
field=task.custom_field, agent=task.agent
)
# get last line of stdout
value = new_task.stdout.split("\n")[-1].strip()
if task.custom_field.type in ["text", "number", "single", "datetime"]:
agent_field.string_value = value
agent_field.save()
elif task.custom_field.type == "multiple":
agent_field.multiple_value = value.split(",")
agent_field.save()
elif task.custom_field.type == "checkbox":
agent_field.bool_value = bool(value)
agent_field.save()
task.save_collector_results()
status = "passing"
else:
@@ -404,15 +352,6 @@ class TaskRunner(APIView):
else:
Alert.handle_alert_failure(new_task)
AuditLog.objects.create(
username=agent.hostname,
agent=agent.hostname,
object_type="agent",
action="task_run",
message=f"Scheduled Task {task.name} was run on {agent.hostname}",
after_value=AutomatedTask.serialize(new_task),
)
return Response("ok")
@@ -503,6 +442,7 @@ class NewAgent(APIView):
action="agent_install",
message=f"{request.user} installed new agent {agent.hostname}",
after_value=Agent.serialize(agent),
debug_info={"ip": request._client_ip},
)
return Response(
@@ -520,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:
@@ -590,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:
@@ -607,3 +554,16 @@ class AgentRecovery(APIView):
reload_nats()
return Response(ret)
class AgentHistoryResult(APIView):
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
def patch(self, request, agentid, pk):
_ = get_object_or_404(Agent, agent_id=agentid)
hist = get_object_or_404(AgentHistory, pk=pk)
s = AgentHistorySerializer(instance=hist, data=request.data, partial=True)
s.is_valid(raise_exception=True)
s.save()
return Response("ok")

View 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 = [
("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),
),
]

View File

@@ -1,6 +1,7 @@
from django.db import models
from agents.models import Agent
from core.models import CoreSettings
from django.db import models
from logs.models import BaseAuditModel
@@ -28,12 +29,11 @@ class Policy(BaseAuditModel):
def save(self, *args, **kwargs):
from alerts.tasks import cache_agents_alert_template
from automation.tasks import generate_agent_checks_task
# get old policy if exists
old_policy = type(self).objects.get(pk=self.pk) if self.pk else None
super(BaseAuditModel, self).save(*args, **kwargs)
super(Policy, self).save(old_model=old_policy, *args, **kwargs)
# generate agent checks only if active and enforced were changed
if old_policy:
@@ -50,7 +50,7 @@ class Policy(BaseAuditModel):
from automation.tasks import generate_agent_checks_task
agents = list(self.related_agents().only("pk").values_list("pk", flat=True))
super(BaseAuditModel, self).delete(*args, **kwargs)
super(Policy, self).delete(*args, **kwargs)
generate_agent_checks_task.delay(agents=agents, create_tasks=True)
@@ -126,9 +126,9 @@ class Policy(BaseAuditModel):
@staticmethod
def serialize(policy):
# serializes the policy and returns json
from .serializers import PolicySerializer
from .serializers import PolicyAuditSerializer
return PolicySerializer(policy).data
return PolicyAuditSerializer(policy).data
@staticmethod
def cascade_policy_tasks(agent):
@@ -430,11 +430,12 @@ class Policy(BaseAuditModel):
# remove policy checks from agent that fell out of policy scope
agent.agentchecks.filter(
managed_by_policy=True,
parent_check__in=[
checkpk
for checkpk in agent_checks_parent_pks
if checkpk not in [check.pk for check in final_list]
]
],
).delete()
return [

View File

@@ -0,0 +1,11 @@
from rest_framework import permissions
from tacticalrmm.permissions import _has_perm
class AutomationPolicyPerms(permissions.BasePermission):
def has_permission(self, r, view):
if r.method == "GET":
return _has_perm(r, "can_list_automation_policies")
else:
return _has_perm(r, "can_manage_automation_policies")

View File

@@ -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,26 +108,7 @@ class PolicyTaskStatusSerializer(ModelSerializer):
fields = "__all__"
class PolicyCheckSerializer(ModelSerializer):
class PolicyAuditSerializer(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")
class Meta:
model = AutomatedTask
model = Policy
fields = "__all__"
depth = 1

View File

@@ -3,7 +3,7 @@ from typing import Any, Dict, List, Union
from tacticalrmm.celery import app
@app.task
@app.task(retry_backoff=5, retry_jitter=True, retry_kwargs={"max_retries": 5})
def generate_agent_checks_task(
policy: int = None,
site: int = None,
@@ -13,7 +13,6 @@ def generate_agent_checks_task(
create_tasks: bool = False,
) -> Union[str, None]:
from agents.models import Agent
from automation.models import Policy
p = Policy.objects.get(pk=policy) if policy else None
@@ -55,10 +54,14 @@ def generate_agent_checks_task(
if create_tasks:
agent.generate_tasks_from_policies()
agent.set_alert_template()
return "ok"
@app.task
@app.task(
acks_late=True, retry_backoff=5, retry_jitter=True, retry_kwargs={"max_retries": 5}
)
# updates policy managed check fields on agents
def update_policy_check_fields_task(check: int) -> str:
from checks.models import Check
@@ -74,11 +77,10 @@ def update_policy_check_fields_task(check: int) -> str:
return "ok"
@app.task
@app.task(retry_backoff=5, retry_jitter=True, retry_kwargs={"max_retries": 5})
# generates policy tasks on agents affected by a policy
def generate_agent_autotasks_task(policy: int = None) -> str:
from agents.models import Agent
from automation.models import Policy
p: Policy = Policy.objects.get(pk=policy)
@@ -102,7 +104,12 @@ def generate_agent_autotasks_task(policy: int = None) -> str:
return "ok"
@app.task
@app.task(
acks_late=True,
retry_backoff=5,
retry_jitter=True,
retry_kwargs={"max_retries": 5},
)
def delete_policy_autotasks_task(task: int) -> str:
from autotasks.models import AutomatedTask
@@ -122,7 +129,12 @@ def run_win_policy_autotasks_task(task: int) -> str:
return "ok"
@app.task
@app.task(
acks_late=True,
retry_backoff=5,
retry_jitter=True,
retry_kwargs={"max_retries": 5},
)
def update_policy_autotasks_fields_task(task: int, update_agent: bool = False) -> str:
from autotasks.models import AutomatedTask

View File

@@ -1,20 +1,16 @@
from itertools import cycle
from unittest.mock import patch
from model_bakery import baker, seq
from agents.models import Agent
from core.models import CoreSettings
from model_bakery import baker, seq
from tacticalrmm.test import TacticalTestCase
from winupdate.models import WinUpdatePolicy
from .serializers import (
AutoTasksFieldSerializer,
PolicyCheckSerializer,
PolicyCheckStatusSerializer,
PolicyOverviewSerializer,
PolicySerializer,
PolicyTableSerializer,
PolicyTaskStatusSerializer,
)
@@ -27,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)
@@ -54,6 +48,8 @@ class TestPolicyViews(TacticalTestCase):
@patch("autotasks.models.AutomatedTask.create_task_on_agent")
def test_add_policy(self, create_task):
from automation.models import Policy
url = "/automation/policies/"
data = {
@@ -72,8 +68,12 @@ class TestPolicyViews(TacticalTestCase):
# create policy with tasks and checks
policy = baker.make("automation.Policy")
self.create_checks(policy=policy)
baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3)
checks = self.create_checks(policy=policy)
tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3)
# assign a task to a check
tasks[0].assigned_check = checks[0] # type: ignore
tasks[0].save() # type: ignore
# test copy tasks and checks to another policy
data = {
@@ -86,8 +86,16 @@ class TestPolicyViews(TacticalTestCase):
resp = self.client.post(f"/automation/policies/", data, format="json")
self.assertEqual(resp.status_code, 200)
self.assertEqual(policy.autotasks.count(), 3) # type: ignore
self.assertEqual(policy.policychecks.count(), 7) # type: ignore
copied_policy = Policy.objects.get(name=data["name"])
self.assertEqual(copied_policy.autotasks.count(), 3) # type: ignore
self.assertEqual(copied_policy.policychecks.count(), 7) # type: ignore
# make sure correct task was assign to the check
self.assertEqual(copied_policy.autotasks.get(name=tasks[0].name).assigned_check.check_type, checks[0].check_type) # type: ignore
create_task.assert_not_called()
self.check_not_authenticated("post", url)
@@ -110,7 +118,7 @@ class TestPolicyViews(TacticalTestCase):
resp = self.client.put(url, data, format="json")
self.assertEqual(resp.status_code, 200)
# only called if active or enforced are updated
# only called if active, enforced, or excluded objects are updated
generate_agent_checks_task.assert_not_called()
data = {
@@ -120,6 +128,23 @@ class TestPolicyViews(TacticalTestCase):
"enforced": False,
}
resp = self.client.put(url, data, format="json")
self.assertEqual(resp.status_code, 200)
generate_agent_checks_task.assert_called_with(
policy=policy.pk, create_tasks=True # type: ignore
)
generate_agent_checks_task.reset_mock()
# make sure policies are re-evaluated when excluded changes
agents = baker.make_recipe("agents.agent", _quantity=2)
clients = baker.make("clients.Client", _quantity=2)
sites = baker.make("clients.Site", _quantity=2)
data = {
"excluded_agents": [agent.pk for agent in agents], # type: ignore
"excluded_sites": [site.pk for site in sites], # type: ignore
"excluded_clients": [client.pk for client in clients], # type: ignore
}
resp = self.client.put(url, data, format="json")
self.assertEqual(resp.status_code, 200)
generate_agent_checks_task.assert_called_with(
@@ -151,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")
@@ -195,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
@@ -262,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):
@@ -283,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}
@@ -323,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",
@@ -347,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",
@@ -376,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
@@ -388,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
@@ -400,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()
@@ -408,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)
@@ -473,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
@@ -481,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
@@ -771,6 +769,7 @@ class TestPolicyTasks(TacticalTestCase):
@patch("automation.tasks.generate_agent_checks_task.delay")
def test_generating_policy_checks_for_all_agents(self, generate_agent_checks_mock):
from core.models import CoreSettings
from .tasks import generate_agent_checks_task
# setup data
@@ -887,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
@@ -900,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
@@ -913,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)
@@ -925,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
@@ -964,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)
@@ -972,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
@@ -1133,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()

View File

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

View File

@@ -1,21 +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,
@@ -24,10 +25,16 @@ from .serializers import (
class GetAddPolicies(APIView):
permission_classes = [IsAuthenticated, AutomationPolicyPerms]
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)
@@ -51,18 +58,30 @@ class GetAddPolicies(APIView):
class GetUpdateDeletePolicy(APIView):
permission_classes = [IsAuthenticated, AutomationPolicyPerms]
def get(self, request, pk):
policy = get_object_or_404(Policy, pk=pk)
return Response(PolicySerializer(policy).data)
def put(self, request, pk):
from .tasks import generate_agent_checks_task
policy = get_object_or_404(Policy, pk=pk)
serializer = PolicySerializer(instance=policy, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
serializer.save()
# check for excluding objects and in the request and if present generate policies
if (
"excluded_sites" in request.data.keys()
or "excluded_clients" in request.data.keys()
or "excluded_agents" in request.data.keys()
):
generate_agent_checks_task.delay(policy=pk, create_tasks=True)
return Response("ok")
def delete(self, request, pk):
@@ -87,18 +106,13 @@ class PolicySync(APIView):
class PolicyAutoTask(APIView):
# 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)
@@ -106,11 +120,9 @@ class PolicyAutoTask(APIView):
class PolicyCheck(APIView):
def get(self, request, pk):
checks = Check.objects.filter(policy__pk=pk, agent=None)
return Response(PolicyCheckSerializer(checks, many=True).data)
permission_classes = [IsAuthenticated, AutomationPolicyPerms]
def patch(self, request, check):
def get(self, request, check):
checks = Check.objects.filter(parent_check=check)
return Response(PolicyCheckStatusSerializer(checks, many=True).data)
@@ -125,8 +137,6 @@ class OverviewPolicy(APIView):
class GetRelated(APIView):
def get(self, request, pk):
response = {}
policy = (
Policy.objects.filter(pk=pk)
.prefetch_related(
@@ -138,47 +148,13 @@ 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):
permission_classes = [IsAuthenticated, AutomationPolicyPerms]
# create new patch policy
def post(self, request):
policy = get_object_or_404(Policy, pk=request.data["policy"])
@@ -191,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
@@ -202,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()
@@ -240,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.")

View File

@@ -1,7 +1,7 @@
# Generated by Django 3.1.7 on 2021-04-04 00:32
from django.db import migrations, models
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):

View File

@@ -0,0 +1,20 @@
# Generated by Django 3.1.7 on 2021-04-27 14:11
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0021_customfield_hide_in_ui'),
('autotasks', '0020_auto_20210421_0226'),
]
operations = [
migrations.AlterField(
model_name='automatedtask',
name='custom_field',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='autotasks', to='core.customfield'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.1 on 2021-05-29 03:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('autotasks', '0021_alter_automatedtask_custom_field'),
]
operations = [
migrations.AddField(
model_name='automatedtask',
name='collector_all_output',
field=models.BooleanField(default=False),
),
]

View 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 = [
("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),
),
]

View File

@@ -6,18 +6,16 @@ from typing import List
import pytz
from alerts.models import SEVERITY_CHOICES
from django.conf import settings
from django.contrib.postgres.fields import ArrayField
from django.db import models
from django.db.models.fields import DateTimeField
from django.db.utils import DatabaseError
from django.utils import timezone as djangotime
from logs.models import BaseAuditModel
from loguru import logger
from logs.models import BaseAuditModel, DebugLog
from tacticalrmm.models import PermissionQuerySet
from packaging import version as pyver
from tacticalrmm.utils import bitdays_to_string
logger.configure(**settings.LOG_CONFIG)
RUN_TIME_DAY_CHOICES = [
(0, "Monday"),
(1, "Tuesday"),
@@ -50,6 +48,8 @@ TASK_STATUS_CHOICES = [
class AutomatedTask(BaseAuditModel):
objects = PermissionQuerySet.as_manager()
agent = models.ForeignKey(
"agents.Agent",
related_name="autotasks",
@@ -64,9 +64,9 @@ class AutomatedTask(BaseAuditModel):
blank=True,
on_delete=models.CASCADE,
)
custom_field = models.OneToOneField(
custom_field = models.ForeignKey(
"core.CustomField",
related_name="autotask",
related_name="autotasks",
null=True,
blank=True,
on_delete=models.SET_NULL,
@@ -104,6 +104,7 @@ class AutomatedTask(BaseAuditModel):
task_type = models.CharField(
max_length=100, choices=TASK_TYPE_CHOICES, default="manual"
)
collector_all_output = models.BooleanField(default=False)
run_time_date = DateTimeField(null=True, blank=True)
remove_if_not_scheduled = models.BooleanField(default=False)
run_asap_after_missed = models.BooleanField(default=False) # added in agent v1.4.7
@@ -134,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":
@@ -182,6 +208,7 @@ class AutomatedTask(BaseAuditModel):
"remove_if_not_scheduled",
"run_asap_after_missed",
"custom_field",
"collector_all_output",
]
@staticmethod
@@ -192,37 +219,31 @@ class AutomatedTask(BaseAuditModel):
@staticmethod
def serialize(task):
# serializes the task and returns json
from .serializers import TaskSerializer
from .serializers import TaskAuditSerializer
return TaskSerializer(task).data
return TaskAuditSerializer(task).data
def create_policy_task(self, agent=None, policy=None):
def create_policy_task(self, agent=None, policy=None, assigned_check=None):
# added to allow new policy tasks to be assigned to check only when the agent check exists already
if (
self.assigned_check
and agent
and agent.agentchecks.filter(parent_check=self.assigned_check.id).exists()
):
assigned_check = agent.agentchecks.get(parent_check=self.assigned_check.id)
# if policy is present, then this task is being copied to another policy
# if agent is present, then this task is being created on an agent from a policy
# exit if neither are set or if both are set
if not agent and not policy or agent and policy:
# also exit if assigned_check is set because this task will be created when the check is
if (
(not agent and not policy)
or (agent and policy)
or (self.assigned_check and not assigned_check)
):
return
assigned_check = None
# get correct assigned check to task if set
if agent and self.assigned_check:
# check if there is a matching check on the agent
if agent.agentchecks.filter(parent_check=self.assigned_check.pk).exists():
assigned_check = agent.agentchecks.filter(
parent_check=self.assigned_check.pk
).first()
elif policy and self.assigned_check:
if policy.policychecks.filter(name=self.assigned_check.name).exists():
assigned_check = policy.policychecks.filter(
name=self.assigned_check.name
).first()
else:
assigned_check = policy.policychecks.filter(
check_type=self.assigned_check.check_type
).first()
task = AutomatedTask.objects.create(
agent=agent,
policy=policy,
@@ -232,11 +253,13 @@ class AutomatedTask(BaseAuditModel):
)
for field in self.policy_fields_to_copy:
setattr(task, field, getattr(self, field))
if field != "assigned_check":
setattr(task, field, getattr(self, field))
task.save()
task.create_task_on_agent()
if agent:
task.create_task_on_agent()
def create_task_on_agent(self):
from agents.models import Agent
@@ -263,7 +286,7 @@ class AutomatedTask(BaseAuditModel):
elif self.task_type == "runonce":
# check if scheduled time is in the past
agent_tz = pytz.timezone(agent.timezone)
agent_tz = pytz.timezone(agent.timezone) # type: ignore
task_time_utc = self.run_time_date.replace(tzinfo=agent_tz).astimezone(
pytz.utc
)
@@ -289,7 +312,7 @@ class AutomatedTask(BaseAuditModel):
},
}
if self.run_asap_after_missed and pyver.parse(agent.version) >= pyver.parse(
if self.run_asap_after_missed and pyver.parse(agent.version) >= pyver.parse( # type: ignore
"1.4.7"
):
nats_data["schedtaskpayload"]["run_asap_after_missed"] = True
@@ -310,19 +333,25 @@ class AutomatedTask(BaseAuditModel):
else:
return "error"
r = asyncio.run(agent.nats_cmd(nats_data, timeout=5))
r = asyncio.run(agent.nats_cmd(nats_data, timeout=5)) # type: ignore
if r != "ok":
self.sync_status = "initial"
self.save(update_fields=["sync_status"])
logger.warning(
f"Unable to create scheduled task {self.name} on {agent.hostname}. It will be created when the agent checks in."
DebugLog.warning(
agent=agent,
log_type="agent_issues",
message=f"Unable to create scheduled task {self.name} on {agent.hostname}. It will be created when the agent checks in.", # type: ignore
)
return "timeout"
else:
self.sync_status = "synced"
self.save(update_fields=["sync_status"])
logger.info(f"{agent.hostname} task {self.name} was successfully created")
DebugLog.info(
agent=agent,
log_type="agent_issues",
message=f"{agent.hostname} task {self.name} was successfully created", # type: ignore
)
return "ok"
@@ -342,19 +371,25 @@ class AutomatedTask(BaseAuditModel):
"enabled": self.enabled,
},
}
r = asyncio.run(agent.nats_cmd(nats_data, timeout=5))
r = asyncio.run(agent.nats_cmd(nats_data, timeout=5)) # type: ignore
if r != "ok":
self.sync_status = "notsynced"
self.save(update_fields=["sync_status"])
logger.warning(
f"Unable to modify scheduled task {self.name} on {agent.hostname}. It will try again on next agent checkin"
DebugLog.warning(
agent=agent,
log_type="agent_issues",
message=f"Unable to modify scheduled task {self.name} on {agent.hostname}({agent.pk}). It will try again on next agent checkin", # type: ignore
)
return "timeout"
else:
self.sync_status = "synced"
self.save(update_fields=["sync_status"])
logger.info(f"{agent.hostname} task {self.name} was successfully modified")
DebugLog.info(
agent=agent,
log_type="agent_issues",
message=f"{agent.hostname} task {self.name} was successfully modified", # type: ignore
)
return "ok"
@@ -371,18 +406,29 @@ class AutomatedTask(BaseAuditModel):
"func": "delschedtask",
"schedtaskpayload": {"name": self.win_task_name},
}
r = asyncio.run(agent.nats_cmd(nats_data, timeout=10))
r = asyncio.run(agent.nats_cmd(nats_data, timeout=10)) # type: ignore
if r != "ok" and "The system cannot find the file specified" not in r:
self.sync_status = "pendingdeletion"
self.save(update_fields=["sync_status"])
logger.warning(
f"{agent.hostname} task {self.name} was successfully modified"
try:
self.save(update_fields=["sync_status"])
except DatabaseError:
pass
DebugLog.warning(
agent=agent,
log_type="agent_issues",
message=f"{agent.hostname} task {self.name} will be deleted on next checkin", # type: ignore
)
return "timeout"
else:
self.delete()
logger.info(f"{agent.hostname} task {self.name} was deleted")
DebugLog.info(
agent=agent,
log_type="agent_issues",
message=f"{agent.hostname}({agent.pk}) task {self.name} was deleted", # type: ignore
)
return "ok"
@@ -395,9 +441,20 @@ class AutomatedTask(BaseAuditModel):
.first()
)
asyncio.run(agent.nats_cmd({"func": "runtask", "taskpk": self.pk}, wait=False))
asyncio.run(agent.nats_cmd({"func": "runtask", "taskpk": self.pk}, wait=False)) # type: ignore
return "ok"
def save_collector_results(self):
agent_field = self.custom_field.get_or_create_field_value(self.agent)
value = (
self.stdout.strip()
if self.collector_all_output
else self.stdout.strip().split("\n")[-1].strip()
)
agent_field.save_to_field(value)
def should_create_alert(self, alert_template=None):
return (
self.dashboard_alert
@@ -417,9 +474,9 @@ class AutomatedTask(BaseAuditModel):
from core.models import CoreSettings
CORE = CoreSettings.objects.first()
# Format of Email sent when Task has email alert
if self.agent:
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Failed"
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self.agent.hostname} - {self} Failed"
else:
subject = f"{self} Failed"
@@ -428,16 +485,15 @@ class AutomatedTask(BaseAuditModel):
+ f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}"
)
CORE.send_mail(subject, body, self.agent.alert_template)
CORE.send_mail(subject, body, self.agent.alert_template) # type: ignore
def send_sms(self):
from core.models import CoreSettings
CORE = CoreSettings.objects.first()
# Format of SMS sent when Task has SMS alert
if self.agent:
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Failed"
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self.agent.hostname} - {self} Failed"
else:
subject = f"{self} Failed"
@@ -446,7 +502,7 @@ class AutomatedTask(BaseAuditModel):
+ f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}"
)
CORE.send_sms(body, alert_template=self.agent.alert_template)
CORE.send_sms(body, alert_template=self.agent.alert_template) # type: ignore
def send_resolved_email(self):
from core.models import CoreSettings
@@ -458,7 +514,7 @@ class AutomatedTask(BaseAuditModel):
+ f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}"
)
CORE.send_mail(subject, body, alert_template=self.agent.alert_template)
CORE.send_mail(subject, body, alert_template=self.agent.alert_template) # type: ignore
def send_resolved_sms(self):
from core.models import CoreSettings
@@ -469,4 +525,4 @@ class AutomatedTask(BaseAuditModel):
subject
+ f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}"
)
CORE.send_sms(body, alert_template=self.agent.alert_template)
CORE.send_sms(body, alert_template=self.agent.alert_template) # type: ignore

View File

@@ -0,0 +1,21 @@
from rest_framework import permissions
from tacticalrmm.permissions import _has_perm, _has_perm_on_agent
class AutoTaskPerms(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_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):
def has_permission(self, r, view):
return _has_perm(r, "can_run_autotasks")

View File

@@ -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:
@@ -68,6 +55,12 @@ class TaskRunnerGetSerializer(serializers.ModelSerializer):
class TaskGOGetSerializer(serializers.ModelSerializer):
script = ScriptCheckSerializer(read_only=True)
script_args = serializers.SerializerMethodField()
def get_script_args(self, obj):
return Script.parse_script_args(
agent=obj.agent, shell=obj.script.shell, args=obj.script_args
)
class Meta:
model = AutomatedTask
@@ -78,3 +71,9 @@ class TaskRunnerPatchSerializer(serializers.ModelSerializer):
class Meta:
model = AutomatedTask
fields = "__all__"
class TaskAuditSerializer(serializers.ModelSerializer):
class Meta:
model = AutomatedTask
fields = "__all__"

View File

@@ -1,17 +1,15 @@
import asyncio
import datetime as dt
from logging import log
import random
from time import sleep
from typing import Union
from django.conf import settings
from django.utils import timezone as djangotime
from loguru import logger
from tacticalrmm.celery import app
from autotasks.models import AutomatedTask
logger.configure(**settings.LOG_CONFIG)
from logs.models import DebugLog
from tacticalrmm.celery import app
@app.task
@@ -53,12 +51,20 @@ def remove_orphaned_win_tasks(agentpk):
agent = Agent.objects.get(pk=agentpk)
logger.info(f"Orphaned task cleanup initiated on {agent.hostname}.")
DebugLog.info(
agent=agent,
log_type="agent_issues",
message=f"Orphaned task cleanup initiated on {agent.hostname}.",
)
r = asyncio.run(agent.nats_cmd({"func": "listschedtasks"}, timeout=10))
if not isinstance(r, list) and not r: # empty list
logger.error(f"Unable to clean up scheduled tasks on {agent.hostname}: {r}")
DebugLog.error(
agent=agent,
log_type="agent_issues",
message=f"Unable to clean up scheduled tasks on {agent.hostname}: {r}",
)
return "notlist"
agent_task_names = list(agent.autotasks.values_list("win_task_name", flat=True))
@@ -83,13 +89,23 @@ def remove_orphaned_win_tasks(agentpk):
}
ret = asyncio.run(agent.nats_cmd(nats_data, timeout=10))
if ret != "ok":
logger.error(
f"Unable to clean up orphaned task {task} on {agent.hostname}: {ret}"
DebugLog.error(
agent=agent,
log_type="agent_issues",
message=f"Unable to clean up orphaned task {task} on {agent.hostname}: {ret}",
)
else:
logger.info(f"Removed orphaned task {task} from {agent.hostname}")
DebugLog.info(
agent=agent,
log_type="agent_issues",
message=f"Removed orphaned task {task} from {agent.hostname}",
)
logger.info(f"Orphaned task cleanup finished on {agent.hostname}")
DebugLog.info(
agent=agent,
log_type="agent_issues",
message=f"Orphaned task cleanup finished on {agent.hostname}",
)
@app.task

View File

@@ -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,19 +184,20 @@ 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
delete_policy_autotasks_task.assert_called_with(task=policy_task.id) # type: ignore
self.check_not_authenticated("delete", url)
@@ -182,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):
@@ -409,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)

View File

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

View File

@@ -1,51 +1,59 @@
from agents.models import Agent
from checks.models import Check
from django.shortcuts import get_object_or_404
from rest_framework.decorators import api_view
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from scripts.models import Script
from tacticalrmm.utils import get_bit_days, get_default_timezone, notify_error
from rest_framework.exceptions import PermissionDenied
from agents.models import Agent
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 .serializers import AutoTaskSerializer, TaskSerializer
from .permissions import AutoTaskPerms, RunAutoTaskPerms
from .serializers import TaskSerializer
class AddAutoTask(APIView):
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,
)
@@ -55,56 +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):
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
@@ -112,18 +99,28 @@ 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:
delete_policy_autotasks_task.delay(task=task.pk)
task.delete()
return Response(f"{task.name} will be deleted shortly")
@api_view()
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}")

View File

@@ -0,0 +1,22 @@
# Generated by Django 3.2.1 on 2021-06-06 16:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('checks', '0023_check_run_interval'),
]
operations = [
migrations.RemoveField(
model_name='checkhistory',
name='check_history',
),
migrations.AddField(
model_name='checkhistory',
name='check_id',
field=models.PositiveIntegerField(default=0),
),
]

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

View File

@@ -1,4 +1,3 @@
import asyncio
import json
import os
import string
@@ -13,11 +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 loguru import logger
from .utils import bytes2human
logger.configure(**settings.LOG_CONFIG)
from tacticalrmm.models import PermissionQuerySet
CHECK_TYPE_CHOICES = [
("diskspace", "Disk Space Check"),
@@ -56,6 +51,7 @@ EVT_LOG_FAIL_WHEN_CHOICES = [
class Check(BaseAuditModel):
objects = PermissionQuerySet.as_manager()
# common fields
@@ -236,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",
@@ -315,9 +311,9 @@ class Check(BaseAuditModel):
)
def add_check_history(self, value: int, more_info: Any = None) -> None:
CheckHistory.objects.create(check_history=self, y=value, results=more_info)
CheckHistory.objects.create(check_id=self.pk, y=value, results=more_info)
def handle_checkv2(self, data):
def handle_check(self, data):
from alerts.models import Alert
# cpuload or mem checks
@@ -348,9 +344,6 @@ class Check(BaseAuditModel):
elif self.check_type == "diskspace":
if data["exists"]:
percent_used = round(data["percent_used"])
total = bytes2human(data["total"])
free = bytes2human(data["free"])
if self.error_threshold and (100 - percent_used) < self.error_threshold:
self.status = "failing"
self.alert_severity = "error"
@@ -364,7 +357,7 @@ class Check(BaseAuditModel):
else:
self.status = "passing"
self.more_info = f"Total: {total}B, Free: {free}B"
self.more_info = data["more_info"]
# add check history
self.add_check_history(100 - percent_used)
@@ -380,12 +373,7 @@ class Check(BaseAuditModel):
self.stdout = data["stdout"]
self.stderr = data["stderr"]
self.retcode = data["retcode"]
try:
# python agent
self.execution_time = "{:.4f}".format(data["stop"] - data["start"])
except:
# golang agent
self.execution_time = "{:.4f}".format(data["runtime"])
self.execution_time = "{:.4f}".format(data["runtime"])
if data["retcode"] in self.info_return_codes:
self.alert_severity = "info"
@@ -421,18 +409,8 @@ class Check(BaseAuditModel):
# ping checks
elif self.check_type == "ping":
success = ["Reply", "bytes", "time", "TTL"]
output = data["output"]
if data["has_stdout"]:
if all(x in output for x in success):
self.status = "passing"
else:
self.status = "failing"
elif data["has_stderr"]:
self.status = "failing"
self.more_info = output
self.status = data["status"]
self.more_info = data["output"]
self.save(update_fields=["more_info"])
self.add_check_history(
@@ -441,41 +419,8 @@ class Check(BaseAuditModel):
# windows service checks
elif self.check_type == "winsvc":
svc_stat = data["status"]
self.more_info = f"Status {svc_stat.upper()}"
if data["exists"]:
if svc_stat == "running":
self.status = "passing"
elif svc_stat == "start_pending" and self.pass_if_start_pending:
self.status = "passing"
else:
if self.agent and self.restart_if_stopped:
nats_data = {
"func": "winsvcaction",
"payload": {"name": self.svc_name, "action": "start"},
}
r = asyncio.run(self.agent.nats_cmd(nats_data, timeout=32))
if r == "timeout" or r == "natsdown":
self.status = "failing"
elif not r["success"] and r["errormsg"]:
self.status = "failing"
elif r["success"]:
self.status = "passing"
self.more_info = f"Status RUNNING"
else:
self.status = "failing"
else:
self.status = "failing"
else:
if self.pass_if_svc_not_exist:
self.status = "passing"
else:
self.status = "failing"
self.more_info = f"Service {self.svc_name} does not exist"
self.status = data["status"]
self.more_info = data["more_info"]
self.save(update_fields=["more_info"])
self.add_check_history(
@@ -483,49 +428,7 @@ class Check(BaseAuditModel):
)
elif self.check_type == "eventlog":
log = []
is_wildcard = self.event_id_is_wildcard
eventType = self.event_type
eventID = self.event_id
source = self.event_source
message = self.event_message
r = data["log"]
for i in r:
if i["eventType"] == eventType:
if not is_wildcard and not int(i["eventID"]) == eventID:
continue
if not source and not message:
if is_wildcard:
log.append(i)
elif int(i["eventID"]) == eventID:
log.append(i)
continue
if source and message:
if is_wildcard:
if source in i["source"] and message in i["message"]:
log.append(i)
elif int(i["eventID"]) == eventID:
if source in i["source"] and message in i["message"]:
log.append(i)
continue
if source and source in i["source"]:
if is_wildcard:
log.append(i)
elif int(i["eventID"]) == eventID:
log.append(i)
if message and message in i["message"]:
if is_wildcard:
log.append(i)
elif int(i["eventID"]) == eventID:
log.append(i)
log = data["log"]
if self.fail_when == "contains":
if log and len(log) >= self.number_of_events_b4_alert:
self.status = "failing"
@@ -556,33 +459,23 @@ 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)
return self.status
def handle_assigned_task(self) -> None:
for task in self.assignedtask.all(): # type: ignore
if task.enabled:
task.run_win_task()
@staticmethod
def serialize(check):
# serializes the check and returns json
from .serializers import CheckSerializer
from .serializers import CheckAuditSerializer
return CheckSerializer(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
return CheckAuditSerializer(check).data
def create_policy_check(self, agent=None, policy=None):
@@ -598,6 +491,14 @@ class Check(BaseAuditModel):
script=self.script,
)
for task in self.assignedtask.all(): # type: ignore
if policy or (
agent and not agent.autotasks.filter(parent_task=task.pk).exists()
):
task.create_policy_task(
agent=agent, policy=policy, assigned_check=check
)
for field in self.policy_fields_to_copy:
setattr(check, field, getattr(self, field))
@@ -770,14 +671,12 @@ class Check(BaseAuditModel):
class CheckHistory(models.Model):
check_history = models.ForeignKey(
Check,
related_name="check_history",
on_delete=models.CASCADE,
)
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.check_history.readable_desc
return str(self.x)

View File

@@ -0,0 +1,23 @@
from rest_framework import permissions
from tacticalrmm.permissions import _has_perm, _has_perm_on_agent
class ChecksPerms(permissions.BasePermission):
def has_permission(self, r, view):
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") and _has_perm_on_agent(
r.user, view.kwargs["agent_id"]
)

View File

@@ -3,9 +3,10 @@ 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
class AssignedTaskField(serializers.ModelSerializer):
@@ -17,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()
@@ -56,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
@@ -64,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)
)
@@ -101,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()
):
@@ -125,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()
):
@@ -158,13 +163,16 @@ class AssignedTaskCheckRunnerField(serializers.ModelSerializer):
class CheckRunnerGetSerializer(serializers.ModelSerializer):
# only send data needed for agent to run a check
assigned_tasks = serializers.SerializerMethodField()
script = ScriptCheckSerializer(read_only=True)
script_args = serializers.SerializerMethodField()
def get_assigned_tasks(self, obj):
if obj.assignedtask.exists():
tasks = obj.assignedtask.all()
return AssignedTaskCheckRunnerField(tasks, many=True).data
def get_script_args(self, obj):
if obj.check_type != "script":
return []
return Script.parse_script_args(
agent=obj.agent, shell=obj.script.shell, args=obj.script_args
)
class Meta:
model = Check
@@ -193,6 +201,7 @@ class CheckRunnerGetSerializer(serializers.ModelSerializer):
"modified_by",
"modified_time",
"history",
"dashboard_alert",
]
@@ -215,3 +224,9 @@ class CheckHistorySerializer(serializers.ModelSerializer):
class Meta:
model = CheckHistory
fields = ("x", "y", "results")
class CheckAuditSerializer(serializers.ModelSerializer):
class Meta:
model = Check
fields = "__all__"

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,60 +1,65 @@
import asyncio
from datetime import datetime as dt
from agents.models import Agent
from automation.models import Policy
from django.db.models import Q
from django.shortcuts import get_object_or_404
from django.utils import timezone as djangotime
from packaging import version as pyver
from rest_framework.decorators import api_view
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from scripts.models import Script
from tacticalrmm.utils import notify_error
from rest_framework.exceptions import PermissionDenied
from .models import Check
from agents.models import Agent
from automation.models import Policy
from tacticalrmm.utils import notify_error
from tacticalrmm.permissions import _has_perm_on_agent
from .models import Check, CheckHistory
from .permissions import ChecksPerms, RunChecksPerms
from .serializers import CheckHistorySerializer, CheckSerializer
class AddCheck(APIView):
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
)
@@ -76,44 +81,43 @@ class AddCheck(APIView):
class GetUpdateDeleteCheck(APIView):
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):
from automation.tasks import (
update_policy_check_fields_task,
)
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)
@@ -122,27 +126,55 @@ class GetUpdateDeleteCheck(APIView):
def delete(self, request, pk):
from automation.tasks import generate_agent_checks_task
check = get_object_or_404(Check, 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.delete()
# Policy check deleted
if check.policy:
Check.objects.filter(parent_check=check.pk).delete()
Check.objects.filter(managed_by_policy=True, parent_check=pk).delete()
# Re-evaluate agent checks is policy was enforced
if check.policy.enforced:
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 CheckHistory(APIView):
def patch(self, request, checkpk):
check = get_object_or_404(Check, pk=checkpk)
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):
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()
@@ -154,7 +186,7 @@ class CheckHistory(APIView):
- djangotime.timedelta(days=request.data["timeFilter"]),
)
check_history = check.check_history.filter(timeFilter).order_by("-x") # type: ignore
check_history = CheckHistory.objects.filter(check_id=pk).filter(timeFilter).order_by("-x") # type: ignore
return Response(
CheckHistorySerializer(
@@ -164,8 +196,9 @@ class CheckHistory(APIView):
@api_view()
def run_checks(request, pk):
agent = get_object_or_404(Agent, pk=pk)
@permission_classes([IsAuthenticated, RunChecksPerms])
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))
@@ -178,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())

View File

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

View File

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

View File

@@ -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(
@@ -33,13 +37,17 @@ class Client(BaseAuditModel):
blank=True,
)
def save(self, *args, **kw):
def save(self, *args, **kwargs):
from alerts.tasks import cache_agents_alert_template
from automation.tasks import generate_agent_checks_task
# get old client if exists
old_client = type(self).objects.get(pk=self.pk) if self.pk else None
super(BaseAuditModel, self).save(*args, **kw)
old_client = Client.objects.get(pk=self.pk) if self.pk else None
super(Client, self).save(
old_model=old_client,
*args,
**kwargs,
)
# check if polcies have changed and initiate task to reapply policies if so
if old_client:
@@ -50,7 +58,6 @@ class Client(BaseAuditModel):
old_client.block_policy_inheritance != self.block_policy_inheritance
)
):
generate_agent_checks_task.delay(
client=self.pk,
create_tasks=True,
@@ -67,32 +74,31 @@ 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")
)
agents = Agent.objects.defer(*AGENT_DEFER).filter(site__client=self)
data = {"error": False, "warning": False}
for agent in agents:
if agent.maintenance_mode:
break
if agent.overdue_email_alert or agent.overdue_text_alert:
if agent.status == "overdue":
data["error"] = True
break
if agent.checks["has_failing_checks"]:
if agent.checks["warning"]:
@@ -102,22 +108,25 @@ class Client(BaseAuditModel):
data["error"] = True
break
if agent.overdue_email_alert or agent.overdue_text_alert:
if agent.status == "overdue":
data["error"] = True
break
if agent.autotasks.exists(): # type: ignore
for i in agent.autotasks.all(): # type: ignore
if i.status == "failing" and i.alert_severity == "error":
data["error"] = True
break
return data
@staticmethod
def serialize(client):
# serializes the client and returns json
from .serializers import ClientSerializer
from .serializers import ClientAuditSerializer
return ClientSerializer(client).data
# serializes the client and returns json
return ClientAuditSerializer(client).data
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)
@@ -144,13 +153,17 @@ class Site(BaseAuditModel):
blank=True,
)
def save(self, *args, **kw):
def save(self, *args, **kwargs):
from alerts.tasks import cache_agents_alert_template
from automation.tasks import generate_agent_checks_task
# get old client if exists
old_site = type(self).objects.get(pk=self.pk) if self.pk else None
super(Site, self).save(*args, **kw)
old_site = Site.objects.get(pk=self.pk) if self.pk else None
super(Site, self).save(
old_model=old_site,
*args,
**kwargs,
)
# check if polcies have changed and initiate task to reapply policies if so
if old_site:
@@ -159,11 +172,10 @@ class Site(BaseAuditModel):
or (old_site.workstation_policy != self.workstation_policy)
or (old_site.block_policy_inheritance != self.block_policy_inheritance)
):
generate_agent_checks_task.delay(site=self.pk, create_tasks=True)
if old_site.alert_template != self.alert_template:
cache_agents_alert_template.delay()
if old_site.alert_template != self.alert_template:
cache_agents_alert_template.delay()
class Meta:
ordering = ("name",)
@@ -174,30 +186,35 @@ 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")
.prefetch_related("agentchecks", "autotasks")
)
data = {"error": False, "warning": False}
for agent in agents:
if agent.maintenance_mode:
break
if agent.overdue_email_alert or agent.overdue_text_alert:
if agent.status == "overdue":
data["error"] = True
break
if agent.checks["has_failing_checks"]:
if agent.checks["warning"]:
@@ -207,19 +224,20 @@ class Site(BaseAuditModel):
data["error"] = True
break
if agent.overdue_email_alert or agent.overdue_text_alert:
if agent.status == "overdue":
data["error"] = True
break
if agent.autotasks.exists(): # type: ignore
for i in agent.autotasks.all(): # type: ignore
if i.status == "failing" and i.alert_severity == "error":
data["error"] = True
break
return data
@staticmethod
def serialize(site):
# serializes the site and returns json
from .serializers import SiteSerializer
from .serializers import SiteAuditSerializer
return SiteSerializer(site).data
# serializes the site and returns json
return SiteAuditSerializer(site).data
MON_TYPE_CHOICES = [
@@ -234,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
)
@@ -256,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(
@@ -291,6 +312,22 @@ class ClientCustomField(models.Model):
else:
return self.string_value
def save_to_field(self, value):
if self.field.type in [
"text",
"number",
"single",
"datetime",
]:
self.string_value = value
self.save()
elif type == "multiple":
self.multiple_value = value.split(",")
self.save()
elif type == "checkbox":
self.bool_value = bool(value)
self.save()
class SiteCustomField(models.Model):
site = models.ForeignKey(
@@ -325,3 +362,19 @@ class SiteCustomField(models.Model):
return self.bool_value
else:
return self.string_value
def save_to_field(self, value):
if self.field.type in [
"text",
"number",
"single",
"datetime",
]:
self.string_value = value
self.save()
elif type == "multiple":
self.multiple_value = value.split(",")
self.save()
elif type == "checkbox":
self.bool_value = bool(value)
self.save()

View File

@@ -0,0 +1,45 @@
from rest_framework import permissions
from tacticalrmm.permissions import _has_perm, _has_perm_on_client, _has_perm_on_site
class ClientsPerms(permissions.BasePermission):
def has_permission(self, r, view):
if r.method == "GET":
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 SitesPerms(permissions.BasePermission):
def has_permission(self, r, view):
if r.method == "GET":
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 DeploymentPerms(permissions.BasePermission):
def has_permission(self, r, view):
if r.method == "GET":
return _has_perm(r, "can_list_deployments")
else:
return _has_perm(r, "can_manage_deployments")

View File

@@ -1,4 +1,9 @@
from rest_framework.serializers import ModelSerializer, ReadOnlyField, ValidationError
from rest_framework.serializers import (
ModelSerializer,
ReadOnlyField,
ValidationError,
SerializerMethodField,
)
from .models import Client, ClientCustomField, Deployment, Site, SiteCustomField
@@ -26,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
@@ -40,6 +47,8 @@ class SiteSerializer(ModelSerializer):
"custom_fields",
"agent_count",
"block_policy_inheritance",
"maintenance_mode",
"failing_checks",
)
def validate(self, val):
@@ -49,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
@@ -69,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
@@ -85,6 +116,8 @@ class ClientSerializer(ModelSerializer):
"sites",
"custom_fields",
"agent_count",
"maintenance_mode",
"failing_checks",
)
def validate(self, val):
@@ -94,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")
@@ -134,3 +148,15 @@ class DeploymentSerializer(ModelSerializer):
"install_flags",
"created",
]
class SiteAuditSerializer(ModelSerializer):
class Meta:
model = Site
fields = "__all__"
class ClientAuditSerializer(ModelSerializer):
class Meta:
model = Client
fields = "__all__"

View File

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

View File

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

View File

@@ -3,35 +3,43 @@ import re
import uuid
import pytz
from django.conf import settings
from django.shortcuts import get_object_or_404
from django.utils import timezone as djangotime
from loguru import logger
from rest_framework.permissions import AllowAny
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
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 (
ClientsPerms,
DeploymentPerms,
SitesPerms,
)
from .serializers import (
ClientCustomFieldSerializer,
ClientSerializer,
ClientTreeSerializer,
DeploymentSerializer,
SiteCustomFieldSerializer,
SiteSerializer,
)
logger.configure(**settings.LOG_CONFIG)
class GetAddClients(APIView):
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
@@ -68,13 +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):
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)
@@ -106,42 +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):
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, 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()
@@ -158,10 +171,16 @@ 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):
class GetUpdateDeleteSite(APIView):
permission_classes = [IsAuthenticated, SitesPerms]
def get(self, request, pk):
site = get_object_or_404(Site, pk=pk)
return Response(SiteSerializer(site).data)
@@ -201,51 +220,55 @@ class GetUpdateSite(APIView):
serializer.is_valid(raise_exception=True)
serializer.save()
return Response("Site was edited!")
return Response("Site was edited")
class DeleteSite(APIView):
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, 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(
request.data["expires"], "%Y-%m-%d %H:%M"
).astimezone(pytz.timezone("UTC"))
now = djangotime.now()
delta = expires - now
obj, token = AuthToken.objects.create(user=request.user, expiry=delta)
obj, token = AuthToken.objects.create(user=installer_user, expiry=delta)
flags = {
"power": request.data["power"],
@@ -254,7 +277,6 @@ class AgentDeployment(APIView):
}
Deployment(
client=client,
site=site,
expiry=expires,
mon_type=request.data["agenttype"],
@@ -263,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):

Some files were not shown because too many files have changed in this diff Show More