Compare commits

...

475 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
silversword411
403762d862 wip script additions 2021-08-31 22:45:53 -04:00
Juan J. Romero
6294530fa3 Securing NGINX added to docs 2021-08-31 15:45:47 +10:00
435 changed files with 27417 additions and 19121 deletions

View File

@@ -1,4 +1,4 @@
FROM python:3.9.6-slim
FROM python:3.9.9-slim
ENV TACTICAL_DIR /opt/tactical
ENV TACTICAL_READY_FILE ${TACTICAL_DIR}/tmp/tactical.ready
@@ -13,10 +13,6 @@ EXPOSE 8000 8383 8005
RUN groupadd -g 1000 tactical && \
useradd -u 1000 -g 1000 tactical
# Copy nats-api file
COPY natsapi/bin/nats-api /usr/local/bin/
RUN chmod +x /usr/local/bin/nats-api
# Copy dev python reqs
COPY .devcontainer/requirements.txt /

View File

@@ -209,6 +209,7 @@ services:
CERT_PRIV_KEY: ${CERT_PRIV_KEY}
APP_PORT: ${APP_PORT}
API_PORT: ${API_PORT}
DEV: 1
networks:
dev:
ipv4_address: ${DOCKER_NGINX_IP}

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,7 @@ EOF
"${VIRTUAL_ENV}"/bin/python manage.py load_chocos
"${VIRTUAL_ENV}"/bin/python manage.py load_community_scripts
"${VIRTUAL_ENV}"/bin/python manage.py reload_nats
"${VIRTUAL_ENV}"/bin/python manage.py create_natsapi_conf
"${VIRTUAL_ENV}"/bin/python manage.py create_installer_user
# create super user

View File

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

3
.gitignore vendored
View File

@@ -48,3 +48,6 @@ nats-rmm.conf
.mypy_cache
docs/site/
reset_db.sh
run_go_cmd.py
nats-api.conf

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

@@ -15,4 +15,5 @@ class Command(BaseCommand):
username=uuid.uuid4().hex,
is_installer_user=True,
password=User.objects.make_random_password(60), # type: ignore
block_dashboard_login=True,
)

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
@@ -24,6 +25,7 @@ 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)
@@ -62,7 +64,7 @@ class User(AbstractUser, BaseAuditModel):
"accounts.Role",
null=True,
blank=True,
related_name="roles",
related_name="users",
on_delete=models.SET_NULL,
)
@@ -79,6 +81,8 @@ class Role(BaseAuditModel):
is_superuser = models.BooleanField(default=False)
# agents
can_list_agents = models.BooleanField(default=False)
can_ping_agents = models.BooleanField(default=False)
can_use_mesh = models.BooleanField(default=False)
can_uninstall_agents = models.BooleanField(default=False)
can_update_agents = models.BooleanField(default=False)
@@ -90,54 +94,84 @@ class Role(BaseAuditModel):
can_install_agents = models.BooleanField(default=False)
can_run_scripts = models.BooleanField(default=False)
can_run_bulk = models.BooleanField(default=False)
can_recover_agents = models.BooleanField(default=False)
can_list_agent_history = models.BooleanField(default=False)
# core
can_list_notes = models.BooleanField(default=False)
can_manage_notes = models.BooleanField(default=False)
can_view_core_settings = models.BooleanField(default=False)
can_edit_core_settings = models.BooleanField(default=False)
can_do_server_maint = models.BooleanField(default=False)
can_code_sign = models.BooleanField(default=False)
can_run_urlactions = models.BooleanField(default=False)
can_view_customfields = models.BooleanField(default=False)
can_manage_customfields = models.BooleanField(default=False)
# checks
can_list_checks = models.BooleanField(default=False)
can_manage_checks = models.BooleanField(default=False)
can_run_checks = models.BooleanField(default=False)
# clients
can_list_clients = models.BooleanField(default=False)
can_manage_clients = models.BooleanField(default=False)
can_list_sites = models.BooleanField(default=False)
can_manage_sites = models.BooleanField(default=False)
can_list_deployments = models.BooleanField(default=False)
can_manage_deployments = models.BooleanField(default=False)
can_view_clients = models.ManyToManyField(
"clients.Client", related_name="role_clients", blank=True
)
can_view_sites = models.ManyToManyField(
"clients.Site", related_name="role_sites", blank=True
)
# automation
can_list_automation_policies = models.BooleanField(default=False)
can_manage_automation_policies = models.BooleanField(default=False)
# automated tasks
can_list_autotasks = models.BooleanField(default=False)
can_manage_autotasks = models.BooleanField(default=False)
can_run_autotasks = models.BooleanField(default=False)
# logs
can_view_auditlogs = models.BooleanField(default=False)
can_list_pendingactions = models.BooleanField(default=False)
can_manage_pendingactions = models.BooleanField(default=False)
can_view_debuglogs = models.BooleanField(default=False)
# scripts
can_list_scripts = models.BooleanField(default=False)
can_manage_scripts = models.BooleanField(default=False)
# alerts
can_list_alerts = models.BooleanField(default=False)
can_manage_alerts = models.BooleanField(default=False)
can_list_alerttemplates = models.BooleanField(default=False)
can_manage_alerttemplates = models.BooleanField(default=False)
# win services
can_manage_winsvcs = models.BooleanField(default=False)
# software
can_list_software = models.BooleanField(default=False)
can_manage_software = models.BooleanField(default=False)
# windows updates
can_manage_winupdates = models.BooleanField(default=False)
# accounts
can_list_accounts = models.BooleanField(default=False)
can_manage_accounts = models.BooleanField(default=False)
can_list_roles = models.BooleanField(default=False)
can_manage_roles = models.BooleanField(default=False)
# authentication
can_list_api_keys = models.BooleanField(default=False)
can_manage_api_keys = models.BooleanField(default=False)
def __str__(self):
return self.name
@@ -148,42 +182,19 @@ class Role(BaseAuditModel):
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 perms():
return [
"is_superuser",
"can_use_mesh",
"can_uninstall_agents",
"can_update_agents",
"can_edit_agent",
"can_manage_procs",
"can_view_eventlogs",
"can_send_cmd",
"can_reboot_agents",
"can_install_agents",
"can_run_scripts",
"can_run_bulk",
"can_manage_notes",
"can_view_core_settings",
"can_edit_core_settings",
"can_do_server_maint",
"can_code_sign",
"can_manage_checks",
"can_run_checks",
"can_manage_clients",
"can_manage_sites",
"can_manage_deployments",
"can_manage_automation_policies",
"can_manage_autotasks",
"can_run_autotasks",
"can_view_auditlogs",
"can_manage_pendingactions",
"can_view_debuglogs",
"can_manage_scripts",
"can_manage_alerts",
"can_manage_winsvcs",
"can_manage_software",
"can_manage_winupdates",
"can_manage_accounts",
"can_manage_roles",
]
def serialize(apikey):
from .serializers import APIKeyAuditSerializer
return APIKeyAuditSerializer(apikey).data

View File

@@ -6,14 +6,38 @@ from tacticalrmm.permissions import _has_perm
class AccountsPerms(permissions.BasePermission):
def has_permission(self, r, view):
if r.method == "GET":
return True
return _has_perm(r, "can_list_accounts")
else:
return _has_perm(r, "can_manage_accounts")
# 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 True
return _has_perm(r, "can_list_roles")
else:
return _has_perm(r, "can_manage_roles")
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, Role
from .models import APIKey, User, Role
class UserUISerializer(ModelSerializer):
@@ -17,6 +21,7 @@ class UserUISerializer(ModelSerializer):
"client_tree_splitter",
"loading_bar_color",
"clear_search_when_switching",
"block_dashboard_login",
]
@@ -33,6 +38,7 @@ class UserSerializer(ModelSerializer):
"last_login",
"last_login_ip",
"role",
"block_dashboard_login",
]
@@ -55,12 +61,38 @@ class TOTPSetupSerializer(ModelSerializer):
class RoleSerializer(ModelSerializer):
user_count = SerializerMethodField()
class Meta:
model = Role
fields = "__all__"
def get_user_count(self, obj):
return obj.users.count()
class RoleAuditSerializer(ModelSerializer):
class Meta:
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"}
@@ -288,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()
@@ -313,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,7 +9,8 @@ urlpatterns = [
path("users/reset_totp/", views.UserActions.as_view()),
path("users/setup_totp/", views.TOTPSetup.as_view()),
path("users/ui/", views.UserUI.as_view()),
path("permslist/", views.PermsList.as_view()),
path("roles/", views.GetAddRoles.as_view()),
path("<int:pk>/role/", views.GetUpdateDeleteRole.as_view()),
path("roles/<int:pk>/", views.GetUpdateDeleteRole.as_view()),
path("apikeys/", views.GetAddAPIKeys.as_view()),
path("apikeys/<int:pk>/", views.GetUpdateDeleteAPIKey.as_view()),
]

View File

@@ -13,9 +13,10 @@ from rest_framework.response import Response
from rest_framework.views import APIView
from tacticalrmm.utils import notify_error
from .models import Role, User
from .permissions import AccountsPerms, RolesPerms
from .models import APIKey, Role, User
from .permissions import APIKeyPerms, AccountsPerms, RolesPerms
from .serializers import (
APIKeySerializer,
RoleSerializer,
TOTPSetupSerializer,
UserSerializer,
@@ -43,10 +44,13 @@ class CheckCreds(KnoxLoginView):
AuditLog.audit_user_failed_login(
request.data["username"], debug_info={"ip": request._client_ip}
)
return Response("bad credentials", status=status.HTTP_400_BAD_REQUEST)
return notify_error("Bad credentials")
user = serializer.validated_data["user"]
if user.block_dashboard_login:
return notify_error("Bad credentials")
# if totp token not set modify response to notify frontend
if not user.totp_key:
login(request, user)
@@ -68,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)
@@ -92,7 +99,7 @@ class LoginView(KnoxLoginView):
AuditLog.audit_user_failed_twofactor(
request.data["username"], debug_info={"ip": request._client_ip}
)
return Response("bad credentials", status=status.HTTP_400_BAD_REQUEST)
return notify_error("Bad credentials")
class GetAddUsers(APIView):
@@ -123,8 +130,10 @@ 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"]
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
@@ -215,11 +224,6 @@ class UserUI(APIView):
return Response("ok")
class PermsList(APIView):
def get(self, request):
return Response(Role.perms())
class GetAddRoles(APIView):
permission_classes = [IsAuthenticated, RolesPerms]
@@ -231,7 +235,7 @@ class GetAddRoles(APIView):
serializer = RoleSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response("ok")
return Response("Role was added")
class GetUpdateDeleteRole(APIView):
@@ -246,9 +250,48 @@ class GetUpdateDeleteRole(APIView):
serializer = RoleSerializer(instance=role, data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response("ok")
return Response("Role was edited")
def delete(self, request, pk):
role = get_object_or_404(Role, pk=pk)
role.delete()
return Response("ok")
return Response("Role was removed")
class GetAddAPIKeys(APIView):
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

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

@@ -22,9 +22,12 @@ from packaging import version as pyver
from core.models import TZ_CHOICES, CoreSettings
from logs.models import BaseAuditModel, DebugLog
from tacticalrmm.models import PermissionQuerySet
class Agent(BaseAuditModel):
objects = PermissionQuerySet.as_manager()
version = models.CharField(default="0.1.0", max_length=255)
salt_ver = models.CharField(default="1.0.3", max_length=255)
operating_system = models.CharField(null=True, blank=True, max_length=255)
@@ -33,7 +36,7 @@ class Agent(BaseAuditModel):
hostname = models.CharField(max_length=255)
salt_id = models.CharField(null=True, blank=True, max_length=255)
local_ip = models.TextField(null=True, blank=True) # deprecated
agent_id = models.CharField(max_length=200)
agent_id = models.CharField(max_length=200, unique=True)
last_seen = models.DateTimeField(null=True, blank=True)
services = models.JSONField(null=True, blank=True)
public_ip = models.CharField(null=True, max_length=255)
@@ -87,6 +90,7 @@ class Agent(BaseAuditModel):
)
def save(self, *args, **kwargs):
from automation.tasks import generate_agent_checks_task
# get old agent if exists
old_agent = Agent.objects.get(pk=self.pk) if self.pk else None
@@ -94,7 +98,7 @@ class Agent(BaseAuditModel):
# check if new agent has been created
# or check if policy have changed on agent
# or if site has changed on agent and if so generate-policies
# or if site has changed on agent and if so generate policies
# or if agent was changed from server or workstation
if (
not old_agent
@@ -103,8 +107,7 @@ class Agent(BaseAuditModel):
or (old_agent.monitoring_type != self.monitoring_type)
or (old_agent.block_policy_inheritance != self.block_policy_inheritance)
):
self.generate_checks_from_policies()
self.generate_tasks_from_policies()
generate_agent_checks_task.delay(agents=[self.pk], create_tasks=True)
def __str__(self):
return self.hostname
@@ -413,12 +416,6 @@ class Agent(BaseAuditModel):
update.action = "approve"
update.save(update_fields=["action"])
DebugLog.info(
agent=self,
log_type="windows_updates",
message=f"Approving windows updates on {self.hostname}",
)
# returns agent policy merged with a client or site specific policy
def get_patch_policy(self):
@@ -705,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)
@@ -747,8 +744,8 @@ class Agent(BaseAuditModel):
try:
ret = msgpack.loads(msg.data) # type: ignore
except Exception as e:
DebugLog.error(agent=self, log_type="agent_issues", message=e)
ret = str(e)
DebugLog.error(agent=self, log_type="agent_issues", message=ret)
await nc.close()
return ret
@@ -867,6 +864,8 @@ RECOVERY_CHOICES = [
class RecoveryAction(models.Model):
objects = PermissionQuerySet.as_manager()
agent = models.ForeignKey(
Agent,
related_name="recoveryactions",
@@ -881,6 +880,8 @@ class RecoveryAction(models.Model):
class Note(models.Model):
objects = PermissionQuerySet.as_manager()
agent = models.ForeignKey(
Agent,
related_name="notes",
@@ -901,6 +902,8 @@ class Note(models.Model):
class AgentCustomField(models.Model):
objects = PermissionQuerySet.as_manager()
agent = models.ForeignKey(
Agent,
related_name="custom_fields",
@@ -961,6 +964,8 @@ AGENT_HISTORY_STATUS = (("success", "Success"), ("failure", "Failure"))
class AgentHistory(models.Model):
objects = PermissionQuerySet.as_manager()
agent = models.ForeignKey(
Agent,
related_name="history",
@@ -974,7 +979,7 @@ class AgentHistory(models.Model):
status = models.CharField(
max_length=50, choices=AGENT_HISTORY_STATUS, default="success"
)
username = models.CharField(max_length=50, default="system")
username = models.CharField(max_length=255, default="system")
results = models.TextField(null=True, blank=True)
script = models.ForeignKey(
"scripts.Script",

View File

@@ -1,16 +1,42 @@
from rest_framework import permissions
from tacticalrmm.permissions import _has_perm
from tacticalrmm.permissions import _has_perm, _has_perm_on_agent
class AgentPerms(permissions.BasePermission):
def has_permission(self, r, view):
if r.method == "GET":
if "agent_id" in view.kwargs.keys():
return _has_perm(r, "can_list_agents") and _has_perm_on_agent(
r.user, view.kwargs["agent_id"]
)
else:
return _has_perm(r, "can_list_agents")
elif r.method == "DELETE":
return _has_perm(r, "can_uninstall_agents") and _has_perm_on_agent(
r.user, view.kwargs["agent_id"]
)
else:
if r.path == "/agents/maintenance/bulk/":
return _has_perm(r, "can_edit_agent")
else:
return _has_perm(r, "can_edit_agent") and _has_perm_on_agent(
r.user, view.kwargs["agent_id"]
)
class RecoverAgentPerms(permissions.BasePermission):
def has_permission(self, r, view):
return _has_perm(r, "can_recover_agents") and _has_perm_on_agent(
r.user, view.kwargs["agent_id"]
)
class MeshPerms(permissions.BasePermission):
def has_permission(self, r, view):
return _has_perm(r, "can_use_mesh")
class UninstallPerms(permissions.BasePermission):
def has_permission(self, r, view):
return _has_perm(r, "can_uninstall_agents")
return _has_perm(r, "can_use_mesh") and _has_perm_on_agent(
r.user, view.kwargs["agent_id"]
)
class UpdateAgentPerms(permissions.BasePermission):
@@ -18,29 +44,39 @@ class UpdateAgentPerms(permissions.BasePermission):
return _has_perm(r, "can_update_agents")
class EditAgentPerms(permissions.BasePermission):
class PingAgentPerms(permissions.BasePermission):
def has_permission(self, r, view):
return _has_perm(r, "can_edit_agent")
return _has_perm(r, "can_ping_agents") and _has_perm_on_agent(
r.user, view.kwargs["agent_id"]
)
class ManageProcPerms(permissions.BasePermission):
def has_permission(self, r, view):
return _has_perm(r, "can_manage_procs")
return _has_perm(r, "can_manage_procs") and _has_perm_on_agent(
r.user, view.kwargs["agent_id"]
)
class EvtLogPerms(permissions.BasePermission):
def has_permission(self, r, view):
return _has_perm(r, "can_view_eventlogs")
return _has_perm(r, "can_view_eventlogs") and _has_perm_on_agent(
r.user, view.kwargs["agent_id"]
)
class SendCMDPerms(permissions.BasePermission):
def has_permission(self, r, view):
return _has_perm(r, "can_send_cmd")
return _has_perm(r, "can_send_cmd") and _has_perm_on_agent(
r.user, view.kwargs["agent_id"]
)
class RebootAgentPerms(permissions.BasePermission):
def has_permission(self, r, view):
return _has_perm(r, "can_reboot_agents")
return _has_perm(r, "can_reboot_agents") and _has_perm_on_agent(
r.user, view.kwargs["agent_id"]
)
class InstallAgentPerms(permissions.BasePermission):
@@ -50,14 +86,38 @@ class InstallAgentPerms(permissions.BasePermission):
class RunScriptPerms(permissions.BasePermission):
def has_permission(self, r, view):
return _has_perm(r, "can_run_scripts")
return _has_perm(r, "can_run_scripts") and _has_perm_on_agent(
r.user, view.kwargs["agent_id"]
)
class ManageNotesPerms(permissions.BasePermission):
class AgentNotesPerms(permissions.BasePermission):
def has_permission(self, r, view):
return _has_perm(r, "can_manage_notes")
# permissions for GET /agents/notes/ endpoint
if r.method == "GET":
# permissions for /agents/<agent_id>/notes endpoint
if "agent_id" in view.kwargs.keys():
return _has_perm(r, "can_list_notes") and _has_perm_on_agent(
r.user, view.kwargs["agent_id"]
)
else:
return _has_perm(r, "can_list_notes")
else:
return _has_perm(r, "can_manage_notes")
class RunBulkPerms(permissions.BasePermission):
def has_permission(self, r, view):
return _has_perm(r, "can_run_bulk")
class AgentHistoryPerms(permissions.BasePermission):
def has_permission(self, r, view):
if "agent_id" in view.kwargs.keys():
return _has_perm(r, "can_list_agent_history") and _has_perm_on_agent(
r.user, view.kwargs["agent_id"]
)
else:
return _has_perm(r, "can_list_agent_history")

View File

@@ -1,14 +1,30 @@
import pytz
from clients.serializers import ClientSerializer
from rest_framework import serializers
from tacticalrmm.utils import get_default_timezone
from winupdate.serializers import WinUpdatePolicySerializer
from .models import Agent, AgentCustomField, Note, AgentHistory
class AgentCustomFieldSerializer(serializers.ModelSerializer):
class Meta:
model = AgentCustomField
fields = (
"id",
"field",
"agent",
"value",
"string_value",
"bool_value",
"multiple_value",
)
extra_kwargs = {
"string_value": {"write_only": True},
"bool_value": {"write_only": True},
"multiple_value": {"write_only": True},
}
class AgentSerializer(serializers.ModelSerializer):
# for vue
winupdatepolicy = WinUpdatePolicySerializer(many=True, read_only=True)
status = serializers.ReadOnlyField()
cpu_model = serializers.ReadOnlyField()
@@ -19,28 +35,18 @@ class AgentSerializer(serializers.ModelSerializer):
checks = serializers.ReadOnlyField()
timezone = serializers.ReadOnlyField()
all_timezones = serializers.SerializerMethodField()
client_name = serializers.ReadOnlyField(source="client.name")
client = serializers.ReadOnlyField(source="client.name")
site_name = serializers.ReadOnlyField(source="site.name")
custom_fields = AgentCustomFieldSerializer(many=True, read_only=True)
patches_last_installed = serializers.ReadOnlyField()
last_seen = serializers.ReadOnlyField()
def get_all_timezones(self, obj):
return pytz.all_timezones
class Meta:
model = Agent
exclude = [
"last_seen",
]
class AgentOverdueActionSerializer(serializers.ModelSerializer):
class Meta:
model = Agent
fields = [
"pk",
"overdue_email_alert",
"overdue_text_alert",
"overdue_dashboard_alert",
]
exclude = ["id"]
class AgentTableSerializer(serializers.ModelSerializer):
@@ -88,10 +94,9 @@ class AgentTableSerializer(serializers.ModelSerializer):
class Meta:
model = Agent
fields = [
"id",
"agent_id",
"alert_template",
"hostname",
"agent_id",
"site_name",
"client_name",
"monitoring_type",
@@ -115,58 +120,6 @@ class AgentTableSerializer(serializers.ModelSerializer):
depth = 2
class AgentCustomFieldSerializer(serializers.ModelSerializer):
class Meta:
model = AgentCustomField
fields = (
"id",
"field",
"agent",
"value",
"string_value",
"bool_value",
"multiple_value",
)
extra_kwargs = {
"string_value": {"write_only": True},
"bool_value": {"write_only": True},
"multiple_value": {"write_only": True},
}
class AgentEditSerializer(serializers.ModelSerializer):
winupdatepolicy = WinUpdatePolicySerializer(many=True, read_only=True)
all_timezones = serializers.SerializerMethodField()
client = ClientSerializer(read_only=True)
custom_fields = AgentCustomFieldSerializer(many=True, read_only=True)
def get_all_timezones(self, obj):
return pytz.all_timezones
class Meta:
model = Agent
fields = [
"id",
"hostname",
"client",
"site",
"monitoring_type",
"description",
"time_zone",
"timezone",
"check_interval",
"overdue_time",
"offline_time",
"overdue_text_alert",
"overdue_email_alert",
"overdue_dashboard_alert",
"all_timezones",
"winupdatepolicy",
"policy",
"custom_fields",
]
class WinAgentSerializer(serializers.ModelSerializer):
class Meta:
model = Agent
@@ -180,27 +133,22 @@ class AgentHostnameSerializer(serializers.ModelSerializer):
class Meta:
model = Agent
fields = (
"id",
"hostname",
"pk",
"agent_id",
"client",
"site",
)
class NoteSerializer(serializers.ModelSerializer):
class AgentNoteSerializer(serializers.ModelSerializer):
username = serializers.ReadOnlyField(source="user.username")
agent_id = serializers.ReadOnlyField(source="agent.agent_id")
class Meta:
model = Note
fields = "__all__"
class NotesSerializer(serializers.ModelSerializer):
notes = NoteSerializer(many=True, read_only=True)
class Meta:
model = Agent
fields = ["hostname", "pk", "notes"]
fields = ("pk", "entry_time", "agent", "user", "note", "username", "agent_id")
extra_kwargs = {"agent": {"write_only": True}, "user": {"write_only": True}}
class AgentHistorySerializer(serializers.ModelSerializer):
@@ -212,8 +160,8 @@ class AgentHistorySerializer(serializers.ModelSerializer):
fields = "__all__"
def get_time(self, history):
timezone = get_default_timezone()
return history.time.astimezone(timezone).strftime("%m %d %Y %H:%M:%S")
tz = self.context["default_tz"]
return history.time.astimezone(tz).strftime("%m %d %Y %H:%M:%S")
class AgentAuditSerializer(serializers.ModelSerializer):

View File

@@ -1,27 +1,26 @@
import asyncio
import datetime as dt
import random
import urllib.parse
from time import sleep
from typing import Union
from alerts.models import Alert
from core.models import CodeSignToken, CoreSettings
from core.models import CoreSettings
from django.conf import settings
from django.utils import timezone as djangotime
from logs.models import DebugLog, PendingAction
from packaging import version as pyver
from scripts.models import Script
from tacticalrmm.celery import app
from tacticalrmm.utils import run_nats_api_cmd
from agents.models import Agent
from agents.utils import get_winagent_url
from tacticalrmm.utils import AGENT_DEFER
def agent_update(pk: int, codesigntoken: str = None, force: bool = False) -> str:
from agents.utils import get_exegen_url
def agent_update(agent_id: str, force: bool = False) -> str:
agent = Agent.objects.get(pk=pk)
agent = Agent.objects.get(agent_id=agent_id)
if pyver.parse(agent.version) <= pyver.parse("1.3.0"):
return "not supported"
@@ -31,19 +30,13 @@ def agent_update(pk: int, codesigntoken: str = None, force: bool = False) -> str
DebugLog.warning(
agent=agent,
log_type="agent_issues",
message=f"Unable to determine arch on {agent.hostname}({agent.pk}). Skipping agent update.",
message=f"Unable to determine arch on {agent.hostname}({agent.agent_id}). Skipping agent update.",
)
return "noarch"
version = settings.LATEST_AGENT_VER
inno = agent.win_inno_exe
if codesigntoken is not None and pyver.parse(version) >= pyver.parse("1.5.0"):
base_url = get_exegen_url() + "/api/v1/winagents/?"
params = {"version": version, "arch": agent.arch, "token": codesigntoken}
url = base_url + urllib.parse.urlencode(params)
else:
url = agent.winagent_dl
url = get_winagent_url(agent.arch)
if not force:
if agent.pendingactions.filter(
@@ -76,31 +69,21 @@ def agent_update(pk: int, codesigntoken: str = None, force: bool = False) -> str
@app.task
def force_code_sign(pks: list[int]) -> None:
try:
token = CodeSignToken.objects.first().tokenv # type:ignore
except:
return
chunks = (pks[i : i + 50] for i in range(0, len(pks), 50))
def force_code_sign(agent_ids: list[str]) -> None:
chunks = (agent_ids[i : i + 50] for i in range(0, len(agent_ids), 50))
for chunk in chunks:
for pk in chunk:
agent_update(pk=pk, codesigntoken=token, force=True)
for agent_id in chunk:
agent_update(agent_id=agent_id, force=True)
sleep(0.05)
sleep(4)
@app.task
def send_agent_update_task(pks: list[int]) -> None:
try:
codesigntoken = CodeSignToken.objects.first().token # type:ignore
except:
codesigntoken = None
chunks = (pks[i : i + 30] for i in range(0, len(pks), 30))
def send_agent_update_task(agent_ids: list[str]) -> None:
chunks = (agent_ids[i : i + 50] for i in range(0, len(agent_ids), 50))
for chunk in chunks:
for pk in chunk:
agent_update(pk, codesigntoken)
for agent_id in chunk:
agent_update(agent_id)
sleep(0.05)
sleep(4)
@@ -111,22 +94,17 @@ def auto_self_agent_update_task() -> None:
if not core.agent_auto_update: # type:ignore
return
try:
codesigntoken = CodeSignToken.objects.first().token # type:ignore
except:
codesigntoken = None
q = Agent.objects.only("pk", "version")
pks: list[int] = [
i.pk
q = Agent.objects.only("agent_id", "version")
agent_ids: list[str] = [
i.agent_id
for i in q
if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER)
]
chunks = (pks[i : i + 30] for i in range(0, len(pks), 30))
chunks = (agent_ids[i : i + 30] for i in range(0, len(agent_ids), 30))
for chunk in chunks:
for pk in chunk:
agent_update(pk, codesigntoken)
for agent_id in chunk:
agent_update(agent_id)
sleep(0.05)
sleep(4)
@@ -290,7 +268,7 @@ def run_script_email_results_task(
server.send_message(msg)
server.quit()
except Exception as e:
DebugLog.error(message=e)
DebugLog.error(message=str(e))
@app.task
@@ -321,25 +299,6 @@ def clear_faults_task(older_than_days: int) -> None:
)
@app.task
def get_wmi_task() -> None:
agents = Agent.objects.only(
"pk", "agent_id", "last_seen", "overdue_time", "offline_time"
)
ids = [i.agent_id for i in agents if i.status == "online"]
run_nats_api_cmd("wmi", ids, timeout=45)
@app.task
def agent_checkin_task() -> None:
run_nats_api_cmd("checkin", timeout=30)
@app.task
def agent_getinfo_task() -> None:
run_nats_api_cmd("agentinfo", timeout=30)
@app.task
def prune_agent_history(older_than_days: int) -> str:
from .models import AgentHistory
@@ -353,9 +312,7 @@ def prune_agent_history(older_than_days: int) -> str:
@app.task
def handle_agents_task() -> None:
q = Agent.objects.prefetch_related("pendingactions", "autotasks").only(
"pk", "agent_id", "version", "last_seen", "overdue_time", "offline_time"
)
q = Agent.objects.defer(*AGENT_DEFER)
agents = [
i
for i in q

File diff suppressed because it is too large Load Diff

View File

@@ -1,33 +1,44 @@
from django.urls import path
from . import views
from checks.views import GetAddChecks
from autotasks.views import GetAddAutoTasks
from logs.views import PendingActions
urlpatterns = [
path("listagents/", views.AgentsTableList.as_view()),
path("listagentsnodetail/", views.list_agents_no_detail),
path("<int:pk>/agenteditdetails/", views.agent_edit_details),
path("overdueaction/", views.overdue_action),
path("sendrawcmd/", views.send_raw_cmd),
path("<pk>/agentdetail/", views.agent_detail),
path("<int:pk>/meshcentral/", views.meshcentral),
# agent views
path("", views.GetAgents.as_view()),
path("<agent:agent_id>/", views.GetUpdateDeleteAgent.as_view()),
path("<agent:agent_id>/cmd/", views.send_raw_cmd),
path("<agent:agent_id>/runscript/", views.run_script),
path("<agent:agent_id>/wmi/", views.WMI.as_view()),
path("<agent:agent_id>/recover/", views.recover),
path("<agent:agent_id>/reboot/", views.Reboot.as_view()),
path("<agent:agent_id>/ping/", views.ping),
# alias for checks get view
path("<agent:agent_id>/checks/", GetAddChecks.as_view()),
# alias for autotasks get view
path("<agent:agent_id>/tasks/", GetAddAutoTasks.as_view()),
# alias for pending actions get view
path("<agent:agent_id>/pendingactions/", PendingActions.as_view()),
# agent remote background
path("<agent:agent_id>/meshcentral/", views.AgentMeshCentral.as_view()),
path("<agent:agent_id>/meshcentral/recover/", views.AgentMeshCentral.as_view()),
path("<agent:agent_id>/processes/", views.AgentProcesses.as_view()),
path("<agent:agent_id>/processes/<int:pid>/", views.AgentProcesses.as_view()),
path("<agent:agent_id>/eventlog/<str:logtype>/<int:days>/", views.get_event_log),
# agent history
path("history/", views.AgentHistoryView.as_view()),
path("<agent:agent_id>/history/", views.AgentHistoryView.as_view()),
# agent notes
path("notes/", views.GetAddNotes.as_view()),
path("notes/<int:pk>/", views.GetEditDeleteNote.as_view()),
path("<agent:agent_id>/notes/", views.GetAddNotes.as_view()),
# bulk actions
path("maintenance/bulk/", views.agent_maintenance),
path("actions/bulk/", views.bulk),
path("versions/", views.get_agent_versions),
path("update/", views.update_agents),
path("installer/", views.install_agent),
path("<str:arch>/getmeshexe/", views.get_mesh_exe),
path("uninstall/", views.uninstall),
path("editagent/", views.edit_agent),
path("<pk>/geteventlog/<logtype>/<days>/", views.get_event_log),
path("getagentversions/", views.get_agent_versions),
path("updateagents/", views.update_agents),
path("<pk>/getprocs/", views.get_processes),
path("<pk>/<pid>/killproc/", views.kill_proc),
path("reboot/", views.Reboot.as_view()),
path("installagent/", views.install_agent),
path("<int:pk>/ping/", views.ping),
path("recover/", views.recover),
path("runscript/", views.run_script),
path("<int:pk>/recovermesh/", views.recover_mesh),
path("<int:pk>/notes/", views.GetAddNotes.as_view()),
path("<int:pk>/note/", views.GetEditDeleteNote.as_view()),
path("bulk/", views.bulk),
path("maintenance/", views.agent_maintenance),
path("<int:pk>/wmi/", views.WMI.as_view()),
path("history/<int:pk>/", views.AgentHistoryView.as_view()),
]

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

@@ -8,53 +8,242 @@ import time
from django.conf import settings
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from django.db.models import Q
from packaging import version as pyver
from rest_framework import status
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.exceptions import PermissionDenied
from core.models import CoreSettings
from logs.models import AuditLog, DebugLog, PendingAction
from scripts.models import Script
from scripts.tasks import handle_bulk_command_task, handle_bulk_script_task
from tacticalrmm.utils import get_default_timezone, notify_error, reload_nats
from tacticalrmm.utils import (
get_default_timezone,
notify_error,
reload_nats,
AGENT_DEFER,
)
from winupdate.serializers import WinUpdatePolicySerializer
from winupdate.tasks import bulk_check_for_updates_task, bulk_install_updates_task
from tacticalrmm.permissions import (
_has_perm_on_agent,
_has_perm_on_client,
_has_perm_on_site,
)
from .models import Agent, AgentCustomField, Note, RecoveryAction, AgentHistory
from .permissions import (
EditAgentPerms,
AgentHistoryPerms,
AgentPerms,
EvtLogPerms,
InstallAgentPerms,
ManageNotesPerms,
RecoverAgentPerms,
AgentNotesPerms,
ManageProcPerms,
MeshPerms,
RebootAgentPerms,
RunBulkPerms,
RunScriptPerms,
SendCMDPerms,
UninstallPerms,
PingAgentPerms,
UpdateAgentPerms,
)
from .serializers import (
AgentCustomFieldSerializer,
AgentEditSerializer,
AgentHistorySerializer,
AgentHostnameSerializer,
AgentOverdueActionSerializer,
AgentSerializer,
AgentTableSerializer,
NoteSerializer,
NotesSerializer,
AgentNoteSerializer,
)
from .tasks import run_script_email_results_task, send_agent_update_task
@api_view()
class GetAgents(APIView):
permission_classes = [IsAuthenticated, AgentPerms]
def get(self, request):
if "site" in request.query_params.keys():
filter = Q(site_id=request.query_params["site"])
elif "client" in request.query_params.keys():
filter = Q(site__client_id=request.query_params["client"])
else:
filter = Q()
# by default detail=true
if (
"detail" not in request.query_params.keys()
or "detail" in request.query_params.keys()
and request.query_params["detail"] == "true"
):
agents = (
Agent.objects.filter_by_role(request.user) # type: ignore
.select_related("site", "policy", "alert_template")
.prefetch_related("agentchecks")
.filter(filter)
.defer(*AGENT_DEFER)
)
ctx = {"default_tz": get_default_timezone()}
serializer = AgentTableSerializer(agents, many=True, context=ctx)
# if detail=false
else:
agents = (
Agent.objects.filter_by_role(request.user) # type: ignore
.select_related("site")
.filter(filter)
.only("agent_id", "hostname", "site")
)
serializer = AgentHostnameSerializer(agents, many=True)
return Response(serializer.data)
class GetUpdateDeleteAgent(APIView):
permission_classes = [IsAuthenticated, AgentPerms]
# get agent details
def get(self, request, agent_id):
agent = get_object_or_404(Agent, agent_id=agent_id)
return Response(AgentSerializer(agent).data)
# edit agent
def put(self, request, agent_id):
agent = get_object_or_404(Agent, agent_id=agent_id)
a_serializer = AgentSerializer(instance=agent, data=request.data, partial=True)
a_serializer.is_valid(raise_exception=True)
a_serializer.save()
if "winupdatepolicy" in request.data.keys():
policy = agent.winupdatepolicy.get() # type: ignore
p_serializer = WinUpdatePolicySerializer(
instance=policy, data=request.data["winupdatepolicy"][0]
)
p_serializer.is_valid(raise_exception=True)
p_serializer.save()
if "custom_fields" in request.data.keys():
for field in request.data["custom_fields"]:
custom_field = field
custom_field["agent"] = agent.id # type: ignore
if AgentCustomField.objects.filter(
field=field["field"], agent=agent.id # type: ignore
):
value = AgentCustomField.objects.get(
field=field["field"], agent=agent.id # type: ignore
)
serializer = AgentCustomFieldSerializer(
instance=value, data=custom_field
)
serializer.is_valid(raise_exception=True)
serializer.save()
else:
serializer = AgentCustomFieldSerializer(data=custom_field)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response("The agent was updated successfully")
# uninstall agent
def delete(self, request, agent_id):
agent = get_object_or_404(Agent, agent_id=agent_id)
asyncio.run(agent.nats_cmd({"func": "uninstall"}, wait=False))
name = agent.hostname
agent.delete()
reload_nats()
return Response(f"{name} will now be uninstalled.")
class AgentProcesses(APIView):
permission_classes = [IsAuthenticated, ManageProcPerms]
# list agent processes
def get(self, request, agent_id):
agent = get_object_or_404(Agent, agent_id=agent_id)
r = asyncio.run(agent.nats_cmd(data={"func": "procs"}, timeout=5))
if r == "timeout" or r == "natsdown":
return notify_error("Unable to contact the agent")
return Response(r)
# kill agent process
def delete(self, request, agent_id, pid):
agent = get_object_or_404(Agent, agent_id=agent_id)
r = asyncio.run(
agent.nats_cmd({"func": "killproc", "procpid": int(pid)}, timeout=15)
)
if r == "timeout" or r == "natsdown":
return notify_error("Unable to contact the agent")
elif r != "ok":
return notify_error(r)
return Response(f"Process with PID: {pid} was ended successfully")
class AgentMeshCentral(APIView):
permission_classes = [IsAuthenticated, MeshPerms]
# get mesh urls
def get(self, request, agent_id):
agent = get_object_or_404(Agent, agent_id=agent_id)
core = CoreSettings.objects.first()
token = agent.get_login_token(
key=core.mesh_token,
user=f"user//{core.mesh_username.lower()}", # type:ignore
)
if token == "err":
return notify_error("Invalid mesh token")
control = f"{core.mesh_site}/?login={token}&gotonode={agent.mesh_node_id}&viewmode=11&hide=31" # type:ignore
terminal = f"{core.mesh_site}/?login={token}&gotonode={agent.mesh_node_id}&viewmode=12&hide=31" # type:ignore
file = f"{core.mesh_site}/?login={token}&gotonode={agent.mesh_node_id}&viewmode=13&hide=31" # type:ignore
AuditLog.audit_mesh_session(
username=request.user.username,
agent=agent,
debug_info={"ip": request._client_ip},
)
ret = {
"hostname": agent.hostname,
"control": control,
"terminal": terminal,
"file": file,
"status": agent.status,
"client": agent.client.name,
"site": agent.site.name,
}
return Response(ret)
# start mesh recovery
def post(self, request, agent_id):
agent = get_object_or_404(Agent, agent_id=agent_id)
data = {"func": "recover", "payload": {"mode": "mesh"}}
r = asyncio.run(agent.nats_cmd(data, timeout=90))
if r != "ok":
return notify_error("Unable to contact the agent")
return Response(f"Repaired mesh agent on {agent.hostname}")
@api_view(["GET"])
@permission_classes([IsAuthenticated, AgentPerms])
def get_agent_versions(request):
agents = Agent.objects.prefetch_related("site").only("pk", "hostname")
agents = (
Agent.objects.filter_by_role(request.user)
.prefetch_related("site")
.only("pk", "hostname")
)
return Response(
{
"versions": [settings.LATEST_AGENT_VER],
@@ -66,20 +255,24 @@ def get_agent_versions(request):
@api_view(["POST"])
@permission_classes([IsAuthenticated, UpdateAgentPerms])
def update_agents(request):
q = Agent.objects.filter(pk__in=request.data["pks"]).only("pk", "version")
pks: list[int] = [
i.pk
q = (
Agent.objects.filter_by_role(request.user)
.filter(agent_id__in=request.data["agent_ids"])
.only("agent_id", "version")
)
agent_ids: list[str] = [
i.agent_id
for i in q
if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER)
]
send_agent_update_task.delay(pks=pks)
send_agent_update_task.delay(agent_ids=agent_ids)
return Response("ok")
@api_view()
@permission_classes([IsAuthenticated, UninstallPerms])
def ping(request, pk):
agent = get_object_or_404(Agent, pk=pk)
@api_view(["GET"])
@permission_classes([IsAuthenticated, PingAgentPerms])
def ping(request, agent_id):
agent = get_object_or_404(Agent, agent_id=agent_id)
status = "offline"
attempts = 0
while 1:
@@ -97,131 +290,12 @@ def ping(request, pk):
return Response({"name": agent.hostname, "status": status})
@api_view(["DELETE"])
@permission_classes([IsAuthenticated, UninstallPerms])
def uninstall(request):
agent = get_object_or_404(Agent, pk=request.data["pk"])
asyncio.run(agent.nats_cmd({"func": "uninstall"}, wait=False))
name = agent.hostname
agent.delete()
reload_nats()
return Response(f"{name} will now be uninstalled.")
@api_view(["PATCH", "PUT"])
@permission_classes([IsAuthenticated, EditAgentPerms])
def edit_agent(request):
agent = get_object_or_404(Agent, pk=request.data["id"])
a_serializer = AgentEditSerializer(instance=agent, data=request.data, partial=True)
a_serializer.is_valid(raise_exception=True)
a_serializer.save()
if "winupdatepolicy" in request.data.keys():
policy = agent.winupdatepolicy.get() # type: ignore
p_serializer = WinUpdatePolicySerializer(
instance=policy, data=request.data["winupdatepolicy"][0]
)
p_serializer.is_valid(raise_exception=True)
p_serializer.save()
if "custom_fields" in request.data.keys():
for field in request.data["custom_fields"]:
custom_field = field
custom_field["agent"] = agent.id # type: ignore
if AgentCustomField.objects.filter(
field=field["field"], agent=agent.id # type: ignore
):
value = AgentCustomField.objects.get(
field=field["field"], agent=agent.id # type: ignore
)
serializer = AgentCustomFieldSerializer(
instance=value, data=custom_field
)
serializer.is_valid(raise_exception=True)
serializer.save()
else:
serializer = AgentCustomFieldSerializer(data=custom_field)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response("ok")
@api_view()
@permission_classes([IsAuthenticated, MeshPerms])
def meshcentral(request, pk):
agent = get_object_or_404(Agent, pk=pk)
core = CoreSettings.objects.first()
token = agent.get_login_token(
key=core.mesh_token, user=f"user//{core.mesh_username}" # type:ignore
)
if token == "err":
return notify_error("Invalid mesh token")
control = f"{core.mesh_site}/?login={token}&gotonode={agent.mesh_node_id}&viewmode=11&hide=31" # type:ignore
terminal = f"{core.mesh_site}/?login={token}&gotonode={agent.mesh_node_id}&viewmode=12&hide=31" # type:ignore
file = f"{core.mesh_site}/?login={token}&gotonode={agent.mesh_node_id}&viewmode=13&hide=31" # type:ignore
AuditLog.audit_mesh_session(
username=request.user.username,
agent=agent,
debug_info={"ip": request._client_ip},
)
ret = {
"hostname": agent.hostname,
"control": control,
"terminal": terminal,
"file": file,
"status": agent.status,
"client": agent.client.name,
"site": agent.site.name,
}
return Response(ret)
@api_view()
def agent_detail(request, pk):
agent = get_object_or_404(Agent, pk=pk)
return Response(AgentSerializer(agent).data)
@api_view()
def get_processes(request, pk):
agent = get_object_or_404(Agent, pk=pk)
r = asyncio.run(agent.nats_cmd(data={"func": "procs"}, timeout=5))
if r == "timeout":
return notify_error("Unable to contact the agent")
return Response(r)
@api_view()
@permission_classes([IsAuthenticated, ManageProcPerms])
def kill_proc(request, pk, pid):
agent = get_object_or_404(Agent, pk=pk)
r = asyncio.run(
agent.nats_cmd({"func": "killproc", "procpid": int(pid)}, timeout=15)
)
if r == "timeout":
return notify_error("Unable to contact the agent")
elif r != "ok":
return notify_error(r)
return Response("ok")
@api_view()
@api_view(["GET"])
@permission_classes([IsAuthenticated, EvtLogPerms])
def get_event_log(request, pk, logtype, days):
agent = get_object_or_404(Agent, pk=pk)
def get_event_log(request, agent_id, logtype, days):
agent = get_object_or_404(Agent, agent_id=agent_id)
timeout = 180 if logtype == "Security" else 30
data = {
"func": "eventlog",
"timeout": timeout,
@@ -231,7 +305,7 @@ def get_event_log(request, pk, logtype, days):
},
}
r = asyncio.run(agent.nats_cmd(data, timeout=timeout + 2))
if r == "timeout":
if r == "timeout" or r == "natsdown":
return notify_error("Unable to contact the agent")
return Response(r)
@@ -239,8 +313,8 @@ def get_event_log(request, pk, logtype, days):
@api_view(["POST"])
@permission_classes([IsAuthenticated, SendCMDPerms])
def send_raw_cmd(request):
agent = get_object_or_404(Agent, pk=request.data["pk"])
def send_raw_cmd(request, agent_id):
agent = get_object_or_404(Agent, agent_id=agent_id)
timeout = int(request.data["timeout"])
data = {
"func": "rawcmd",
@@ -276,81 +350,11 @@ def send_raw_cmd(request):
return Response(r)
class AgentsTableList(APIView):
def patch(self, request):
if "sitePK" in request.data.keys():
queryset = (
Agent.objects.select_related("site", "policy", "alert_template")
.prefetch_related("agentchecks")
.filter(site_id=request.data["sitePK"])
)
elif "clientPK" in request.data.keys():
queryset = (
Agent.objects.select_related("site", "policy", "alert_template")
.prefetch_related("agentchecks")
.filter(site__client_id=request.data["clientPK"])
)
else:
queryset = Agent.objects.select_related(
"site", "policy", "alert_template"
).prefetch_related("agentchecks")
queryset = queryset.only(
"pk",
"hostname",
"agent_id",
"site",
"policy",
"alert_template",
"monitoring_type",
"description",
"needs_reboot",
"overdue_text_alert",
"overdue_email_alert",
"overdue_time",
"offline_time",
"last_seen",
"boot_time",
"logged_in_username",
"last_logged_in_user",
"time_zone",
"maintenance_mode",
"pending_actions_count",
"has_patches_pending",
)
ctx = {"default_tz": get_default_timezone()}
serializer = AgentTableSerializer(queryset, many=True, context=ctx)
return Response(serializer.data)
@api_view()
def list_agents_no_detail(request):
agents = Agent.objects.select_related("site").only("pk", "hostname", "site")
return Response(AgentHostnameSerializer(agents, many=True).data)
@api_view()
def agent_edit_details(request, pk):
agent = get_object_or_404(Agent, pk=pk)
return Response(AgentEditSerializer(agent).data)
@api_view(["POST"])
def overdue_action(request):
agent = get_object_or_404(Agent, pk=request.data["pk"])
serializer = AgentOverdueActionSerializer(
instance=agent, data=request.data, partial=True
)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(agent.hostname)
class Reboot(APIView):
permission_classes = [IsAuthenticated, RebootAgentPerms]
# reboot now
def post(self, request):
agent = get_object_or_404(Agent, pk=request.data["pk"])
def post(self, request, agent_id):
agent = get_object_or_404(Agent, agent_id=agent_id)
r = asyncio.run(agent.nats_cmd({"func": "rebootnow"}, timeout=10))
if r != "ok":
return notify_error("Unable to contact the agent")
@@ -358,8 +362,8 @@ class Reboot(APIView):
return Response("ok")
# reboot later
def patch(self, request):
agent = get_object_or_404(Agent, pk=request.data["pk"])
def patch(self, request, agent_id):
agent = get_object_or_404(Agent, agent_id=agent_id)
try:
obj = dt.datetime.strptime(request.data["datetime"], "%Y-%m-%d %H:%M")
@@ -412,17 +416,24 @@ def install_agent(request):
version = settings.LATEST_AGENT_VER
arch = request.data["arch"]
if not _has_perm_on_site(request.user, site_id):
raise PermissionDenied()
# response type is blob so we have to use
# status codes and render error message on the frontend
if arch == "64" and not os.path.exists(
os.path.join(settings.EXE_DIR, "meshagent.exe")
):
return Response(status=status.HTTP_406_NOT_ACCEPTABLE)
return notify_error(
"Missing 64 bit meshagent.exe. Upload it from Settings > Global Settings > MeshCentral"
)
if arch == "32" and not os.path.exists(
os.path.join(settings.EXE_DIR, "meshagent-x86.exe")
):
return Response(status=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE)
return notify_error(
"Missing 32 bit meshagent.exe. Upload it from Settings > Global Settings > MeshCentral"
)
inno = (
f"winagent-v{version}.exe" if arch == "64" else f"winagent-v{version}-x86.exe"
@@ -539,8 +550,9 @@ def install_agent(request):
@api_view(["POST"])
def recover(request):
agent = get_object_or_404(Agent, pk=request.data["pk"])
@permission_classes([IsAuthenticated, RecoverAgentPerms])
def recover(request, agent_id):
agent = get_object_or_404(Agent, agent_id=agent_id)
mode = request.data["mode"]
# attempt a realtime recovery, otherwise fall back to old recovery method
@@ -577,8 +589,8 @@ def recover(request):
@api_view(["POST"])
@permission_classes([IsAuthenticated, RunScriptPerms])
def run_script(request):
agent = get_object_or_404(Agent, pk=request.data["pk"])
def run_script(request, agent_id):
agent = get_object_or_404(Agent, agent_id=agent_id)
script = get_object_or_404(Script, pk=request.data["script"])
output = request.data["output"]
args = request.data["args"]
@@ -671,17 +683,6 @@ def run_script(request):
return Response(f"{script.name} will now be run on {agent.hostname}")
@api_view()
def recover_mesh(request, pk):
agent = get_object_or_404(Agent, pk=pk)
data = {"func": "recover", "payload": {"mode": "mesh"}}
r = asyncio.run(agent.nats_cmd(data, timeout=90))
if r != "ok":
return notify_error("Unable to contact the agent")
return Response(f"Repaired mesh agent on {agent.hostname}")
@api_view(["POST"])
def get_mesh_exe(request, arch):
filename = "meshagent.exe" if arch == "64" else "meshagent-x86.exe"
@@ -704,34 +705,62 @@ def get_mesh_exe(request, arch):
class GetAddNotes(APIView):
def get(self, request, pk):
agent = get_object_or_404(Agent, pk=pk)
return Response(NotesSerializer(agent).data)
permission_classes = [IsAuthenticated, AgentNotesPerms]
def post(self, request, pk):
agent = get_object_or_404(Agent, pk=pk)
serializer = NoteSerializer(data=request.data, partial=True)
def get(self, request, agent_id=None):
if agent_id:
agent = get_object_or_404(Agent, agent_id=agent_id)
notes = Note.objects.filter(agent=agent)
else:
notes = Note.objects.filter_by_role(request.user)
return Response(AgentNoteSerializer(notes, many=True).data)
def post(self, request):
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
if not _has_perm_on_agent(request.user, agent.agent_id):
raise PermissionDenied()
data = {
"note": request.data["note"],
"agent": agent.pk,
"user": request.user.pk,
}
serializer = AgentNoteSerializer(data=data)
serializer.is_valid(raise_exception=True)
serializer.save(agent=agent, user=request.user)
serializer.save()
return Response("Note added!")
class GetEditDeleteNote(APIView):
permission_classes = [IsAuthenticated, ManageNotesPerms]
permission_classes = [IsAuthenticated, AgentNotesPerms]
def get(self, request, pk):
note = get_object_or_404(Note, pk=pk)
return Response(NoteSerializer(note).data)
def patch(self, request, pk):
if not _has_perm_on_agent(request.user, note.agent.agent_id):
raise PermissionDenied()
return Response(AgentNoteSerializer(note).data)
def put(self, request, pk):
note = get_object_or_404(Note, pk=pk)
serializer = NoteSerializer(instance=note, data=request.data, partial=True)
if not _has_perm_on_agent(request.user, note.agent.agent_id):
raise PermissionDenied()
serializer = AgentNoteSerializer(instance=note, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response("Note edited!")
def delete(self, request, pk):
note = get_object_or_404(Note, pk=pk)
if not _has_perm_on_agent(request.user, note.agent.agent_id):
raise PermissionDenied()
note.delete()
return Response("Note was deleted!")
@@ -743,13 +772,27 @@ def bulk(request):
return notify_error("Must select at least 1 agent")
if request.data["target"] == "client":
q = Agent.objects.filter(site__client_id=request.data["client"])
if not _has_perm_on_client(request.user, request.data["client"]):
raise PermissionDenied()
q = Agent.objects.filter_by_role(request.user).filter(
site__client_id=request.data["client"]
)
elif request.data["target"] == "site":
q = Agent.objects.filter(site_id=request.data["site"])
if not _has_perm_on_site(request.user, request.data["site"]):
raise PermissionDenied()
q = Agent.objects.filter_by_role(request.user).filter(
site_id=request.data["site"]
)
elif request.data["target"] == "agents":
q = Agent.objects.filter(pk__in=request.data["agents"])
q = Agent.objects.filter_by_role(request.user).filter(
agent_id__in=request.data["agents"]
)
elif request.data["target"] == "all":
q = Agent.objects.only("pk", "monitoring_type")
q = Agent.objects.filter_by_role(request.user).only("pk", "monitoring_type")
else:
return notify_error("Something went wrong")
@@ -807,40 +850,60 @@ def bulk(request):
@api_view(["POST"])
@permission_classes([IsAuthenticated, AgentPerms])
def agent_maintenance(request):
if request.data["type"] == "Client":
Agent.objects.filter(site__client_id=request.data["id"]).update(
maintenance_mode=request.data["action"]
if not _has_perm_on_client(request.user, request.data["id"]):
raise PermissionDenied()
count = (
Agent.objects.filter_by_role(request.user)
.filter(site__client_id=request.data["id"])
.update(maintenance_mode=request.data["action"])
)
elif request.data["type"] == "Site":
Agent.objects.filter(site_id=request.data["id"]).update(
maintenance_mode=request.data["action"]
)
if not _has_perm_on_site(request.user, request.data["id"]):
raise PermissionDenied()
elif request.data["type"] == "Agent":
agent = Agent.objects.get(pk=request.data["id"])
agent.maintenance_mode = request.data["action"]
agent.save(update_fields=["maintenance_mode"])
count = (
Agent.objects.filter_by_role(request.user)
.filter(site_id=request.data["id"])
.update(maintenance_mode=request.data["action"])
)
else:
return notify_error("Invalid data")
return Response("ok")
if count:
action = "disabled" if not request.data["action"] else "enabled"
return Response(f"Maintenance mode has been {action} on {count} agents")
else:
return Response(
f"No agents have been put in maintenance mode. You might not have permissions to the resources."
)
class WMI(APIView):
def get(self, request, pk):
agent = get_object_or_404(Agent, pk=pk)
permission_classes = [IsAuthenticated, AgentPerms]
def post(self, request, agent_id):
agent = get_object_or_404(Agent, agent_id=agent_id)
r = asyncio.run(agent.nats_cmd({"func": "sysinfo"}, timeout=20))
if r != "ok":
return notify_error("Unable to contact the agent")
return Response("ok")
return Response("Agent WMI data refreshed successfully")
class AgentHistoryView(APIView):
def get(self, request, pk):
agent = get_object_or_404(Agent, pk=pk)
history = AgentHistory.objects.filter(agent=agent)
permission_classes = [IsAuthenticated, AgentHistoryPerms]
return Response(AgentHistorySerializer(history, many=True).data)
def get(self, request, agent_id=None):
if agent_id:
agent = get_object_or_404(Agent, agent_id=agent_id)
history = AgentHistory.objects.filter(agent=agent)
else:
history = AgentHistory.objects.filter_by_role(request.user)
ctx = {"default_tz": get_default_timezone()}
return Response(AgentHistorySerializer(history, many=True, context=ctx).data)

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

@@ -9,6 +9,7 @@ from django.db.models.fields import BooleanField, PositiveIntegerField
from django.utils import timezone as djangotime
from logs.models import BaseAuditModel, DebugLog
from tacticalrmm.models import PermissionQuerySet
if TYPE_CHECKING:
from agents.models import Agent
@@ -31,6 +32,8 @@ ALERT_TYPE_CHOICES = [
class Alert(models.Model):
objects = PermissionQuerySet.as_manager()
agent = models.ForeignKey(
"agents.Agent",
related_name="agent",
@@ -453,7 +456,8 @@ class Alert(models.Model):
if match:
name = match.group(1)
if hasattr(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
@@ -461,7 +465,7 @@ class Alert(models.Model):
try:
temp_args.append(re.sub("\\{\\{.*\\}\\}", value, arg)) # type: ignore
except Exception as e:
DebugLog.error(log_type="scripting", message=e)
DebugLog.error(log_type="scripting", message=str(e))
continue
else:

View File

@@ -1,11 +1,55 @@
from django.shortcuts import get_object_or_404
from rest_framework import permissions
from tacticalrmm.permissions import _has_perm
from tacticalrmm.permissions import _has_perm, _has_perm_on_agent
class ManageAlertsPerms(permissions.BasePermission):
def _has_perm_on_alert(user, id: int):
from alerts.models import Alert
role = user.role
if user.is_superuser or (role and getattr(role, "is_superuser")):
return True
# make sure non-superusers with empty roles aren't permitted
elif not role:
return False
alert = get_object_or_404(Alert, id=id)
if alert.agent:
agent_id = alert.agent.agent_id
elif alert.assigned_check:
agent_id = alert.assigned_check.agent.agent_id
elif alert.assigned_task:
agent_id = alert.assigned_task.agent.agent_id
else:
return True
return _has_perm_on_agent(user, agent_id)
class AlertPerms(permissions.BasePermission):
def has_permission(self, r, view):
if r.method == "GET" or r.method == "PATCH":
return True
if "pk" in view.kwargs.keys():
return _has_perm(r, "can_list_alerts") and _has_perm_on_alert(
r.user, view.kwargs["pk"]
)
else:
return _has_perm(r, "can_list_alerts")
else:
if "pk" in view.kwargs.keys():
return _has_perm(r, "can_manage_alerts") and _has_perm_on_alert(
r.user, view.kwargs["pk"]
)
else:
return _has_perm(r, "can_manage_alerts")
return _has_perm(r, "can_manage_alerts")
class AlertTemplatePerms(permissions.BasePermission):
def has_permission(self, r, view):
if r.method == "GET":
return _has_perm(r, "can_list_alerttemplates")
else:
return _has_perm(r, "can_manage_alerttemplates")

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,8 +113,8 @@ class AlertTemplateSerializer(ModelSerializer):
class AlertTemplateRelationSerializer(ModelSerializer):
policies = PolicySerializer(read_only=True, many=True)
clients = ClientSerializer(read_only=True, many=True)
sites = SiteSerializer(read_only=True, many=True)
clients = ClientMinimumSerializer(read_only=True, many=True)
sites = SiteMinimumSerializer(read_only=True, many=True)
class Meta:
model = AlertTemplate

View File

@@ -1,5 +1,6 @@
from datetime import datetime, timedelta
from unittest.mock import patch
from itertools import cycle
from core.models import CoreSettings
from django.conf import settings
@@ -8,6 +9,7 @@ from model_bakery import baker, seq
from tacticalrmm.test import TacticalTestCase
from alerts.tasks import cache_agents_alert_template
from agents.tasks import handle_agents_task
from .models import Alert, AlertTemplate
from .serializers import (
@@ -16,6 +18,8 @@ from .serializers import (
AlertTemplateSerializer,
)
base_url = "/alerts"
class TestAlertsViews(TacticalTestCase):
def setUp(self):
@@ -23,7 +27,7 @@ class TestAlertsViews(TacticalTestCase):
self.setup_coresettings()
def test_get_alerts(self):
url = "/alerts/alerts/"
url = "/alerts/"
# create check, task, and agent to test each serializer function
check = baker.make_recipe("checks.diskspace_check")
@@ -116,7 +120,7 @@ class TestAlertsViews(TacticalTestCase):
self.check_not_authenticated("patch", url)
def test_add_alert(self):
url = "/alerts/alerts/"
url = "/alerts/"
agent = baker.make_recipe("agents.agent")
data = {
@@ -133,11 +137,11 @@ class TestAlertsViews(TacticalTestCase):
def test_get_alert(self):
# returns 404 for invalid alert pk
resp = self.client.get("/alerts/alerts/500/", format="json")
resp = self.client.get("/alerts/500/", format="json")
self.assertEqual(resp.status_code, 404)
alert = baker.make("alerts.Alert")
url = f"/alerts/alerts/{alert.pk}/" # type: ignore
url = f"/alerts/{alert.pk}/" # type: ignore
resp = self.client.get(url, format="json")
serializer = AlertSerializer(alert)
@@ -149,16 +153,15 @@ class TestAlertsViews(TacticalTestCase):
def test_update_alert(self):
# returns 404 for invalid alert pk
resp = self.client.put("/alerts/alerts/500/", format="json")
resp = self.client.put("/alerts/500/", format="json")
self.assertEqual(resp.status_code, 404)
alert = baker.make("alerts.Alert", resolved=False, snoozed=False)
url = f"/alerts/alerts/{alert.pk}/" # type: ignore
url = f"/alerts/{alert.pk}/" # type: ignore
# test resolving alert
data = {
"id": alert.pk, # type: ignore
"type": "resolve",
}
resp = self.client.put(url, data, format="json")
@@ -167,26 +170,26 @@ class TestAlertsViews(TacticalTestCase):
self.assertTrue(Alert.objects.get(pk=alert.pk).resolved_on) # type: ignore
# test snoozing alert
data = {"id": alert.pk, "type": "snooze", "snooze_days": "30"} # type: ignore
data = {"type": "snooze", "snooze_days": "30"} # type: ignore
resp = self.client.put(url, data, format="json")
self.assertEqual(resp.status_code, 200)
self.assertTrue(Alert.objects.get(pk=alert.pk).snoozed) # type: ignore
self.assertTrue(Alert.objects.get(pk=alert.pk).snooze_until) # type: ignore
# test snoozing alert without snooze_days
data = {"id": alert.pk, "type": "snooze"} # type: ignore
data = {"type": "snooze"} # type: ignore
resp = self.client.put(url, data, format="json")
self.assertEqual(resp.status_code, 400)
# test unsnoozing alert
data = {"id": alert.pk, "type": "unsnooze"} # type: ignore
data = {"type": "unsnooze"} # type: ignore
resp = self.client.put(url, data, format="json")
self.assertEqual(resp.status_code, 200)
self.assertFalse(Alert.objects.get(pk=alert.pk).snoozed) # type: ignore
self.assertFalse(Alert.objects.get(pk=alert.pk).snooze_until) # type: ignore
# test invalid type
data = {"id": alert.pk, "type": "invalid"} # type: ignore
data = {"type": "invalid"} # type: ignore
resp = self.client.put(url, data, format="json")
self.assertEqual(resp.status_code, 400)
@@ -194,13 +197,13 @@ class TestAlertsViews(TacticalTestCase):
def test_delete_alert(self):
# returns 404 for invalid alert pk
resp = self.client.put("/alerts/alerts/500/", format="json")
resp = self.client.put("/alerts/500/", format="json")
self.assertEqual(resp.status_code, 404)
alert = baker.make("alerts.Alert")
# test delete alert
url = f"/alerts/alerts/{alert.pk}/" # type: ignore
url = f"/alerts/{alert.pk}/" # type: ignore
resp = self.client.delete(url, format="json")
self.assertEqual(resp.status_code, 200)
@@ -242,7 +245,7 @@ class TestAlertsViews(TacticalTestCase):
self.assertTrue(Alert.objects.filter(snoozed=False).exists())
def test_get_alert_templates(self):
url = "/alerts/alerttemplates/"
url = "/alerts/templates/"
alert_templates = baker.make("alerts.AlertTemplate", _quantity=3)
resp = self.client.get(url, format="json")
@@ -254,7 +257,7 @@ class TestAlertsViews(TacticalTestCase):
self.check_not_authenticated("get", url)
def test_add_alert_template(self):
url = "/alerts/alerttemplates/"
url = "/alerts/templates/"
data = {
"name": "Test Template",
@@ -267,11 +270,11 @@ class TestAlertsViews(TacticalTestCase):
def test_get_alert_template(self):
# returns 404 for invalid alert template pk
resp = self.client.get("/alerts/alerttemplates/500/", format="json")
resp = self.client.get("/alerts/templates/500/", format="json")
self.assertEqual(resp.status_code, 404)
alert_template = baker.make("alerts.AlertTemplate")
url = f"/alerts/alerttemplates/{alert_template.pk}/" # type: ignore
url = f"/alerts/templates/{alert_template.pk}/" # type: ignore
resp = self.client.get(url, format="json")
serializer = AlertTemplateSerializer(alert_template)
@@ -283,16 +286,15 @@ class TestAlertsViews(TacticalTestCase):
def test_update_alert_template(self):
# returns 404 for invalid alert pk
resp = self.client.put("/alerts/alerttemplates/500/", format="json")
resp = self.client.put("/alerts/templates/500/", format="json")
self.assertEqual(resp.status_code, 404)
alert_template = baker.make("alerts.AlertTemplate")
url = f"/alerts/alerttemplates/{alert_template.pk}/" # type: ignore
url = f"/alerts/templates/{alert_template.pk}/" # type: ignore
# test data
data = {
"id": alert_template.pk, # type: ignore
"agent_email_on_resolved": True,
"agent_text_on_resolved": True,
"agent_include_desktops": True,
@@ -308,13 +310,13 @@ class TestAlertsViews(TacticalTestCase):
def test_delete_alert_template(self):
# returns 404 for invalid alert pk
resp = self.client.put("/alerts/alerttemplates/500/", format="json")
resp = self.client.put("/alerts/templates/500/", format="json")
self.assertEqual(resp.status_code, 404)
alert_template = baker.make("alerts.AlertTemplate")
# test delete alert
url = f"/alerts/alerttemplates/{alert_template.pk}/" # type: ignore
url = f"/alerts/templates/{alert_template.pk}/" # type: ignore
resp = self.client.delete(url, format="json")
self.assertEqual(resp.status_code, 200)
@@ -332,7 +334,7 @@ class TestAlertsViews(TacticalTestCase):
core.alert_template = alert_template # type: ignore
core.save() # type: ignore
url = f"/alerts/alerttemplates/{alert_template.pk}/related/" # type: ignore
url = f"/alerts/templates/{alert_template.pk}/related/" # type: ignore
resp = self.client.get(url, format="json")
serializer = AlertTemplateRelationSerializer(alert_template)
@@ -675,25 +677,14 @@ class TestAlertTasks(TacticalTestCase):
url = "/api/v3/checkin/"
agent_template_text.version = settings.LATEST_AGENT_VER
agent_template_text.last_seen = djangotime.now()
agent_template_text.save()
agent_template_email.version = settings.LATEST_AGENT_VER
agent_template_email.last_seen = djangotime.now()
agent_template_email.save()
data = {
"agent_id": agent_template_text.agent_id,
"version": settings.LATEST_AGENT_VER,
}
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
data = {
"agent_id": agent_template_email.agent_id,
"version": settings.LATEST_AGENT_VER,
}
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
handle_agents_task()
recovery_sms.assert_called_with(
pk=Alert.objects.get(agent=agent_template_text).pk
@@ -1364,15 +1355,7 @@ class TestAlertTasks(TacticalTestCase):
agent.last_seen = djangotime.now()
agent.save()
url = "/api/v3/checkin/"
data = {
"agent_id": agent.agent_id,
"version": settings.LATEST_AGENT_VER,
}
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
handle_agents_task()
# this is what data should be
data = {
@@ -1434,3 +1417,155 @@ class TestAlertTasks(TacticalTestCase):
prune_resolved_alerts(30)
self.assertEqual(Alert.objects.count(), 31)
class TestAlertPermissions(TacticalTestCase):
def setUp(self):
self.setup_coresettings()
self.client_setup()
def test_get_alerts_permissions(self):
agent = baker.make_recipe("agents.agent")
agent1 = baker.make_recipe("agents.agent")
agent2 = baker.make_recipe("agents.agent")
agents = [agent, agent1, agent2]
checks = baker.make("checks.Check", agent=cycle(agents), _quantity=3)
tasks = baker.make("autotasks.AutomatedTask", agent=cycle(agents), _quantity=3)
baker.make(
"alerts.Alert", alert_type="task", assigned_task=cycle(tasks), _quantity=3
)
baker.make(
"alerts.Alert",
alert_type="check",
assigned_check=cycle(checks),
_quantity=3,
)
baker.make(
"alerts.Alert", alert_type="availability", agent=cycle(agents), _quantity=3
)
baker.make("alerts.Alert", alert_type="custom", _quantity=4)
# test super user access
r = self.check_authorized_superuser("patch", f"{base_url}/")
self.assertEqual(len(r.data), 13) # type: ignore
user = self.create_user_with_roles([])
self.client.force_authenticate(user=user) # type: ignore
self.check_not_authorized("patch", f"{base_url}/")
# add list software role to user
user.role.can_list_alerts = True
user.role.save()
r = self.check_authorized("patch", f"{base_url}/")
self.assertEqual(len(r.data), 13) # type: ignore
# test limiting to client
user.role.can_view_clients.set([agent.client])
r = self.check_authorized("patch", f"{base_url}/")
self.assertEqual(len(r.data), 7) # type: ignore
# test limiting to site
user.role.can_view_clients.clear()
user.role.can_view_sites.set([agent1.site])
r = self.client.patch(f"{base_url}/")
self.assertEqual(len(r.data), 7) # type: ignore
# test limiting to site and client
user.role.can_view_clients.set([agent2.client])
r = self.client.patch(f"{base_url}/")
self.assertEqual(len(r.data), 10) # type: ignore
@patch("alerts.models.Alert.delete", return_value=1)
def test_edit_delete_get_alert_permissions(self, delete):
agent = baker.make_recipe("agents.agent")
agent1 = baker.make_recipe("agents.agent")
agent2 = baker.make_recipe("agents.agent")
agents = [agent, agent1, agent2]
checks = baker.make("checks.Check", agent=cycle(agents), _quantity=3)
tasks = baker.make("autotasks.AutomatedTask", agent=cycle(agents), _quantity=3)
alert_tasks = baker.make(
"alerts.Alert", alert_type="task", assigned_task=cycle(tasks), _quantity=3
)
alert_checks = baker.make(
"alerts.Alert",
alert_type="check",
assigned_check=cycle(checks),
_quantity=3,
)
alert_agents = baker.make(
"alerts.Alert", alert_type="availability", agent=cycle(agents), _quantity=3
)
alert_custom = baker.make("alerts.Alert", alert_type="custom", _quantity=4)
# alert task url
task_url = f"{base_url}/{alert_tasks[0].id}/" # for agent
unauthorized_task_url = f"{base_url}/{alert_tasks[1].id}/" # for agent1
# alert check url
check_url = f"{base_url}/{alert_checks[0].id}/" # for agent
unauthorized_check_url = f"{base_url}/{alert_checks[1].id}/" # for agent1
# alert agent url
agent_url = f"{base_url}/{alert_agents[0].id}/" # for agent
unauthorized_agent_url = f"{base_url}/{alert_agents[1].id}/" # for agent1
# custom alert url
custom_url = f"{base_url}/{alert_custom[0].id}/" # no agent associated
authorized_urls = [task_url, check_url, agent_url, custom_url]
unauthorized_urls = [
unauthorized_agent_url,
unauthorized_check_url,
unauthorized_task_url,
]
for method in ["get", "put", "delete"]:
# test superuser access
for url in authorized_urls:
self.check_authorized_superuser(method, url)
for url in unauthorized_urls:
self.check_authorized_superuser(method, url)
user = self.create_user_with_roles([])
self.client.force_authenticate(user=user) # type: ignore
# test user without role
for url in authorized_urls:
self.check_not_authorized(method, url)
for url in unauthorized_urls:
self.check_not_authorized(method, url)
# add user to role and test
setattr(
user.role,
"can_list_alerts" if method == "get" else "can_manage_alerts",
True,
)
user.role.save()
# test user with role
for url in authorized_urls:
self.check_authorized(method, url)
for url in unauthorized_urls:
self.check_authorized(method, url)
# limit user to client if agent check
user.role.can_view_clients.set([agent.client])
for url in authorized_urls:
self.check_authorized(method, url)
for url in unauthorized_urls:
self.check_not_authorized(method, url)
# limit user to client if agent check
user.role.can_view_sites.set([agent1.site])
for url in authorized_urls:
self.check_authorized(method, url)
for url in unauthorized_urls:
self.check_authorized(method, url)

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

@@ -10,7 +10,7 @@ from rest_framework.views import APIView
from tacticalrmm.utils import notify_error
from .models import Alert, AlertTemplate
from .permissions import ManageAlertsPerms
from .permissions import AlertPerms, AlertTemplatePerms
from .serializers import (
AlertSerializer,
AlertTemplateRelationSerializer,
@@ -20,7 +20,7 @@ from .tasks import cache_agents_alert_template
class GetAddAlerts(APIView):
permission_classes = [IsAuthenticated, ManageAlertsPerms]
permission_classes = [IsAuthenticated, AlertPerms]
def patch(self, request):
@@ -92,7 +92,8 @@ class GetAddAlerts(APIView):
)
alerts = (
Alert.objects.filter(clientFilter)
Alert.objects.filter_by_role(request.user)
.filter(clientFilter)
.filter(severityFilter)
.filter(resolvedFilter)
.filter(snoozedFilter)
@@ -101,7 +102,7 @@ class GetAddAlerts(APIView):
return Response(AlertSerializer(alerts, many=True).data)
else:
alerts = Alert.objects.all()
alerts = Alert.objects.filter_by_role(request.user)
return Response(AlertSerializer(alerts, many=True).data)
def post(self, request):
@@ -113,11 +114,10 @@ class GetAddAlerts(APIView):
class GetUpdateDeleteAlert(APIView):
permission_classes = [IsAuthenticated, ManageAlertsPerms]
permission_classes = [IsAuthenticated, AlertPerms]
def get(self, request, pk):
alert = get_object_or_404(Alert, pk=pk)
return Response(AlertSerializer(alert).data)
def put(self, request, pk):
@@ -169,7 +169,7 @@ class GetUpdateDeleteAlert(APIView):
class BulkAlerts(APIView):
permission_classes = [IsAuthenticated, ManageAlertsPerms]
permission_classes = [IsAuthenticated, AlertPerms]
def post(self, request):
if request.data["bulk_action"] == "resolve":
@@ -193,11 +193,10 @@ class BulkAlerts(APIView):
class GetAddAlertTemplates(APIView):
permission_classes = [IsAuthenticated, ManageAlertsPerms]
permission_classes = [IsAuthenticated, AlertTemplatePerms]
def get(self, request):
alert_templates = AlertTemplate.objects.all()
return Response(AlertTemplateSerializer(alert_templates, many=True).data)
def post(self, request):
@@ -212,7 +211,7 @@ class GetAddAlertTemplates(APIView):
class GetUpdateDeleteAlertTemplate(APIView):
permission_classes = [IsAuthenticated, ManageAlertsPerms]
permission_classes = [IsAuthenticated, AlertTemplatePerms]
def get(self, request, pk):
alert_template = get_object_or_404(AlertTemplate, pk=pk)
@@ -243,6 +242,8 @@ class GetUpdateDeleteAlertTemplate(APIView):
class RelatedAlertTemplate(APIView):
permission_classes = [IsAuthenticated, AlertTemplatePerms]
def get(self, request, pk):
alert_template = get_object_or_404(AlertTemplate, pk=pk)
return Response(AlertTemplateRelationSerializer(alert_template).data)

View File

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

View File

@@ -23,7 +23,7 @@ from checks.serializers import CheckRunnerGetSerializer
from checks.utils import bytes2human
from logs.models import PendingAction, DebugLog
from software.models import InstalledSoftware
from tacticalrmm.utils import SoftwareList, filter_software, notify_error, reload_nats
from tacticalrmm.utils import notify_error, reload_nats
from winupdate.models import WinUpdate, WinUpdatePolicy
@@ -32,55 +32,11 @@ class CheckIn(APIView):
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
def patch(self, request):
def put(self, request):
"""
!!! DEPRECATED AS OF AGENT 1.6.0 !!!
!!! DEPRECATED AS OF AGENT 1.7.0 !!!
Endpoint be removed in a future release
"""
from alerts.models import Alert
updated = False
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
if pyver.parse(request.data["version"]) > pyver.parse(
agent.version
) or pyver.parse(request.data["version"]) == pyver.parse(
settings.LATEST_AGENT_VER
):
updated = True
agent.version = request.data["version"]
agent.last_seen = djangotime.now()
agent.save(update_fields=["version", "last_seen"])
# change agent update pending status to completed if agent has just updated
if (
updated
and agent.pendingactions.filter( # type: ignore
action_type="agentupdate", status="pending"
).exists()
):
agent.pendingactions.filter( # type: ignore
action_type="agentupdate", status="pending"
).update(status="completed")
# handles any alerting actions
if Alert.objects.filter(agent=agent, resolved=False).exists():
Alert.handle_alert_resolve(agent)
# sync scheduled tasks
if agent.autotasks.exclude(sync_status="synced").exists(): # type: ignore
tasks = agent.autotasks.exclude(sync_status="synced") # type: ignore
for task in tasks:
if task.sync_status == "pendingdeletion":
task.delete_task_on_agent()
elif task.sync_status == "initial":
task.modify_task_on_agent()
elif task.sync_status == "notsynced":
task.create_task_on_agent()
return Response("ok")
def put(self, request):
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
serializer = WinAgentSerializer(instance=agent, data=request.data, partial=True)
@@ -109,11 +65,8 @@ class CheckIn(APIView):
return Response("ok")
if request.data["func"] == "software":
raw: SoftwareList = request.data["software"]
if not isinstance(raw, list):
return notify_error("err")
sw = request.data["software"]
sw = filter_software(raw)
if not InstalledSoftware.objects.filter(agent=agent).exists():
InstalledSoftware(agent=agent, software=sw).save()
else:
@@ -168,18 +121,18 @@ 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))
@@ -371,6 +324,13 @@ class TaskRunner(APIView):
serializer.is_valid(raise_exception=True)
new_task = serializer.save(last_run=djangotime.now())
AgentHistory.objects.create(
agent=agent,
type="task_run",
script=task.script,
script_results=request.data,
)
# check if task is a collector and update the custom field
if task.custom_field:
if not task.stderr:
@@ -500,11 +460,7 @@ class Software(APIView):
def post(self, request):
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
raw: SoftwareList = request.data["software"]
if not isinstance(raw, list):
return notify_error("err")
sw = filter_software(raw)
sw = request.data["software"]
if not InstalledSoftware.objects.filter(agent=agent).exists():
InstalledSoftware(agent=agent, software=sw).save()
else:
@@ -570,7 +526,18 @@ class AgentRecovery(APIView):
permission_classes = [IsAuthenticated]
def get(self, request, agentid):
agent = get_object_or_404(Agent, agent_id=agentid)
agent = get_object_or_404(
Agent.objects.prefetch_related("recoveryactions").only(
"pk", "agent_id", "last_seen"
),
agent_id=agentid,
)
# TODO remove these 2 lines after agent v1.7.0 has been out for a while
# this is handled now by nats-api service
agent.last_seen = djangotime.now()
agent.save(update_fields=["last_seen"])
recovery = agent.recoveryactions.filter(last_run=None).last() # type: ignore
ret = {"mode": "pass", "shellcmd": ""}
if recovery is None:

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

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

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,32 +108,6 @@ class PolicyTaskStatusSerializer(ModelSerializer):
fields = "__all__"
class PolicyCheckSerializer(ModelSerializer):
class Meta:
model = Check
fields = (
"id",
"check_type",
"readable_desc",
"assignedtask",
"text_alert",
"email_alert",
"dashboard_alert",
)
depth = 1
class AutoTasksFieldSerializer(ModelSerializer):
assigned_check = PolicyCheckSerializer(read_only=True)
script = ReadOnlyField(source="script.id")
custom_field = ReadOnlyField(source="custom_field.id")
class Meta:
model = AutomatedTask
fields = "__all__"
depth = 1
class PolicyAuditSerializer(ModelSerializer):
class Meta:
model = Policy

View File

@@ -54,6 +54,8 @@ def generate_agent_checks_task(
if create_tasks:
agent.generate_tasks_from_policies()
agent.set_alert_template()
return "ok"

View File

@@ -8,12 +8,9 @@ from tacticalrmm.test import TacticalTestCase
from winupdate.models import WinUpdatePolicy
from .serializers import (
AutoTasksFieldSerializer,
PolicyCheckSerializer,
PolicyCheckStatusSerializer,
PolicyOverviewSerializer,
PolicySerializer,
PolicyTableSerializer,
PolicyTaskStatusSerializer,
)
@@ -26,12 +23,10 @@ class TestPolicyViews(TacticalTestCase):
def test_get_all_policies(self):
url = "/automation/policies/"
policies = baker.make("automation.Policy", _quantity=3)
baker.make("automation.Policy", _quantity=3)
resp = self.client.get(url, format="json")
serializer = PolicyTableSerializer(policies, many=True)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data, serializer.data) # type: ignore
self.assertEqual(len(resp.data), 3)
self.check_not_authenticated("get", url)
@@ -181,38 +176,6 @@ class TestPolicyViews(TacticalTestCase):
self.check_not_authenticated("delete", url)
def test_get_all_policy_tasks(self):
# create policy with tasks
policy = baker.make("automation.Policy")
tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3)
url = f"/automation/{policy.pk}/policyautomatedtasks/" # type: ignore
resp = self.client.get(url, format="json")
serializer = AutoTasksFieldSerializer(tasks, many=True)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data, serializer.data) # type: ignore
self.assertEqual(len(resp.data), 3) # type: ignore
self.check_not_authenticated("get", url)
def test_get_all_policy_checks(self):
# setup data
policy = baker.make("automation.Policy")
checks = self.create_checks(policy=policy)
url = f"/automation/{policy.pk}/policychecks/" # type: ignore
resp = self.client.get(url, format="json")
serializer = PolicyCheckSerializer(checks, many=True)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data, serializer.data) # type: ignore
self.assertEqual(len(resp.data), 7) # type: ignore
self.check_not_authenticated("get", url)
def test_get_policy_check_status(self):
# setup data
site = baker.make("clients.Site")
@@ -225,14 +188,14 @@ class TestPolicyViews(TacticalTestCase):
managed_by_policy=True,
parent_check=policy_diskcheck.pk,
)
url = f"/automation/policycheckstatus/{policy_diskcheck.pk}/check/"
url = f"/automation/checks/{policy_diskcheck.pk}/status/"
resp = self.client.patch(url, format="json")
resp = self.client.get(url, format="json")
serializer = PolicyCheckStatusSerializer([managed_check], many=True)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data, serializer.data) # type: ignore
self.check_not_authenticated("patch", url)
self.check_not_authenticated("get", url)
def test_policy_overview(self):
from clients.models import Client
@@ -292,15 +255,15 @@ class TestPolicyViews(TacticalTestCase):
"autotasks.AutomatedTask", parent_task=task.id, _quantity=5 # type: ignore
)
url = f"/automation/policyautomatedtaskstatus/{task.id}/task/" # type: ignore
url = f"/automation/tasks/{task.id}/status/" # type: ignore
serializer = PolicyTaskStatusSerializer(policy_tasks, many=True)
resp = self.client.patch(url, format="json")
resp = self.client.get(url, format="json")
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data, serializer.data) # type: ignore
self.assertEqual(len(resp.data), 5) # type: ignore
self.check_not_authenticated("patch", url)
self.check_not_authenticated("get", url)
@patch("automation.tasks.run_win_policy_autotasks_task.delay")
def test_run_win_task(self, mock_task):
@@ -313,16 +276,16 @@ class TestPolicyViews(TacticalTestCase):
_quantity=6,
)
url = "/automation/runwintask/1/"
resp = self.client.put(url, format="json")
url = "/automation/tasks/1/run/"
resp = self.client.post(url, format="json")
self.assertEqual(resp.status_code, 200)
mock_task.assert_called() # type: ignore
self.check_not_authenticated("put", url)
self.check_not_authenticated("post", url)
def test_create_new_patch_policy(self):
url = "/automation/winupdatepolicy/"
url = "/automation/patchpolicy/"
# test policy doesn't exist
data = {"policy": 500}
@@ -353,15 +316,14 @@ class TestPolicyViews(TacticalTestCase):
def test_update_patch_policy(self):
# test policy doesn't exist
resp = self.client.put("/automation/winupdatepolicy/500/", format="json")
resp = self.client.put("/automation/patchpolicy/500/", format="json")
self.assertEqual(resp.status_code, 404)
policy = baker.make("automation.Policy")
patch_policy = baker.make("winupdate.WinUpdatePolicy", policy=policy)
url = f"/automation/winupdatepolicy/{patch_policy.pk}/" # type: ignore
url = f"/automation/patchpolicy/{patch_policy.pk}/" # type: ignore
data = {
"id": patch_policy.pk, # type: ignore
"policy": policy.pk, # type: ignore
"critical": "approve",
"important": "approve",
@@ -377,7 +339,7 @@ class TestPolicyViews(TacticalTestCase):
self.check_not_authenticated("put", url)
def test_reset_patch_policy(self):
url = "/automation/winupdatepolicy/reset/"
url = "/automation/patchpolicy/reset/"
inherit_fields = {
"critical": "inherit",
@@ -406,7 +368,7 @@ class TestPolicyViews(TacticalTestCase):
# test reset agents in site
data = {"site": sites[0].id} # type: ignore
resp = self.client.patch(url, data, format="json")
resp = self.client.post(url, data, format="json")
self.assertEqual(resp.status_code, 200)
agents = Agent.objects.filter(site=sites[0]) # type: ignore
@@ -418,7 +380,7 @@ class TestPolicyViews(TacticalTestCase):
# test reset agents in client
data = {"client": clients[1].id} # type: ignore
resp = self.client.patch(url, data, format="json")
resp = self.client.post(url, data, format="json")
self.assertEqual(resp.status_code, 200)
agents = Agent.objects.filter(site__client=clients[1]) # type: ignore
@@ -430,7 +392,7 @@ class TestPolicyViews(TacticalTestCase):
# test reset all agents
data = {}
resp = self.client.patch(url, data, format="json")
resp = self.client.post(url, data, format="json")
self.assertEqual(resp.status_code, 200)
agents = Agent.objects.all()
@@ -438,17 +400,17 @@ class TestPolicyViews(TacticalTestCase):
for k, v in inherit_fields.items():
self.assertEqual(getattr(agent.winupdatepolicy.get(), k), v)
self.check_not_authenticated("patch", url)
self.check_not_authenticated("post", url)
def test_delete_patch_policy(self):
# test patch policy doesn't exist
resp = self.client.delete("/automation/winupdatepolicy/500/", format="json")
resp = self.client.delete("/automation/patchpolicy/500/", format="json")
self.assertEqual(resp.status_code, 404)
winupdate_policy = baker.make_recipe(
"winupdate.winupdate_policy", policy__name="Test Policy"
)
url = f"/automation/winupdatepolicy/{winupdate_policy.pk}/"
url = f"/automation/patchpolicy/{winupdate_policy.pk}/"
resp = self.client.delete(url, format="json")
self.assertEqual(resp.status_code, 200)
@@ -503,7 +465,7 @@ class TestPolicyTasks(TacticalTestCase):
# Add Client to Policy
policy.server_clients.add(server_agents[13].client) # type: ignore
policy.workstation_clients.add(workstation_agents[15].client) # type: ignore
policy.workstation_clients.add(workstation_agents[13].client) # type: ignore
resp = self.client.get(
f"/automation/policies/{policy.pk}/related/", format="json" # type: ignore
@@ -511,22 +473,28 @@ class TestPolicyTasks(TacticalTestCase):
self.assertEqual(resp.status_code, 200)
self.assertEquals(len(resp.data["server_clients"]), 1) # type: ignore
self.assertEquals(len(resp.data["server_sites"]), 5) # type: ignore
self.assertEquals(len(resp.data["server_sites"]), 0) # type: ignore
self.assertEquals(len(resp.data["workstation_clients"]), 1) # type: ignore
self.assertEquals(len(resp.data["workstation_sites"]), 5) # type: ignore
self.assertEquals(len(resp.data["agents"]), 10) # type: ignore
self.assertEquals(len(resp.data["workstation_sites"]), 0) # type: ignore
self.assertEquals(len(resp.data["agents"]), 0) # type: ignore
# Add Site to Policy and the agents and sites length shouldn't change
policy.server_sites.add(server_agents[13].site) # type: ignore
policy.workstation_sites.add(workstation_agents[15].site) # type: ignore
self.assertEquals(len(resp.data["server_sites"]), 5) # type: ignore
self.assertEquals(len(resp.data["workstation_sites"]), 5) # type: ignore
self.assertEquals(len(resp.data["agents"]), 10) # type: ignore
# Add Site to Policy
policy.server_sites.add(server_agents[10].site) # type: ignore
policy.workstation_sites.add(workstation_agents[10].site) # type: ignore
resp = self.client.get(
f"/automation/policies/{policy.pk}/related/", format="json" # type: ignore
)
self.assertEquals(len(resp.data["server_sites"]), 1) # type: ignore
self.assertEquals(len(resp.data["workstation_sites"]), 1) # type: ignore
self.assertEquals(len(resp.data["agents"]), 0) # type: ignore
# Add Agent to Policy and the agents length shouldn't change
policy.agents.add(server_agents[13]) # type: ignore
policy.agents.add(workstation_agents[15]) # type: ignore
self.assertEquals(len(resp.data["agents"]), 10) # type: ignore
# Add Agent to Policy
policy.agents.add(server_agents[2]) # type: ignore
policy.agents.add(workstation_agents[2]) # type: ignore
resp = self.client.get(
f"/automation/policies/{policy.pk}/related/", format="json" # type: ignore
)
self.assertEquals(len(resp.data["agents"]), 2) # type: ignore
def test_generating_agent_policy_checks(self):
from .tasks import generate_agent_checks_task
@@ -918,11 +886,13 @@ class TestPolicyTasks(TacticalTestCase):
@patch("autotasks.models.AutomatedTask.create_task_on_agent")
@patch("autotasks.models.AutomatedTask.delete_task_on_agent")
def test_delete_policy_tasks(self, delete_task_on_agent, create_task):
from .tasks import delete_policy_autotasks_task
from .tasks import delete_policy_autotasks_task, generate_agent_checks_task
policy = baker.make("automation.Policy", active=True)
tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3)
baker.make_recipe("agents.server_agent", policy=policy)
agent = baker.make_recipe("agents.server_agent", policy=policy)
generate_agent_checks_task(agents=[agent.pk], create_tasks=True)
delete_policy_autotasks_task(task=tasks[0].id) # type: ignore
@@ -931,11 +901,13 @@ class TestPolicyTasks(TacticalTestCase):
@patch("autotasks.models.AutomatedTask.create_task_on_agent")
@patch("autotasks.models.AutomatedTask.run_win_task")
def test_run_policy_task(self, run_win_task, create_task):
from .tasks import run_win_policy_autotasks_task
from .tasks import run_win_policy_autotasks_task, generate_agent_checks_task
policy = baker.make("automation.Policy", active=True)
tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3)
baker.make_recipe("agents.server_agent", policy=policy)
agent = baker.make_recipe("agents.server_agent", policy=policy)
generate_agent_checks_task(agents=[agent.pk], create_tasks=True)
run_win_policy_autotasks_task(task=tasks[0].id) # type: ignore
@@ -944,7 +916,10 @@ class TestPolicyTasks(TacticalTestCase):
@patch("autotasks.models.AutomatedTask.create_task_on_agent")
@patch("autotasks.models.AutomatedTask.modify_task_on_agent")
def test_update_policy_tasks(self, modify_task_on_agent, create_task):
from .tasks import update_policy_autotasks_fields_task
from .tasks import (
update_policy_autotasks_fields_task,
generate_agent_checks_task,
)
# setup data
policy = baker.make("automation.Policy", active=True)
@@ -956,6 +931,8 @@ class TestPolicyTasks(TacticalTestCase):
)
agent = baker.make_recipe("agents.server_agent", policy=policy)
generate_agent_checks_task(agents=[agent.pk], create_tasks=True)
tasks[0].enabled = False # type: ignore
tasks[0].save() # type: ignore
@@ -995,6 +972,8 @@ class TestPolicyTasks(TacticalTestCase):
@patch("autotasks.models.AutomatedTask.create_task_on_agent")
def test_policy_exclusions(self, create_task):
from .tasks import generate_agent_checks_task
# setup data
policy = baker.make("automation.Policy", active=True)
baker.make_recipe("checks.memory_check", policy=policy)
@@ -1003,6 +982,8 @@ class TestPolicyTasks(TacticalTestCase):
"agents.agent", policy=policy, monitoring_type="server"
)
generate_agent_checks_task(agents=[agent.pk], create_tasks=True)
# make sure related agents on policy returns correctly
self.assertEqual(policy.related_agents().count(), 1) # type: ignore
self.assertEqual(agent.agentchecks.count(), 1) # type: ignore
@@ -1164,3 +1145,9 @@ class TestPolicyTasks(TacticalTestCase):
# should get policies from agent policy
self.assertTrue(agent.autotasks.all())
self.assertTrue(agent.agentchecks.all())
class TestAutomationPermission(TacticalTestCase):
def setUp(self):
self.client_setup()
self.setup_coresettings()

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,23 +1,22 @@
from agents.models import Agent
from agents.serializers import AgentHostnameSerializer
from autotasks.models import AutomatedTask
from checks.models import Check
from clients.models import Client
from clients.serializers import ClientSerializer, SiteSerializer
from django.shortcuts import get_object_or_404
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.exceptions import PermissionDenied
from tacticalrmm.utils import notify_error
from tacticalrmm.permissions import _has_perm_on_client, _has_perm_on_site
from winupdate.models import WinUpdatePolicy
from winupdate.serializers import WinUpdatePolicySerializer
from .models import Policy
from .permissions import AutomationPolicyPerms
from .serializers import (
AutoTasksFieldSerializer,
PolicyCheckSerializer,
PolicyCheckStatusSerializer,
PolicyRelatedSerializer,
PolicyOverviewSerializer,
PolicySerializer,
PolicyTableSerializer,
@@ -31,7 +30,11 @@ class GetAddPolicies(APIView):
def get(self, request):
policies = Policy.objects.all()
return Response(PolicyTableSerializer(policies, many=True).data)
return Response(
PolicyTableSerializer(
policies, context={"user": request.user}, many=True
).data
)
def post(self, request):
serializer = PolicySerializer(data=request.data, partial=True)
@@ -102,19 +105,14 @@ class PolicySync(APIView):
class PolicyAutoTask(APIView):
permission_classes = [IsAuthenticated, AutomationPolicyPerms]
# tasks associated with policy
def get(self, request, pk):
tasks = AutomatedTask.objects.filter(policy=pk)
return Response(AutoTasksFieldSerializer(tasks, many=True).data)
# get status of all tasks
def patch(self, request, task):
def get(self, request, task):
tasks = AutomatedTask.objects.filter(parent_task=task)
return Response(PolicyTaskStatusSerializer(tasks, many=True).data)
# bulk run win tasks associated with policy
def put(self, request, task):
def post(self, request, task):
from .tasks import run_win_policy_autotasks_task
run_win_policy_autotasks_task.delay(task=task)
@@ -124,11 +122,7 @@ class PolicyAutoTask(APIView):
class PolicyCheck(APIView):
permission_classes = [IsAuthenticated, AutomationPolicyPerms]
def get(self, request, pk):
checks = Check.objects.filter(policy__pk=pk, agent=None)
return Response(PolicyCheckSerializer(checks, many=True).data)
def patch(self, request, check):
def get(self, request, check):
checks = Check.objects.filter(parent_check=check)
return Response(PolicyCheckStatusSerializer(checks, many=True).data)
@@ -143,8 +137,6 @@ class OverviewPolicy(APIView):
class GetRelated(APIView):
def get(self, request, pk):
response = {}
policy = (
Policy.objects.filter(pk=pk)
.prefetch_related(
@@ -156,43 +148,9 @@ class GetRelated(APIView):
.first()
)
response["default_server_policy"] = policy.is_default_server_policy
response["default_workstation_policy"] = policy.is_default_workstation_policy
response["server_clients"] = ClientSerializer(
policy.server_clients.all(), many=True
).data
response["workstation_clients"] = ClientSerializer(
policy.workstation_clients.all(), many=True
).data
filtered_server_sites = list()
filtered_workstation_sites = list()
for client in policy.server_clients.all():
for site in client.sites.all():
if site not in policy.server_sites.all():
filtered_server_sites.append(site)
response["server_sites"] = SiteSerializer(
filtered_server_sites + list(policy.server_sites.all()), many=True
).data
for client in policy.workstation_clients.all():
for site in client.sites.all():
if site not in policy.workstation_sites.all():
filtered_workstation_sites.append(site)
response["workstation_sites"] = SiteSerializer(
filtered_workstation_sites + list(policy.workstation_sites.all()), many=True
).data
response["agents"] = AgentHostnameSerializer(
policy.related_agents().only("pk", "hostname"),
many=True,
).data
return Response(response)
return Response(
PolicyRelatedSerializer(policy, context={"user": request.user}).data
)
class UpdatePatchPolicy(APIView):
@@ -209,8 +167,8 @@ class UpdatePatchPolicy(APIView):
return Response("ok")
# update patch policy
def put(self, request, patchpolicy):
policy = get_object_or_404(WinUpdatePolicy, pk=patchpolicy)
def put(self, request, pk):
policy = get_object_or_404(WinUpdatePolicy, pk=pk)
serializer = WinUpdatePolicySerializer(
instance=policy, data=request.data, partial=True
@@ -220,20 +178,41 @@ class UpdatePatchPolicy(APIView):
return Response("ok")
# bulk reset agent patch policy
def patch(self, request):
# delete patch policy
def delete(self, request, pk):
get_object_or_404(WinUpdatePolicy, pk=pk).delete()
return Response("ok")
class ResetPatchPolicy(APIView):
# bulk reset agent patch policy
def post(self, request):
agents = None
if "client" in request.data:
agents = Agent.objects.prefetch_related("winupdatepolicy").filter(
site__client_id=request.data["client"]
if not _has_perm_on_client(request.user, request.data["client"]):
raise PermissionDenied()
agents = (
Agent.objects.filter_by_role(request.user)
.prefetch_related("winupdatepolicy")
.filter(site__client_id=request.data["client"])
)
elif "site" in request.data:
agents = Agent.objects.prefetch_related("winupdatepolicy").filter(
site_id=request.data["site"]
if not _has_perm_on_site(request.user, request.data["site"]):
raise PermissionDenied()
agents = (
Agent.objects.filter_by_role(request.user)
.prefetch_related("winupdatepolicy")
.filter(site_id=request.data["site"])
)
else:
agents = Agent.objects.prefetch_related("winupdatepolicy").only("pk")
agents = (
Agent.objects.filter_by_role(request.user)
.prefetch_related("winupdatepolicy")
.only("pk")
)
for agent in agents:
winupdatepolicy = agent.winupdatepolicy.get()
@@ -258,10 +237,4 @@ class UpdatePatchPolicy(APIView):
]
)
return Response("ok")
# delete patch policy
def delete(self, request, patchpolicy):
get_object_or_404(WinUpdatePolicy, pk=patchpolicy).delete()
return Response("ok")
return Response("The patch policy on the affected agents has been reset.")

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

@@ -12,6 +12,7 @@ from django.db.models.fields import DateTimeField
from django.db.utils import DatabaseError
from django.utils import timezone as djangotime
from logs.models import BaseAuditModel, DebugLog
from tacticalrmm.models import PermissionQuerySet
from packaging import version as pyver
from tacticalrmm.utils import bitdays_to_string
@@ -47,6 +48,8 @@ TASK_STATUS_CHOICES = [
class AutomatedTask(BaseAuditModel):
objects = PermissionQuerySet.as_manager()
agent = models.ForeignKey(
"agents.Agent",
related_name="autotasks",
@@ -132,6 +135,31 @@ class AutomatedTask(BaseAuditModel):
def __str__(self):
return self.name
def save(self, *args, **kwargs):
from autotasks.tasks import enable_or_disable_win_task
from automation.tasks import update_policy_autotasks_fields_task
# get old agent if exists
old_task = AutomatedTask.objects.get(pk=self.pk) if self.pk else None
super(AutomatedTask, self).save(old_model=old_task, *args, **kwargs)
# check if automated task was enabled/disabled and send celery task
if old_task and old_task.enabled != self.enabled:
if self.agent:
enable_or_disable_win_task.delay(pk=self.pk)
# check if automated task was enabled/disabled and send celery task
elif old_task.policy:
update_policy_autotasks_fields_task.delay(
task=self.pk, update_agent=True
)
# check if policy task was edited and then check if it was a field worth copying to rest of agent tasks
elif old_task and old_task.policy:
for field in self.policy_fields_to_copy:
if getattr(self, field) != getattr(old_task, field):
update_policy_autotasks_fields_task.delay(task=self.pk)
break
@property
def schedule(self):
if self.task_type == "manual":
@@ -197,6 +225,14 @@ class AutomatedTask(BaseAuditModel):
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

View File

@@ -1,14 +1,19 @@
from rest_framework import permissions
from tacticalrmm.permissions import _has_perm
from tacticalrmm.permissions import _has_perm, _has_perm_on_agent
class ManageAutoTaskPerms(permissions.BasePermission):
class AutoTaskPerms(permissions.BasePermission):
def has_permission(self, r, view):
if r.method == "GET":
return True
return _has_perm(r, "can_manage_autotasks")
if "agent_id" in view.kwargs.keys():
return _has_perm(r, "can_list_autotasks") and _has_perm_on_agent(
r.user, view.kwargs["agent_id"]
)
else:
return _has_perm(r, "can_list_autotasks")
else:
return _has_perm(r, "can_manage_autotasks")
class RunAutoTaskPerms(permissions.BasePermission):

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:

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,17 +184,17 @@ class TestAutotaskViews(TacticalTestCase):
policy_task = baker.make("autotasks.AutomatedTask", policy=policy)
# test invalid url
resp = self.client.delete("/tasks/500/automatedtasks/", format="json")
resp = self.client.delete(f"{base_url}/500/", format="json")
self.assertEqual(resp.status_code, 404)
# test delete agent task
url = f"/tasks/{agent_task.id}/automatedtasks/" # type: ignore
url = f"{base_url}/{agent_task.id}/" # type: ignore
resp = self.client.delete(url, format="json")
self.assertEqual(resp.status_code, 200)
delete_win_task_schedule.assert_called_with(pk=agent_task.id) # type: ignore
# test delete policy task
url = f"/tasks/{policy_task.id}/automatedtasks/" # type: ignore
url = f"{base_url}/{policy_task.id}/" # type: ignore
resp = self.client.delete(url, format="json")
self.assertEqual(resp.status_code, 200)
self.assertFalse(AutomatedTask.objects.filter(pk=policy_task.id)) # type: ignore
@@ -183,16 +209,16 @@ class TestAutotaskViews(TacticalTestCase):
task = baker.make("autotasks.AutomatedTask", agent=agent)
# test invalid url
resp = self.client.get("/tasks/runwintask/500/", format="json")
resp = self.client.post(f"{base_url}/500/run/", format="json")
self.assertEqual(resp.status_code, 404)
# test run agent task
url = f"/tasks/runwintask/{task.id}/" # type: ignore
resp = self.client.get(url, format="json")
url = f"{base_url}/{task.id}/run/" # type: ignore
resp = self.client.post(url, format="json")
self.assertEqual(resp.status_code, 200)
run_win_task.assert_called()
self.check_not_authenticated("get", url)
self.check_not_authenticated("post", url)
class TestAutoTaskCeleryTasks(TacticalTestCase):
@@ -410,3 +436,227 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
timeout=5,
)
self.assertEqual(ret.status, "SUCCESS")
class TestTaskPermissions(TacticalTestCase):
def setUp(self):
self.setup_coresettings()
self.client_setup()
def test_get_tasks_permissions(self):
agent = baker.make_recipe("agents.agent")
policy = baker.make("automation.Policy")
unauthorized_agent = baker.make_recipe("agents.agent")
task = baker.make("autotasks.AutomatedTask", agent=agent, _quantity=5)
unauthorized_task = baker.make(
"autotasks.AutomatedTask", agent=unauthorized_agent, _quantity=7
)
policy_tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=2)
# test super user access
self.check_authorized_superuser("get", f"{base_url}/")
self.check_authorized_superuser("get", f"/agents/{agent.agent_id}/tasks/")
self.check_authorized_superuser(
"get", f"/agents/{unauthorized_agent.agent_id}/tasks/"
)
self.check_authorized_superuser(
"get", f"/automation/policies/{policy.id}/tasks/"
)
user = self.create_user_with_roles([])
self.client.force_authenticate(user=user)
self.check_not_authorized("get", f"{base_url}/")
self.check_not_authorized("get", f"/agents/{agent.agent_id}/tasks/")
self.check_not_authorized(
"get", f"/agents/{unauthorized_agent.agent_id}/tasks/"
)
self.check_not_authorized("get", f"/automation/policies/{policy.id}/tasks/")
# add list software role to user
user.role.can_list_autotasks = True
user.role.save()
r = self.check_authorized("get", f"{base_url}/")
self.assertEqual(len(r.data), 14)
r = self.check_authorized("get", f"/agents/{agent.agent_id}/tasks/")
self.assertEqual(len(r.data), 5)
r = self.check_authorized(
"get", f"/agents/{unauthorized_agent.agent_id}/tasks/"
)
self.assertEqual(len(r.data), 7)
r = self.check_authorized("get", f"/automation/policies/{policy.id}/tasks/")
self.assertEqual(len(r.data), 2)
# test limiting to client
user.role.can_view_clients.set([agent.client])
self.check_not_authorized(
"get", f"/agents/{unauthorized_agent.agent_id}/tasks/"
)
self.check_authorized("get", f"/agents/{agent.agent_id}/tasks/")
self.check_authorized("get", f"/automation/policies/{policy.id}/tasks/")
# make sure queryset is limited too
r = self.client.get(f"{base_url}/")
self.assertEqual(len(r.data), 7)
def test_add_task_permissions(self):
agent = baker.make_recipe("agents.agent")
unauthorized_agent = baker.make_recipe("agents.agent")
policy = baker.make("automation.Policy")
script = baker.make("scripts.Script")
policy_data = {
"policy": policy.id, # type: ignore
"name": "Test Task Manual",
"run_time_days": [],
"timeout": 120,
"enabled": True,
"script": script.id,
"script_args": [],
"task_type": "manual",
"assigned_check": None,
}
agent_data = {
"agent": agent.agent_id,
"name": "Test Task Manual",
"run_time_days": [],
"timeout": 120,
"enabled": True,
"script": script.id,
"script_args": [],
"task_type": "manual",
"assigned_check": None,
}
unauthorized_agent_data = {
"agent": unauthorized_agent.agent_id,
"name": "Test Task Manual",
"run_time_days": [],
"timeout": 120,
"enabled": True,
"script": script.id,
"script_args": [],
"task_type": "manual",
"assigned_check": None,
}
url = f"{base_url}/"
for data in [policy_data, agent_data]:
# test superuser access
self.check_authorized_superuser("post", url, data)
user = self.create_user_with_roles([])
self.client.force_authenticate(user=user)
# test user without role
self.check_not_authorized("post", url, data)
# add user to role and test
setattr(user.role, "can_manage_autotasks", True)
user.role.save()
self.check_authorized("post", url, data)
# limit user to client
user.role.can_view_clients.set([agent.client])
if "agent" in data.keys():
self.check_authorized("post", url, data)
self.check_not_authorized("post", url, unauthorized_agent_data)
else:
self.check_authorized("post", url, data)
# mock the task delete method so it actually isn't deleted
@patch("autotasks.models.AutomatedTask.delete")
def test_task_get_edit_delete_permissions(self, delete_task):
agent = baker.make_recipe("agents.agent")
unauthorized_agent = baker.make_recipe("agents.agent")
policy = baker.make("automation.Policy")
task = baker.make("autotasks.AutomatedTask", agent=agent)
unauthorized_task = baker.make(
"autotasks.AutomatedTask", agent=unauthorized_agent
)
policy_task = baker.make("autotasks.AutomatedTask", policy=policy)
for method in ["get", "put", "delete"]:
url = f"{base_url}/{task.id}/"
unauthorized_url = f"{base_url}/{unauthorized_task.id}/"
policy_url = f"{base_url}/{policy_task.id}/"
# test superuser access
self.check_authorized_superuser(method, url)
self.check_authorized_superuser(method, unauthorized_url)
self.check_authorized_superuser(method, policy_url)
user = self.create_user_with_roles([])
self.client.force_authenticate(user=user)
# test user without role
self.check_not_authorized(method, url)
self.check_not_authorized(method, unauthorized_url)
self.check_not_authorized(method, policy_url)
# add user to role and test
setattr(
user.role,
"can_list_autotasks" if method == "get" else "can_manage_autotasks",
True,
)
user.role.save()
self.check_authorized(method, url)
self.check_authorized(method, unauthorized_url)
self.check_authorized(method, policy_url)
# limit user to client if agent task
user.role.can_view_clients.set([agent.client])
self.check_authorized(method, url)
self.check_not_authorized(method, unauthorized_url)
self.check_authorized(method, policy_url)
def test_task_action_permissions(self):
agent = baker.make_recipe("agents.agent")
unauthorized_agent = baker.make_recipe("agents.agent")
task = baker.make("autotasks.AutomatedTask", agent=agent)
unauthorized_task = baker.make(
"autotasks.AutomatedTask", agent=unauthorized_agent
)
url = f"{base_url}/{task.id}/run/"
unauthorized_url = f"{base_url}/{unauthorized_task.id}/run/"
# test superuser access
self.check_authorized_superuser("post", url)
self.check_authorized_superuser("post", unauthorized_url)
user = self.create_user_with_roles([])
self.client.force_authenticate(user=user)
# test user without role
self.check_not_authorized("post", url)
self.check_not_authorized("post", unauthorized_url)
# add user to role and test
user.role.can_run_autotasks = True
user.role.save()
self.check_authorized("post", url)
self.check_authorized("post", unauthorized_url)
# limit user to client if agent task
user.role.can_view_sites.set([agent.site])
self.check_authorized("post", url)
self.check_not_authorized("post", unauthorized_url)
def test_policy_fields_to_copy_exists(self):
fields = [i.name for i in AutomatedTask._meta.get_fields()]
task = baker.make("autotasks.AutomatedTask")
for i in task.policy_fields_to_copy: # type: ignore
self.assertIn(i, fields)

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,55 +1,59 @@
from django.shortcuts import get_object_or_404
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.exceptions import PermissionDenied
from agents.models import Agent
from checks.models import Check
from scripts.models import Script
from tacticalrmm.utils import get_bit_days, get_default_timezone, notify_error
from automation.models import Policy
from tacticalrmm.utils import get_bit_days
from tacticalrmm.permissions import _has_perm_on_agent
from .models import AutomatedTask
from .permissions import ManageAutoTaskPerms, RunAutoTaskPerms
from .serializers import AutoTaskSerializer, TaskSerializer
from .permissions import AutoTaskPerms, RunAutoTaskPerms
from .serializers import TaskSerializer
class AddAutoTask(APIView):
permission_classes = [IsAuthenticated, ManageAutoTaskPerms]
class GetAddAutoTasks(APIView):
permission_classes = [IsAuthenticated, AutoTaskPerms]
def get(self, request, agent_id=None, policy=None):
if agent_id:
agent = get_object_or_404(Agent, agent_id=agent_id)
tasks = AutomatedTask.objects.filter(agent=agent)
elif policy:
policy = get_object_or_404(Policy, id=policy)
tasks = AutomatedTask.objects.filter(policy=policy)
else:
tasks = AutomatedTask.objects.filter_by_role(request.user)
return Response(TaskSerializer(tasks, many=True).data)
def post(self, request):
from automation.models import Policy
from automation.tasks import generate_agent_autotasks_task
from autotasks.tasks import create_win_task_schedule
data = request.data
script = get_object_or_404(Script, pk=data["autotask"]["script"])
data = request.data.copy()
# Determine if adding check to Policy or Agent
if "policy" in data:
policy = get_object_or_404(Policy, id=data["policy"])
# Object used for filter and save
parent = {"policy": policy}
else:
agent = get_object_or_404(Agent, pk=data["agent"])
parent = {"agent": agent}
# Determine if adding to an agent and replace agent_id with pk
if "agent" in data.keys():
agent = get_object_or_404(Agent, agent_id=data["agent"])
check = None
if data["autotask"]["assigned_check"]:
check = get_object_or_404(Check, pk=data["autotask"]["assigned_check"])
if not _has_perm_on_agent(request.user, agent.agent_id):
raise PermissionDenied()
data["agent"] = agent.pk
bit_weekdays = None
if data["autotask"]["run_time_days"]:
bit_weekdays = get_bit_days(data["autotask"]["run_time_days"])
if "run_time_days" in data.keys():
if data["run_time_days"]:
bit_weekdays = get_bit_days(data["run_time_days"])
data.pop("run_time_days")
del data["autotask"]["run_time_days"]
serializer = TaskSerializer(data=data["autotask"], partial=True, context=parent)
serializer = TaskSerializer(data=data)
serializer.is_valid(raise_exception=True)
task = serializer.save(
**parent,
script=script,
win_task_name=AutomatedTask.generate_task_name(),
assigned_check=check,
run_time_bit_weekdays=bit_weekdays,
)
@@ -59,58 +63,35 @@ class AddAutoTask(APIView):
elif task.policy:
generate_agent_autotasks_task.delay(policy=task.policy.pk)
return Response("Task will be created shortly!")
return Response(
"The task has been created. It will show up on the agent on next checkin"
)
class AutoTask(APIView):
permission_classes = [IsAuthenticated, ManageAutoTaskPerms]
class GetEditDeleteAutoTask(APIView):
permission_classes = [IsAuthenticated, AutoTaskPerms]
def get(self, request, pk):
agent = get_object_or_404(Agent, pk=pk)
ctx = {
"default_tz": get_default_timezone(),
"agent_tz": agent.time_zone,
}
return Response(AutoTaskSerializer(agent, context=ctx).data)
task = get_object_or_404(AutomatedTask, pk=pk)
if task.agent and not _has_perm_on_agent(request.user, task.agent.agent_id):
raise PermissionDenied()
return Response(TaskSerializer(task).data)
def put(self, request, pk):
from automation.tasks import update_policy_autotasks_fields_task
task = get_object_or_404(AutomatedTask, pk=pk)
if task.agent and not _has_perm_on_agent(request.user, task.agent.agent_id):
raise PermissionDenied()
serializer = TaskSerializer(instance=task, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
serializer.save()
if task.policy:
update_policy_autotasks_fields_task.delay(task=task.pk)
return Response("ok")
def patch(self, request, pk):
from automation.tasks import update_policy_autotasks_fields_task
from autotasks.tasks import enable_or_disable_win_task
task = get_object_or_404(AutomatedTask, pk=pk)
if "enableordisable" in request.data:
action = request.data["enableordisable"]
task.enabled = action
task.save(update_fields=["enabled"])
action = "enabled" if action else "disabled"
if task.policy:
update_policy_autotasks_fields_task.delay(
task=task.pk, update_agent=True
)
elif task.agent:
enable_or_disable_win_task.delay(pk=task.pk)
return Response(f"Task will be {action} shortly")
else:
return notify_error("The request was invalid")
return Response("The task was updated")
def delete(self, request, pk):
from automation.tasks import delete_policy_autotasks_task
@@ -118,6 +99,9 @@ class AutoTask(APIView):
task = get_object_or_404(AutomatedTask, pk=pk)
if task.agent and not _has_perm_on_agent(request.user, task.agent.agent_id):
raise PermissionDenied()
if task.agent:
delete_win_task_schedule.delay(pk=task.pk)
elif task.policy:
@@ -127,11 +111,16 @@ class AutoTask(APIView):
return Response(f"{task.name} will be deleted shortly")
@api_view()
@permission_classes([IsAuthenticated, RunAutoTaskPerms])
def run_task(request, pk):
from autotasks.tasks import run_win_task
class RunAutoTask(APIView):
permission_classes = [IsAuthenticated, RunAutoTaskPerms]
task = get_object_or_404(AutomatedTask, pk=pk)
run_win_task.delay(pk=pk)
return Response(f"{task.name} will now be run on {task.agent.hostname}")
def post(self, request, pk):
from autotasks.tasks import run_win_task
task = get_object_or_404(AutomatedTask, pk=pk)
if task.agent and not _has_perm_on_agent(request.user, task.agent.agent_id):
raise PermissionDenied()
run_win_task.delay(pk=pk)
return Response(f"{task.name} will now be run on {task.agent.hostname}")

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

@@ -12,6 +12,7 @@ from django.contrib.postgres.fields import ArrayField
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from logs.models import BaseAuditModel
from tacticalrmm.models import PermissionQuerySet
CHECK_TYPE_CHOICES = [
("diskspace", "Disk Space Check"),
@@ -50,6 +51,7 @@ EVT_LOG_FAIL_WHEN_CHOICES = [
class Check(BaseAuditModel):
objects = PermissionQuerySet.as_manager()
# common fields
@@ -230,16 +232,16 @@ class Check(BaseAuditModel):
return self.last_run
@property
def non_editable_fields(self) -> list[str]:
@staticmethod
def non_editable_fields() -> list[str]:
return [
"check_type",
"status",
"more_info",
"last_run",
"fail_count",
"outage_history",
"extra_details",
"status",
"stdout",
"stderr",
"retcode",
@@ -457,7 +459,7 @@ class Check(BaseAuditModel):
elif self.status == "passing":
self.fail_count = 0
self.save(update_fields=["status", "fail_count", "alert_severity"])
self.save()
if Alert.objects.filter(assigned_check=self, resolved=False).exists():
Alert.handle_alert_resolve(self)
@@ -475,21 +477,6 @@ class Check(BaseAuditModel):
return CheckAuditSerializer(check).data
# for policy diskchecks
@staticmethod
def all_disks():
return [f"{i}:" for i in string.ascii_uppercase]
# for policy service checks
@staticmethod
def load_default_services():
with open(
os.path.join(settings.BASE_DIR, "services/default_services.json")
) as f:
default_services = json.load(f)
return default_services
def create_policy_check(self, agent=None, policy=None):
if (not agent and not policy) or (agent and policy):
@@ -684,10 +671,12 @@ class Check(BaseAuditModel):
class CheckHistory(models.Model):
objects = PermissionQuerySet.as_manager()
check_id = models.PositiveIntegerField(default=0)
x = models.DateTimeField(auto_now_add=True)
y = models.PositiveIntegerField(null=True, blank=True, default=None)
results = models.JSONField(null=True, blank=True)
def __str__(self):
return self.x
return str(self.x)

View File

@@ -1,16 +1,23 @@
from rest_framework import permissions
from tacticalrmm.permissions import _has_perm
from tacticalrmm.permissions import _has_perm, _has_perm_on_agent
class ManageChecksPerms(permissions.BasePermission):
class ChecksPerms(permissions.BasePermission):
def has_permission(self, r, view):
if r.method == "GET":
return True
return _has_perm(r, "can_manage_checks")
if r.method == "GET" or r.method == "PATCH":
if "agent_id" in view.kwargs.keys():
return _has_perm(r, "can_list_checks") and _has_perm_on_agent(
r.user, view.kwargs["agent_id"]
)
else:
return _has_perm(r, "can_list_checks")
else:
return _has_perm(r, "can_manage_checks")
class RunChecksPerms(permissions.BasePermission):
def has_permission(self, r, view):
return _has_perm(r, "can_run_checks")
return _has_perm(r, "can_run_checks") and _has_perm_on_agent(
r.user, view.kwargs["agent_id"]
)

View File

@@ -3,7 +3,7 @@ import validators as _v
from rest_framework import serializers
from autotasks.models import AutomatedTask
from scripts.serializers import ScriptCheckSerializer, ScriptSerializer
from scripts.serializers import ScriptCheckSerializer
from .models import Check, CheckHistory
from scripts.models import Script
@@ -18,7 +18,6 @@ class AssignedTaskField(serializers.ModelSerializer):
class CheckSerializer(serializers.ModelSerializer):
readable_desc = serializers.ReadOnlyField()
script = ScriptSerializer(read_only=True)
assigned_task = serializers.SerializerMethodField()
last_run = serializers.ReadOnlyField(source="last_run_as_timezone")
history_info = serializers.ReadOnlyField()
@@ -57,6 +56,11 @@ class CheckSerializer(serializers.ModelSerializer):
def validate(self, val):
try:
check_type = val["check_type"]
filter = (
{"agent": val["agent"]}
if "agent" in val.keys()
else {"policy": val["policy"]}
)
except KeyError:
return val
@@ -65,7 +69,7 @@ class CheckSerializer(serializers.ModelSerializer):
if check_type == "diskspace":
if not self.instance: # only on create
checks = (
Check.objects.filter(**self.context)
Check.objects.filter(**filter)
.filter(check_type="diskspace")
.exclude(managed_by_policy=True)
)
@@ -102,7 +106,7 @@ class CheckSerializer(serializers.ModelSerializer):
if check_type == "cpuload" and not self.instance:
if (
Check.objects.filter(**self.context, check_type="cpuload")
Check.objects.filter(**filter, check_type="cpuload")
.exclude(managed_by_policy=True)
.exists()
):
@@ -126,7 +130,7 @@ class CheckSerializer(serializers.ModelSerializer):
if check_type == "memory" and not self.instance:
if (
Check.objects.filter(**self.context, check_type="memory")
Check.objects.filter(**filter, check_type="memory")
.exclude(managed_by_policy=True)
.exists()
):

View File

@@ -8,21 +8,46 @@ from tacticalrmm.test import TacticalTestCase
from .serializers import CheckSerializer
base_url = "/checks"
class TestCheckViews(TacticalTestCase):
def setUp(self):
self.authenticate()
self.setup_coresettings()
def test_get_checks(self):
url = f"{base_url}/"
agent = baker.make_recipe("agents.agent")
baker.make("checks.Check", agent=agent, _quantity=4)
baker.make("checks.Check", _quantity=4)
resp = self.client.get(url, format="json")
self.assertEqual(resp.status_code, 200)
self.assertEqual(len(resp.data), 8) # type: ignore
# test checks agent url
url = f"/agents/{agent.agent_id}/checks/"
resp = self.client.get(url, format="json")
self.assertEqual(resp.status_code, 200)
self.assertEqual(len(resp.data), 4) # type: ignore
# test agent doesn't exist
url = f"/agents/jh3498uf8fkh4ro8hfd8df98/checks/"
resp = self.client.get(url, format="json")
self.assertEqual(resp.status_code, 404)
self.check_not_authenticated("get", url)
def test_delete_agent_check(self):
# setup data
agent = baker.make_recipe("agents.agent")
check = baker.make_recipe("checks.diskspace_check", agent=agent)
resp = self.client.delete("/checks/500/check/", format="json")
resp = self.client.delete(f"{base_url}/500/", format="json")
self.assertEqual(resp.status_code, 404)
url = f"/checks/{check.pk}/check/"
url = f"{base_url}/{check.pk}/"
resp = self.client.delete(url, format="json")
self.assertEqual(resp.status_code, 200)
@@ -30,11 +55,11 @@ class TestCheckViews(TacticalTestCase):
self.check_not_authenticated("delete", url)
def test_get_disk_check(self):
def test_get_check(self):
# setup data
disk_check = baker.make_recipe("checks.diskspace_check")
url = f"/checks/{disk_check.pk}/check/"
url = f"{base_url}/{disk_check.pk}/"
resp = self.client.get(url, format="json")
serializer = CheckSerializer(disk_check)
@@ -46,296 +71,161 @@ class TestCheckViews(TacticalTestCase):
def test_add_disk_check(self):
# setup data
agent = baker.make_recipe("agents.agent")
policy = baker.make("automation.Policy")
url = "/checks/checks/"
url = f"{base_url}/"
valid_payload = {
"pk": agent.pk,
"check": {
"check_type": "diskspace",
"disk": "C:",
"error_threshold": 55,
"warning_threshold": 0,
"fails_b4_alert": 3,
},
agent_payload = {
"agent": agent.agent_id,
"check_type": "diskspace",
"disk": "C:",
"error_threshold": 55,
"warning_threshold": 0,
"fails_b4_alert": 3,
}
resp = self.client.post(url, valid_payload, format="json")
self.assertEqual(resp.status_code, 200)
# this should fail because we already have a check for drive C: in setup
invalid_payload = {
"pk": agent.pk,
"check": {
"check_type": "diskspace",
"disk": "C:",
"error_threshold": 55,
"warning_threshold": 0,
"fails_b4_alert": 3,
},
policy_payload = {
"policy": policy.id,
"check_type": "diskspace",
"disk": "C:",
"error_threshold": 55,
"warning_threshold": 0,
"fails_b4_alert": 3,
}
resp = self.client.post(url, invalid_payload, format="json")
self.assertEqual(resp.status_code, 400)
for payload in [agent_payload, policy_payload]:
# this should fail because both error and warning threshold are 0
invalid_payload = {
"pk": agent.pk,
"check": {
"check_type": "diskspace",
"disk": "C:",
"error_threshold": 0,
"warning_threshold": 0,
"fails_b4_alert": 3,
},
}
# add valid check
resp = self.client.post(url, payload, format="json")
self.assertEqual(resp.status_code, 200)
resp = self.client.post(url, invalid_payload, format="json")
self.assertEqual(resp.status_code, 400)
# this should fail since we just added it
resp = self.client.post(url, payload, format="json")
self.assertEqual(resp.status_code, 400)
# this should fail because both error is greater than warning threshold
invalid_payload = {
"pk": agent.pk,
"check": {
"check_type": "diskspace",
"disk": "C:",
"error_threshold": 50,
"warning_threshold": 30,
"fails_b4_alert": 3,
},
}
# this should fail because both error and warning threshold are 0
payload["error_threshold"] = 0
payload["warning_threshold"] = 0
resp = self.client.post(url, invalid_payload, format="json")
self.assertEqual(resp.status_code, 400)
resp = self.client.post(url, payload, format="json")
self.assertEqual(resp.status_code, 400)
# this should fail because error threshold is greater than warning threshold
payload["error_threshold"] = 50
payload["warning_threshold"] = 30
resp = self.client.post(url, payload, format="json")
self.assertEqual(resp.status_code, 400)
self.check_not_authenticated("post", url)
def test_add_cpuload_check(self):
url = "/checks/checks/"
url = f"{base_url}/"
agent = baker.make_recipe("agents.agent")
payload = {
"pk": agent.pk,
"check": {
"check_type": "cpuload",
"error_threshold": 66,
"warning_threshold": 0,
"fails_b4_alert": 9,
},
policy = baker.make("automation.Policy")
agent_payload = {
"agent": agent.agent_id,
"check_type": "cpuload",
"error_threshold": 66,
"warning_threshold": 0,
"fails_b4_alert": 9,
}
resp = self.client.post(url, payload, format="json")
self.assertEqual(resp.status_code, 200)
payload["error_threshold"] = 87
resp = self.client.post(url, payload, format="json")
self.assertEqual(resp.status_code, 400)
self.assertEqual(
resp.json()["non_field_errors"][0],
"A cpuload check for this agent already exists",
)
# should fail because both error and warning thresholds are 0
invalid_payload = {
"pk": agent.pk,
"check": {
"check_type": "cpuload",
"error_threshold": 0,
"warning_threshold": 0,
"fails_b4_alert": 9,
},
policy_payload = {
"policy": policy.id,
"check_type": "cpuload",
"error_threshold": 66,
"warning_threshold": 0,
"fails_b4_alert": 9,
}
resp = self.client.post(url, invalid_payload, format="json")
self.assertEqual(resp.status_code, 400)
for payload in [agent_payload, policy_payload]:
# should fail because error is less than warning
invalid_payload = {
"pk": agent.pk,
"check": {
"check_type": "cpuload",
"error_threshold": 10,
"warning_threshold": 50,
"fails_b4_alert": 9,
},
}
# add cpu check
resp = self.client.post(url, payload, format="json")
self.assertEqual(resp.status_code, 200)
resp = self.client.post(url, invalid_payload, format="json")
self.assertEqual(resp.status_code, 400)
# should fail since cpu check already exists
resp = self.client.post(url, payload, format="json")
self.assertEqual(resp.status_code, 400)
# this should fail because both error and warning threshold are 0
payload["error_threshold"] = 0
payload["warning_threshold"] = 0
resp = self.client.post(url, payload, format="json")
self.assertEqual(resp.status_code, 400)
# this should fail because error threshold is less than warning threshold
payload["error_threshold"] = 20
payload["warning_threshold"] = 30
resp = self.client.post(url, payload, format="json")
self.assertEqual(resp.status_code, 400)
self.check_not_authenticated("post", url)
def test_add_memory_check(self):
url = "/checks/checks/"
url = f"{base_url}/"
agent = baker.make_recipe("agents.agent")
payload = {
"pk": agent.pk,
"check": {
"check_type": "memory",
"error_threshold": 78,
"warning_threshold": 0,
"fails_b4_alert": 1,
},
}
resp = self.client.post(url, payload, format="json")
self.assertEqual(resp.status_code, 200)
payload["error_threshold"] = 55
resp = self.client.post(url, payload, format="json")
self.assertEqual(resp.status_code, 400)
self.assertEqual(
resp.json()["non_field_errors"][0],
"A memory check for this agent already exists",
)
# should fail because both error and warning thresholds are 0
invalid_payload = {
"pk": agent.pk,
"check": {
"check_type": "memory",
"error_threshold": 0,
"warning_threshold": 0,
"fails_b4_alert": 9,
},
}
resp = self.client.post(url, invalid_payload, format="json")
self.assertEqual(resp.status_code, 400)
# should fail because error is less than warning
invalid_payload = {
"pk": agent.pk,
"check": {
"check_type": "memory",
"error_threshold": 10,
"warning_threshold": 50,
"fails_b4_alert": 9,
},
}
resp = self.client.post(url, invalid_payload, format="json")
self.assertEqual(resp.status_code, 400)
def test_get_policy_disk_check(self):
# setup data
policy = baker.make("automation.Policy")
disk_check = baker.make_recipe("checks.diskspace_check", policy=policy)
url = f"/checks/{disk_check.pk}/check/"
agent_payload = {
"agent": agent.agent_id,
"check_type": "memory",
"error_threshold": 78,
"warning_threshold": 0,
"fails_b4_alert": 1,
}
resp = self.client.get(url, format="json")
serializer = CheckSerializer(disk_check)
policy_payload = {
"policy": policy.id,
"check_type": "memory",
"error_threshold": 78,
"warning_threshold": 0,
"fails_b4_alert": 1,
}
for payload in [agent_payload, policy_payload]:
# add memory check
resp = self.client.post(url, payload, format="json")
self.assertEqual(resp.status_code, 200)
# should fail since cpu check already exists
resp = self.client.post(url, payload, format="json")
self.assertEqual(resp.status_code, 400)
# this should fail because both error and warning threshold are 0
payload["error_threshold"] = 0
payload["warning_threshold"] = 0
resp = self.client.post(url, payload, format="json")
self.assertEqual(resp.status_code, 400)
# this should fail because error threshold is less than warning threshold
payload["error_threshold"] = 20
payload["warning_threshold"] = 30
resp = self.client.post(url, payload, format="json")
self.assertEqual(resp.status_code, 400)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data, serializer.data) # type: ignore
self.check_not_authenticated("post", url)
def test_add_policy_disk_check(self):
# setup data
policy = baker.make("automation.Policy")
url = "/checks/checks/"
valid_payload = {
"policy": policy.pk, # type: ignore
"check": {
"check_type": "diskspace",
"disk": "M:",
"error_threshold": 86,
"warning_threshold": 0,
"fails_b4_alert": 2,
},
}
# should fail because both error and warning thresholds are 0
invalid_payload = {
"policy": policy.pk, # type: ignore
"check": {
"check_type": "diskspace",
"error_threshold": 0,
"warning_threshold": 0,
"fails_b4_alert": 9,
},
}
resp = self.client.post(url, invalid_payload, format="json")
self.assertEqual(resp.status_code, 400)
# should fail because warning is less than error
invalid_payload = {
"policy": policy.pk, # type: ignore
"check": {
"check_type": "diskspace",
"error_threshold": 80,
"warning_threshold": 50,
"fails_b4_alert": 9,
},
}
resp = self.client.post(url, valid_payload, format="json")
self.assertEqual(resp.status_code, 200)
# this should fail because we already have a check for drive M: in setup
invalid_payload = {
"policy": policy.pk, # type: ignore
"check": {
"check_type": "diskspace",
"disk": "M:",
"error_threshold": 34,
"warning_threshold": 0,
"fails_b4_alert": 9,
},
}
resp = self.client.post(url, invalid_payload, format="json")
self.assertEqual(resp.status_code, 400)
def test_get_disks_for_policies(self):
url = "/checks/getalldisks/"
r = self.client.get(url)
self.assertIsInstance(r.data, list) # type: ignore
self.assertEqual(26, len(r.data)) # type: ignore
def test_edit_check_alert(self):
# setup data
policy = baker.make("automation.Policy")
agent = baker.make_recipe("agents.agent")
policy_disk_check = baker.make_recipe("checks.diskspace_check", policy=policy)
agent_disk_check = baker.make_recipe("checks.diskspace_check", agent=agent)
url_a = f"/checks/{agent_disk_check.pk}/check/"
url_p = f"/checks/{policy_disk_check.pk}/check/"
valid_payload = {"email_alert": False, "check_alert": True}
invalid_payload = {"email_alert": False}
with self.assertRaises(KeyError) as err:
resp = self.client.patch(url_a, invalid_payload, format="json")
with self.assertRaises(KeyError) as err:
resp = self.client.patch(url_p, invalid_payload, format="json")
resp = self.client.patch(url_a, valid_payload, format="json")
self.assertEqual(resp.status_code, 200)
resp = self.client.patch(url_p, valid_payload, format="json")
self.assertEqual(resp.status_code, 200)
self.check_not_authenticated("patch", url_a)
@patch("agents.models.Agent.nats_cmd")
def test_run_checks(self, nats_cmd):
agent = baker.make_recipe("agents.agent", version="1.4.1")
agent_b4_141 = baker.make_recipe("agents.agent", version="1.4.0")
url = f"/checks/runchecks/{agent_b4_141.pk}/"
url = f"{base_url}/{agent_b4_141.agent_id}/run/"
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
nats_cmd.assert_called_with({"func": "runchecks"}, wait=False)
nats_cmd.reset_mock()
nats_cmd.return_value = "busy"
url = f"/checks/runchecks/{agent.pk}/"
url = f"{base_url}/{agent.agent_id}/run/"
r = self.client.get(url)
self.assertEqual(r.status_code, 400)
nats_cmd.assert_called_with({"func": "runchecks"}, timeout=15)
@@ -343,7 +233,7 @@ class TestCheckViews(TacticalTestCase):
nats_cmd.reset_mock()
nats_cmd.return_value = "ok"
url = f"/checks/runchecks/{agent.pk}/"
url = f"{base_url}/{agent.agent_id}/run/"
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
nats_cmd.assert_called_with({"func": "runchecks"}, timeout=15)
@@ -351,7 +241,7 @@ class TestCheckViews(TacticalTestCase):
nats_cmd.reset_mock()
nats_cmd.return_value = "timeout"
url = f"/checks/runchecks/{agent.pk}/"
url = f"{base_url}/{agent.agent_id}/run/"
r = self.client.get(url)
self.assertEqual(r.status_code, 400)
nats_cmd.assert_called_with({"func": "runchecks"}, timeout=15)
@@ -379,7 +269,7 @@ class TestCheckViews(TacticalTestCase):
resp = self.client.patch("/checks/history/500/", format="json")
self.assertEqual(resp.status_code, 404)
url = f"/checks/history/{check.id}/"
url = f"/checks/{check.id}/history/"
# test with timeFilter last 30 days
data = {"timeFilter": 30}
@@ -873,74 +763,7 @@ class TestCheckTasks(TacticalTestCase):
self.assertEqual(new_check.status, "failing")
self.assertEqual(new_check.alert_severity, "info")
""" # test failing and attempt start
winsvc.restart_if_stopped = True
winsvc.alert_severity = "warning"
winsvc.save()
nats_cmd.return_value = "timeout"
data = {"id": winsvc.id, "exists": True, "status": "not running"}
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=winsvc.id)
self.assertEqual(new_check.status, "failing")
self.assertEqual(new_check.alert_severity, "warning")
nats_cmd.assert_called()
nats_cmd.reset_mock()
# test failing and attempt start
winsvc.alert_severity = "error"
winsvc.save()
nats_cmd.return_value = {"success": False, "errormsg": "Some Error"}
data = {"id": winsvc.id, "exists": True, "status": "not running"}
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=winsvc.id)
self.assertEqual(new_check.status, "failing")
self.assertEqual(new_check.alert_severity, "error")
nats_cmd.assert_called()
nats_cmd.reset_mock()
# test success and attempt start
nats_cmd.return_value = {"success": True}
data = {"id": winsvc.id, "exists": True, "status": "not running"}
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=winsvc.id)
self.assertEqual(new_check.status, "passing")
nats_cmd.assert_called()
nats_cmd.reset_mock()
# test failing and service not exist
data = {"id": winsvc.id, "exists": False, "status": ""}
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=winsvc.id)
self.assertEqual(new_check.status, "failing")
# test success and service not exist
winsvc.pass_if_svc_not_exist = True
winsvc.save()
data = {"id": winsvc.id, "exists": False, "status": ""}
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=winsvc.id)
self.assertEqual(new_check.status, "passing") """
""" def test_handle_eventlog_check(self):
def test_handle_eventlog_check(self):
from checks.models import Check
url = "/api/v3/checkrunner/"
@@ -984,6 +807,8 @@ class TestCheckTasks(TacticalTestCase):
],
}
no_logs_data = {"id": eventlog.id, "log": []}
# test failing when contains
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
@@ -993,11 +818,8 @@ class TestCheckTasks(TacticalTestCase):
self.assertEquals(new_check.alert_severity, "warning")
self.assertEquals(new_check.status, "failing")
# test passing when not contains and message
eventlog.event_message = "doesnt exist"
eventlog.save()
resp = self.client.patch(url, data, format="json")
# test passing when contains
resp = self.client.patch(url, no_logs_data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=eventlog.id)
@@ -1007,11 +829,9 @@ class TestCheckTasks(TacticalTestCase):
# test failing when not contains and message and source
eventlog.fail_when = "not_contains"
eventlog.alert_severity = "error"
eventlog.event_message = "doesnt exist"
eventlog.event_source = "doesnt exist"
eventlog.save()
resp = self.client.patch(url, data, format="json")
resp = self.client.patch(url, no_logs_data, format="json")
self.assertEqual(resp.status_code, 200)
new_check = Check.objects.get(pk=eventlog.id)
@@ -1020,10 +840,6 @@ class TestCheckTasks(TacticalTestCase):
self.assertEquals(new_check.alert_severity, "error")
# test passing when contains with source and message
eventlog.event_message = "test"
eventlog.event_source = "source"
eventlog.save()
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
@@ -1031,115 +847,261 @@ class TestCheckTasks(TacticalTestCase):
self.assertEquals(new_check.status, "passing")
# test failing with wildcard not contains and source
eventlog.event_id_is_wildcard = True
eventlog.event_source = "doesn't exist"
eventlog.event_message = ""
eventlog.event_id = 0
eventlog.save()
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
class TestCheckPermissions(TacticalTestCase):
def setUp(self):
self.setup_coresettings()
self.client_setup()
new_check = Check.objects.get(pk=eventlog.id)
def test_get_checks_permissions(self):
agent = baker.make_recipe("agents.agent")
policy = baker.make("automation.Policy")
unauthorized_agent = baker.make_recipe("agents.agent")
check = baker.make("checks.Check", agent=agent, _quantity=5)
unauthorized_check = baker.make(
"checks.Check", agent=unauthorized_agent, _quantity=7
)
self.assertEquals(new_check.status, "failing")
self.assertEquals(new_check.alert_severity, "error")
policy_checks = baker.make("checks.Check", policy=policy, _quantity=2)
# test passing with wildcard contains
eventlog.event_source = ""
eventlog.event_message = ""
eventlog.save()
# test super user access
self.check_authorized_superuser("get", f"{base_url}/")
self.check_authorized_superuser("get", f"/agents/{agent.agent_id}/checks/")
self.check_authorized_superuser(
"get", f"/agents/{unauthorized_agent.agent_id}/checks/"
)
self.check_authorized_superuser(
"get", f"/automation/policies/{policy.id}/checks/"
)
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
user = self.create_user_with_roles([])
self.client.force_authenticate(user=user) # type: ignore
new_check = Check.objects.get(pk=eventlog.id)
self.check_not_authorized("get", f"{base_url}/")
self.check_not_authorized("get", f"/agents/{agent.agent_id}/checks/")
self.check_not_authorized(
"get", f"/agents/{unauthorized_agent.agent_id}/checks/"
)
self.check_not_authorized("get", f"/automation/policies/{policy.id}/checks/")
self.assertEquals(new_check.status, "passing")
# add list software role to user
user.role.can_list_checks = True
user.role.save()
# test failing with wildcard contains and message
eventlog.fail_when = "contains"
eventlog.event_type = "error"
eventlog.alert_severity = "info"
eventlog.event_message = "test"
eventlog.event_source = ""
eventlog.save()
r = self.check_authorized("get", f"{base_url}/")
self.assertEqual(len(r.data), 14) # type: ignore
r = self.check_authorized("get", f"/agents/{agent.agent_id}/checks/")
self.assertEqual(len(r.data), 5) # type: ignore
r = self.check_authorized(
"get", f"/agents/{unauthorized_agent.agent_id}/checks/"
)
self.assertEqual(len(r.data), 7) # type: ignore
r = self.check_authorized("get", f"/automation/policies/{policy.id}/checks/")
self.assertEqual(len(r.data), 2) # type: ignore
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
# test limiting to client
user.role.can_view_clients.set([agent.client])
self.check_not_authorized(
"get", f"/agents/{unauthorized_agent.agent_id}/checks/"
)
self.check_authorized("get", f"/agents/{agent.agent_id}/checks/")
self.check_authorized("get", f"/automation/policies/{policy.id}/checks/")
new_check = Check.objects.get(pk=eventlog.id)
# make sure queryset is limited too
r = self.client.get(f"{base_url}/")
self.assertEqual(len(r.data), 7) # type: ignore
self.assertEquals(new_check.status, "failing")
self.assertEquals(new_check.alert_severity, "info")
def test_add_check_permissions(self):
agent = baker.make_recipe("agents.agent")
unauthorized_agent = baker.make_recipe("agents.agent")
policy = baker.make("automation.Policy")
# test passing with wildcard not contains message and source
eventlog.event_message = "doesnt exist"
eventlog.event_source = "doesnt exist"
eventlog.save()
policy_data = {
"policy": policy.id,
"check_type": "diskspace",
"disk": "C:",
"error_threshold": 55,
"warning_threshold": 0,
"fails_b4_alert": 3,
}
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
agent_data = {
"agent": agent.agent_id,
"check_type": "diskspace",
"disk": "C:",
"error_threshold": 55,
"warning_threshold": 0,
"fails_b4_alert": 3,
}
new_check = Check.objects.get(pk=eventlog.id)
unauthorized_agent_data = {
"agent": unauthorized_agent.agent_id,
"check_type": "diskspace",
"disk": "C:",
"error_threshold": 55,
"warning_threshold": 0,
"fails_b4_alert": 3,
}
self.assertEquals(new_check.status, "passing")
url = f"{base_url}/"
# test multiple events found and contains
# this should pass since only two events are found
eventlog.number_of_events_b4_alert = 3
eventlog.event_id_is_wildcard = False
eventlog.event_source = None
eventlog.event_message = None
eventlog.event_id = 123
eventlog.event_type = "error"
eventlog.fail_when = "contains"
eventlog.save()
for data in [policy_data, agent_data]:
# test superuser access
self.check_authorized_superuser("post", url, data)
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
user = self.create_user_with_roles([])
self.client.force_authenticate(user=user) # type: ignore
new_check = Check.objects.get(pk=eventlog.id)
# test user without role
self.check_not_authorized("post", url, data)
self.assertEquals(new_check.status, "passing")
# add user to role and test
setattr(user.role, "can_manage_checks", True)
user.role.save()
# this should pass since there are two events returned
eventlog.number_of_events_b4_alert = 2
eventlog.save()
self.check_authorized("post", url, data)
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
# limit user to client
user.role.can_view_clients.set([agent.client])
if "agent" in data.keys():
self.check_authorized("post", url, data)
self.check_not_authorized("post", url, unauthorized_agent_data)
else:
self.check_authorized("post", url, data)
new_check = Check.objects.get(pk=eventlog.id)
# mock the check delete method so it actually isn't deleted
@patch("checks.models.Check.delete")
def test_check_get_edit_delete_permissions(self, delete_check):
agent = baker.make_recipe("agents.agent")
unauthorized_agent = baker.make_recipe("agents.agent")
policy = baker.make("automation.Policy")
check = baker.make("checks.Check", agent=agent)
unauthorized_check = baker.make("checks.Check", agent=unauthorized_agent)
policy_check = baker.make("checks.Check", policy=policy)
self.assertEquals(new_check.status, "failing")
for method in ["get", "put", "delete"]:
# test not contains
# this should fail since only two events are found
eventlog.number_of_events_b4_alert = 3
eventlog.event_id_is_wildcard = False
eventlog.event_source = None
eventlog.event_message = None
eventlog.event_id = 123
eventlog.event_type = "error"
eventlog.fail_when = "not_contains"
eventlog.save()
url = f"{base_url}/{check.id}/"
unauthorized_url = f"{base_url}/{unauthorized_check.id}/"
policy_url = f"{base_url}/{policy_check.id}/"
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
# test superuser access
self.check_authorized_superuser(method, url)
self.check_authorized_superuser(method, unauthorized_url)
self.check_authorized_superuser(method, policy_url)
new_check = Check.objects.get(pk=eventlog.id)
user = self.create_user_with_roles([])
self.client.force_authenticate(user=user) # type: ignore
self.assertEquals(new_check.status, "failing")
# test user without role
self.check_not_authorized(method, url)
self.check_not_authorized(method, unauthorized_url)
self.check_not_authorized(method, policy_url)
# this should pass since there are two events returned
eventlog.number_of_events_b4_alert = 2
eventlog.save()
# add user to role and test
setattr(
user.role,
"can_list_checks" if method == "get" else "can_manage_checks",
True,
)
user.role.save()
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
self.check_authorized(method, url)
self.check_authorized(method, unauthorized_url)
self.check_authorized(method, policy_url)
new_check = Check.objects.get(pk=eventlog.id)
# limit user to client if agent check
user.role.can_view_clients.set([agent.client])
self.assertEquals(new_check.status, "passing") """
self.check_authorized(method, url)
self.check_not_authorized(method, unauthorized_url)
self.check_authorized(method, policy_url)
def test_check_action_permissions(self):
agent = baker.make_recipe("agents.agent")
unauthorized_agent = baker.make_recipe("agents.agent")
check = baker.make("checks.Check", agent=agent)
unauthorized_check = baker.make("checks.Check", agent=unauthorized_agent)
for action in ["reset", "run"]:
if action == "reset":
url = f"{base_url}/{check.id}/{action}/"
unauthorized_url = f"{base_url}/{unauthorized_check.id}/{action}/"
else:
url = f"{base_url}/{agent.agent_id}/{action}/"
unauthorized_url = f"{base_url}/{unauthorized_agent.agent_id}/{action}/"
# test superuser access
self.check_authorized_superuser("post", url)
self.check_authorized_superuser("post", unauthorized_url)
user = self.create_user_with_roles([])
self.client.force_authenticate(user=user) # type: ignore
# test user without role
self.check_not_authorized("post", url)
self.check_not_authorized("post", unauthorized_url)
# add user to role and test
setattr(
user.role,
"can_manage_checks" if action == "reset" else "can_run_checks",
True,
)
user.role.save()
self.check_authorized("post", url)
self.check_authorized("post", unauthorized_url)
# limit user to client if agent check
user.role.can_view_sites.set([agent.site])
self.check_authorized("post", url)
self.check_not_authorized("post", unauthorized_url)
def test_check_history_permissions(self):
agent = baker.make_recipe("agents.agent")
unauthorized_agent = baker.make_recipe("agents.agent")
check = baker.make("checks.Check", agent=agent)
unauthorized_check = baker.make("checks.Check", agent=unauthorized_agent)
url = f"{base_url}/{check.id}/history/"
unauthorized_url = f"{base_url}/{unauthorized_check.id}/history/"
# test superuser access
self.check_authorized_superuser("patch", url)
self.check_authorized_superuser("patch", unauthorized_url)
user = self.create_user_with_roles([])
self.client.force_authenticate(user=user) # type: ignore
# test user without role
self.check_not_authorized("patch", url)
self.check_not_authorized("patch", unauthorized_url)
# add user to role and test
setattr(
user.role,
"can_list_checks",
True,
)
user.role.save()
self.check_authorized("patch", url)
self.check_authorized("patch", unauthorized_url)
# limit user to client if agent check
user.role.can_view_sites.set([agent.site])
self.check_authorized("patch", url)
self.check_not_authorized("patch", unauthorized_url)
def test_policy_fields_to_copy_exists(self):
from .models import Check
fields = [i.name for i in Check._meta.get_fields()]
check = baker.make("checks.Check")
for i in check.policy_fields_to_copy: # type: ignore
self.assertIn(i, fields)

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.GetCheckHistory.as_view()),
path("", views.GetAddChecks.as_view()),
path("<int:pk>/", views.GetUpdateDeleteCheck.as_view()),
path("<int:pk>/reset/", views.ResetCheck.as_view()),
path("<agent:agent_id>/run/", views.run_checks),
path("<int:pk>/history/", views.GetCheckHistory.as_view()),
]

View File

@@ -9,57 +9,57 @@ from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.exceptions import PermissionDenied
from agents.models import Agent
from automation.models import Policy
from scripts.models import Script
from tacticalrmm.utils import notify_error
from tacticalrmm.permissions import _has_perm_on_agent
from .models import Check, CheckHistory
from .permissions import ManageChecksPerms, RunChecksPerms
from .permissions import ChecksPerms, RunChecksPerms
from .serializers import CheckHistorySerializer, CheckSerializer
class AddCheck(APIView):
permission_classes = [IsAuthenticated, ManageChecksPerms]
class GetAddChecks(APIView):
permission_classes = [IsAuthenticated, ChecksPerms]
def get(self, request, agent_id=None, policy=None):
if agent_id:
agent = get_object_or_404(Agent, agent_id=agent_id)
checks = Check.objects.filter(agent=agent)
elif policy:
policy = get_object_or_404(Policy, id=policy)
checks = Check.objects.filter(policy=policy)
else:
checks = Check.objects.filter_by_role(request.user)
return Response(CheckSerializer(checks, many=True).data)
def post(self, request):
from automation.tasks import generate_agent_checks_task
policy = None
agent = None
data = request.data.copy()
# Determine if adding check to Agent and replace agent_id with pk
if "agent" in data.keys():
agent = get_object_or_404(Agent, agent_id=data["agent"])
if not _has_perm_on_agent(request.user, agent.agent_id):
raise PermissionDenied()
# Determine if adding check to Policy or Agent
if "policy" in request.data:
policy = get_object_or_404(Policy, id=request.data["policy"])
# Object used for filter and save
parent = {"policy": policy}
else:
agent = get_object_or_404(Agent, pk=request.data["pk"])
parent = {"agent": agent}
script = None
if "script" in request.data["check"]:
script = get_object_or_404(Script, pk=request.data["check"]["script"])
data["agent"] = agent.pk
# set event id to 0 if wildcard because it needs to be an integer field for db
# will be ignored anyway by the agent when doing wildcard check
if (
request.data["check"]["check_type"] == "eventlog"
and request.data["check"]["event_id_is_wildcard"]
):
request.data["check"]["event_id"] = 0
if data["check_type"] == "eventlog" and data["event_id_is_wildcard"]:
data["event_id"] = 0
serializer = CheckSerializer(
data=request.data["check"], partial=True, context=parent
)
serializer = CheckSerializer(data=data, partial=True)
serializer.is_valid(raise_exception=True)
new_check = serializer.save(**parent, script=script)
new_check = serializer.save()
# Generate policy Checks
if policy:
generate_agent_checks_task.delay(policy=policy.pk)
elif agent:
if "policy" in data.keys():
generate_agent_checks_task.delay(policy=data["policy"])
elif "agent" in data.keys():
checks = agent.agentchecks.filter( # type: ignore
check_type=new_check.check_type, managed_by_policy=True
)
@@ -81,44 +81,43 @@ class AddCheck(APIView):
class GetUpdateDeleteCheck(APIView):
permission_classes = [IsAuthenticated, ManageChecksPerms]
permission_classes = [IsAuthenticated, ChecksPerms]
def get(self, request, pk):
check = get_object_or_404(Check, pk=pk)
if check.agent and not _has_perm_on_agent(request.user, check.agent.agent_id):
raise PermissionDenied()
return Response(CheckSerializer(check).data)
def patch(self, request, pk):
def put(self, request, pk):
from automation.tasks import update_policy_check_fields_task
check = get_object_or_404(Check, pk=pk)
data = request.data.copy()
if check.agent and not _has_perm_on_agent(request.user, check.agent.agent_id):
raise PermissionDenied()
# remove fields that should not be changed when editing a check from the frontend
if (
"check_alert" not in request.data.keys()
and "check_reset" not in request.data.keys()
):
[request.data.pop(i) for i in check.non_editable_fields]
[data.pop(i) for i in Check.non_editable_fields() if i in data.keys()]
# set event id to 0 if wildcard because it needs to be an integer field for db
# will be ignored anyway by the agent when doing wildcard check
if check.check_type == "eventlog":
try:
request.data["event_id_is_wildcard"]
data["event_id_is_wildcard"]
except KeyError:
pass
else:
if request.data["event_id_is_wildcard"]:
request.data["event_id"] = 0
if data["event_id_is_wildcard"]:
data["event_id"] = 0
serializer = CheckSerializer(instance=check, data=request.data, partial=True)
serializer = CheckSerializer(instance=check, data=data, partial=True)
serializer.is_valid(raise_exception=True)
check = serializer.save()
# resolve any alerts that are open
if "check_reset" in request.data.keys():
if check.alert.filter(resolved=False).exists():
check.alert.get(resolved=False).resolve()
if check.policy:
update_policy_check_fields_task.delay(check=check.pk)
@@ -129,6 +128,9 @@ class GetUpdateDeleteCheck(APIView):
check = get_object_or_404(Check, pk=pk)
if check.agent and not _has_perm_on_agent(request.user, check.agent.agent_id):
raise PermissionDenied()
check.delete()
# Policy check deleted
@@ -137,18 +139,42 @@ class GetUpdateDeleteCheck(APIView):
# Re-evaluate agent checks is policy was enforced
if check.policy.enforced:
generate_agent_checks_task.delay(policy=check.policy)
generate_agent_checks_task.delay(policy=check.policy.pk)
# Agent check deleted
elif check.agent:
check.agent.generate_checks_from_policies()
generate_agent_checks_task.delay(agents=[check.agent.pk])
return Response(f"{check.readable_desc} was deleted!")
class ResetCheck(APIView):
permission_classes = [IsAuthenticated, ChecksPerms]
def post(self, request, pk):
check = get_object_or_404(Check, pk=pk)
if check.agent and not _has_perm_on_agent(request.user, check.agent.agent_id):
raise PermissionDenied()
check.status = "passing"
check.save()
# resolve any alerts that are open
if check.alert.filter(resolved=False).exists():
check.alert.get(resolved=False).resolve()
return Response("The check status was reset")
class GetCheckHistory(APIView):
def patch(self, request, checkpk):
check = get_object_or_404(Check, pk=checkpk)
permission_classes = [IsAuthenticated, ChecksPerms]
def patch(self, request, pk):
check = get_object_or_404(Check, pk=pk)
if check.agent and not _has_perm_on_agent(request.user, check.agent.agent_id):
raise PermissionDenied()
timeFilter = Q()
@@ -160,7 +186,7 @@ class GetCheckHistory(APIView):
- djangotime.timedelta(days=request.data["timeFilter"]),
)
check_history = CheckHistory.objects.filter(check_id=checkpk).filter(timeFilter).order_by("-x") # type: ignore
check_history = CheckHistory.objects.filter(check_id=pk).filter(timeFilter).order_by("-x") # type: ignore
return Response(
CheckHistorySerializer(
@@ -171,8 +197,8 @@ class GetCheckHistory(APIView):
@api_view()
@permission_classes([IsAuthenticated, RunChecksPerms])
def run_checks(request, pk):
agent = get_object_or_404(Agent, pk=pk)
def run_checks(request, agent_id):
agent = get_object_or_404(Agent, agent_id=agent_id)
if pyver.parse(agent.version) >= pyver.parse("1.4.1"):
r = asyncio.run(agent.nats_cmd({"func": "runchecks"}, timeout=15))
@@ -185,14 +211,3 @@ def run_checks(request, pk):
else:
asyncio.run(agent.nats_cmd({"func": "runchecks"}, wait=False))
return Response(f"Checks will now be re-run on {agent.hostname}")
@api_view()
def load_checks(request, pk):
checks = Check.objects.filter(agent__pk=pk)
return Response(CheckSerializer(checks, many=True).data)
@api_view()
def get_disks_for_policies(request):
return Response(Check.all_disks())

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(
@@ -70,29 +74,20 @@ class Client(BaseAuditModel):
@property
def agent_count(self) -> int:
return Agent.objects.filter(site__client=self).count()
return Agent.objects.defer(*AGENT_DEFER).filter(site__client=self).count()
@property
def has_maintenanace_mode_agents(self):
return (
Agent.objects.filter(site__client=self, maintenance_mode=True).count() > 0
Agent.objects.defer(*AGENT_DEFER)
.filter(site__client=self, maintenance_mode=True)
.count()
> 0
)
@property
def has_failing_checks(self):
agents = (
Agent.objects.only(
"pk",
"overdue_email_alert",
"overdue_text_alert",
"last_seen",
"overdue_time",
"offline_time",
)
.filter(site__client=self)
.prefetch_related("agentchecks", "autotasks")
)
agents = Agent.objects.defer(*AGENT_DEFER).filter(site__client=self)
data = {"error": False, "warning": False}
for agent in agents:
@@ -130,6 +125,8 @@ class Client(BaseAuditModel):
class Site(BaseAuditModel):
objects = PermissionQuerySet.as_manager()
client = models.ForeignKey(Client, related_name="sites", on_delete=models.CASCADE)
name = models.CharField(max_length=255)
block_policy_inheritance = models.BooleanField(default=False)
@@ -189,23 +186,21 @@ class Site(BaseAuditModel):
@property
def agent_count(self) -> int:
return Agent.objects.filter(site=self).count()
return Agent.objects.defer(*AGENT_DEFER).filter(site=self).count()
@property
def has_maintenanace_mode_agents(self):
return Agent.objects.filter(site=self, maintenance_mode=True).count() > 0
return (
Agent.objects.defer(*AGENT_DEFER)
.filter(site=self, maintenance_mode=True)
.count()
> 0
)
@property
def has_failing_checks(self):
agents = (
Agent.objects.only(
"pk",
"overdue_email_alert",
"overdue_text_alert",
"last_seen",
"overdue_time",
"offline_time",
)
Agent.objects.defer(*AGENT_DEFER)
.filter(site=self)
.prefetch_related("agentchecks", "autotasks")
)
@@ -257,10 +252,9 @@ ARCH_CHOICES = [
class Deployment(models.Model):
objects = PermissionQuerySet.as_manager()
uid = models.UUIDField(primary_key=False, default=uuid.uuid4, editable=False)
client = models.ForeignKey(
"clients.Client", related_name="deployclients", on_delete=models.CASCADE
)
site = models.ForeignKey(
"clients.Site", related_name="deploysites", on_delete=models.CASCADE
)
@@ -279,6 +273,10 @@ class Deployment(models.Model):
def __str__(self):
return f"{self.client} - {self.site} - {self.mon_type}"
@property
def client(self):
return self.site.client
class ClientCustomField(models.Model):
client = models.ForeignKey(

View File

@@ -1,27 +1,45 @@
from rest_framework import permissions
from tacticalrmm.permissions import _has_perm
from tacticalrmm.permissions import _has_perm, _has_perm_on_client, _has_perm_on_site
class ManageClientsPerms(permissions.BasePermission):
class ClientsPerms(permissions.BasePermission):
def has_permission(self, r, view):
if r.method == "GET":
return True
return _has_perm(r, "can_manage_clients")
if "pk" in view.kwargs.keys():
return _has_perm(r, "can_list_clients") and _has_perm_on_client(
r.user, view.kwargs["pk"]
)
else:
return _has_perm(r, "can_list_clients")
elif r.method == "PUT" or r.method == "DELETE":
return _has_perm(r, "can_manage_clients") and _has_perm_on_client(
r.user, view.kwargs["pk"]
)
else:
return _has_perm(r, "can_manage_clients")
class ManageSitesPerms(permissions.BasePermission):
class SitesPerms(permissions.BasePermission):
def has_permission(self, r, view):
if r.method == "GET":
return True
return _has_perm(r, "can_manage_sites")
if "pk" in view.kwargs.keys():
return _has_perm(r, "can_list_sites") and _has_perm_on_site(
r.user, view.kwargs["pk"]
)
else:
return _has_perm(r, "can_list_sites")
elif r.method == "PUT" or r.method == "DELETE":
return _has_perm(r, "can_manage_sites") and _has_perm_on_site(
r.user, view.kwargs["pk"]
)
else:
return _has_perm(r, "can_manage_sites")
class ManageDeploymentPerms(permissions.BasePermission):
class DeploymentPerms(permissions.BasePermission):
def has_permission(self, r, view):
if r.method == "GET":
return True
return _has_perm(r, "can_manage_deployments")
return _has_perm(r, "can_list_deployments")
else:
return _has_perm(r, "can_manage_deployments")

View File

@@ -1,9 +1,8 @@
from django.db.models.base import Model
from rest_framework.serializers import (
ModelSerializer,
ReadOnlyField,
Serializer,
ValidationError,
SerializerMethodField,
)
from .models import Client, ClientCustomField, Deployment, Site, SiteCustomField
@@ -32,6 +31,8 @@ class SiteSerializer(ModelSerializer):
client_name = ReadOnlyField(source="client.name")
custom_fields = SiteCustomFieldSerializer(many=True, read_only=True)
agent_count = ReadOnlyField()
maintenance_mode = ReadOnlyField(source="has_maintenanace_mode_agents")
failing_checks = ReadOnlyField(source="has_failing_checks")
class Meta:
model = Site
@@ -46,6 +47,8 @@ class SiteSerializer(ModelSerializer):
"custom_fields",
"agent_count",
"block_policy_inheritance",
"maintenance_mode",
"failing_checks",
)
def validate(self, val):
@@ -55,6 +58,20 @@ class SiteSerializer(ModelSerializer):
return val
class SiteMinimumSerializer(ModelSerializer):
client_name = ReadOnlyField(source="client.name")
class Meta:
model = Site
fields = "__all__"
class ClientMinimumSerializer(ModelSerializer):
class Meta:
model = Client
fields = "__all__"
class ClientCustomFieldSerializer(ModelSerializer):
class Meta:
model = ClientCustomField
@@ -75,9 +92,17 @@ class ClientCustomFieldSerializer(ModelSerializer):
class ClientSerializer(ModelSerializer):
sites = SiteSerializer(many=True, read_only=True)
sites = SerializerMethodField()
custom_fields = ClientCustomFieldSerializer(many=True, read_only=True)
agent_count = ReadOnlyField()
maintenance_mode = ReadOnlyField(source="has_maintenanace_mode_agents")
failing_checks = ReadOnlyField(source="has_failing_checks")
def get_sites(self, obj):
return SiteSerializer(
obj.sites.select_related("client").filter_by_role(self.context["user"]),
many=True,
).data
class Meta:
model = Client
@@ -91,6 +116,8 @@ class ClientSerializer(ModelSerializer):
"sites",
"custom_fields",
"agent_count",
"maintenance_mode",
"failing_checks",
)
def validate(self, val):
@@ -100,25 +127,6 @@ class ClientSerializer(ModelSerializer):
return val
class SiteTreeSerializer(ModelSerializer):
maintenance_mode = ReadOnlyField(source="has_maintenanace_mode_agents")
failing_checks = ReadOnlyField(source="has_failing_checks")
class Meta:
model = Site
fields = "__all__"
class ClientTreeSerializer(ModelSerializer):
sites = SiteTreeSerializer(many=True, read_only=True)
maintenance_mode = ReadOnlyField(source="has_maintenanace_mode_agents")
failing_checks = ReadOnlyField(source="has_failing_checks")
class Meta:
model = Client
fields = "__all__"
class DeploymentSerializer(ModelSerializer):
client_id = ReadOnlyField(source="client.id")
site_id = ReadOnlyField(source="site.id")

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

@@ -8,17 +8,22 @@ from django.utils import timezone as djangotime
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.exceptions import PermissionDenied
from agents.models import Agent
from core.models import CoreSettings
from tacticalrmm.utils import notify_error
from tacticalrmm.permissions import _has_perm_on_client, _has_perm_on_site
from .models import Client, ClientCustomField, Deployment, Site, SiteCustomField
from .permissions import ManageClientsPerms, ManageDeploymentPerms, ManageSitesPerms
from .permissions import (
ClientsPerms,
DeploymentPerms,
SitesPerms,
)
from .serializers import (
ClientCustomFieldSerializer,
ClientSerializer,
ClientTreeSerializer,
DeploymentSerializer,
SiteCustomFieldSerializer,
SiteSerializer,
@@ -26,11 +31,15 @@ from .serializers import (
class GetAddClients(APIView):
permission_classes = [IsAuthenticated, ManageClientsPerms]
permission_classes = [IsAuthenticated, ClientsPerms]
def get(self, request):
clients = Client.objects.all()
return Response(ClientSerializer(clients, many=True).data)
clients = Client.objects.select_related(
"workstation_policy", "server_policy", "alert_template"
).filter_by_role(request.user)
return Response(
ClientSerializer(clients, context={"user": request.user}, many=True).data
)
def post(self, request):
# create client
@@ -67,15 +76,19 @@ class GetAddClients(APIView):
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(f"{client} was added!")
# add user to allowed clients in role if restricted user created the client
if request.user.role and request.user.role.can_view_clients.exists():
request.user.role.can_view_clients.add(client)
return Response(f"{client.name} was added")
class GetUpdateClient(APIView):
permission_classes = [IsAuthenticated, ManageClientsPerms]
class GetUpdateDeleteClient(APIView):
permission_classes = [IsAuthenticated, ClientsPerms]
def get(self, request, pk):
client = get_object_or_404(Client, pk=pk)
return Response(ClientSerializer(client).data)
return Response(ClientSerializer(client, context={"user": request.user}).data)
def put(self, request, pk):
client = get_object_or_404(Client, pk=pk)
@@ -107,46 +120,41 @@ class GetUpdateClient(APIView):
serializer.is_valid(raise_exception=True)
serializer.save()
return Response("The Client was updated")
return Response("{client} was updated")
class DeleteClient(APIView):
permission_classes = [IsAuthenticated, ManageClientsPerms]
def delete(self, request, pk, sitepk):
def delete(self, request, pk):
from automation.tasks import generate_agent_checks_task
client = get_object_or_404(Client, pk=pk)
agents = Agent.objects.filter(site__client=client)
if not sitepk:
# only run tasks if it affects clients
if client.agent_count > 0 and "move_to_site" in request.query_params.keys():
agents = Agent.objects.filter(site__client=client)
site = get_object_or_404(Site, pk=request.query_params["move_to_site"])
agents.update(site=site)
generate_agent_checks_task.delay(all=True, create_tasks=True)
elif client.agent_count > 0:
return notify_error(
"There needs to be a site specified to move existing agents to"
"Agents exist under this client. There needs to be a site specified to move existing agents to"
)
site = get_object_or_404(Site, pk=sitepk)
agents.update(site=site)
generate_agent_checks_task.delay(all=True, create_tasks=True)
client.delete()
return Response(f"{client.name} was deleted!")
class GetClientTree(APIView):
def get(self, request):
clients = Client.objects.all()
return Response(ClientTreeSerializer(clients, many=True).data)
return Response(f"{client.name} was deleted")
class GetAddSites(APIView):
permission_classes = [IsAuthenticated, ManageSitesPerms]
permission_classes = [IsAuthenticated, SitesPerms]
def get(self, request):
sites = Site.objects.all()
sites = Site.objects.filter_by_role(request.user)
return Response(SiteSerializer(sites, many=True).data)
def post(self, request):
if not _has_perm_on_client(request.user, request.data["site"]["client"]):
raise PermissionDenied()
serializer = SiteSerializer(data=request.data["site"])
serializer.is_valid(raise_exception=True)
site = serializer.save()
@@ -163,11 +171,15 @@ class GetAddSites(APIView):
serializer.is_valid(raise_exception=True)
serializer.save()
# add user to allowed sites in role if restricted user created the client
if request.user.role and request.user.role.can_view_sites.exists():
request.user.role.can_view_sites.add(site)
return Response(f"Site {site.name} was added!")
class GetUpdateSite(APIView):
permission_classes = [IsAuthenticated, ManageSitesPerms]
class GetUpdateDeleteSite(APIView):
permission_classes = [IsAuthenticated, SitesPerms]
def get(self, request, pk):
site = get_object_or_404(Site, pk=pk)
@@ -208,50 +220,47 @@ class GetUpdateSite(APIView):
serializer.is_valid(raise_exception=True)
serializer.save()
return Response("Site was edited!")
return Response("Site was edited")
class DeleteSite(APIView):
permission_classes = [IsAuthenticated, ManageSitesPerms]
def delete(self, request, pk, sitepk):
def delete(self, request, pk):
from automation.tasks import generate_agent_checks_task
site = get_object_or_404(Site, pk=pk)
if site.client.sites.count() == 1:
return notify_error("A client must have at least 1 site.")
agents = Agent.objects.filter(site=site)
# only run tasks if it affects clients
if site.agent_count > 0 and "move_to_site" in request.query_params.keys():
agents = Agent.objects.filter(site=site)
new_site = get_object_or_404(Site, pk=request.query_params["move_to_site"])
agents.update(site=new_site)
generate_agent_checks_task.delay(all=True, create_tasks=True)
if not sitepk:
elif site.agent_count > 0:
return notify_error(
"There needs to be a site specified to move the agents to"
)
agent_site = get_object_or_404(Site, pk=sitepk)
agents.update(site=agent_site)
generate_agent_checks_task.delay(all=True, create_tasks=True)
site.delete()
return Response(f"{site.name} was deleted!")
return Response(f"{site.name} was deleted")
class AgentDeployment(APIView):
permission_classes = [IsAuthenticated, ManageDeploymentPerms]
permission_classes = [IsAuthenticated, DeploymentPerms]
def get(self, request):
deps = Deployment.objects.all()
deps = Deployment.objects.filter_by_role(request.user)
return Response(DeploymentSerializer(deps, many=True).data)
def post(self, request):
from knox.models import AuthToken
from accounts.models import User
client = get_object_or_404(Client, pk=request.data["client"])
site = get_object_or_404(Site, pk=request.data["site"])
if not _has_perm_on_site(request.user, site.pk):
raise PermissionDenied()
installer_user = User.objects.filter(is_installer_user=True).first()
expires = dt.datetime.strptime(
@@ -268,7 +277,6 @@ class AgentDeployment(APIView):
}
Deployment(
client=client,
site=site,
expiry=expires,
mon_type=request.data["agenttype"],
@@ -277,17 +285,21 @@ class AgentDeployment(APIView):
token_key=token,
install_flags=flags,
).save()
return Response("ok")
return Response("The deployment was added successfully")
def delete(self, request, pk):
d = get_object_or_404(Deployment, pk=pk)
if not _has_perm_on_site(request.user, d.site.pk):
raise PermissionDenied()
try:
d.auth_token.delete()
except:
pass
d.delete()
return Response("ok")
return Response("The deployment was deleted")
class GenerateAgent(APIView):

View File

@@ -0,0 +1,24 @@
import os
import json
from django.core.management.base import BaseCommand
from django.conf import settings
class Command(BaseCommand):
help = "Generate conf for nats-api"
def handle(self, *args, **kwargs):
db = settings.DATABASES["default"]
config = {
"key": settings.SECRET_KEY,
"natsurl": f"tls://{settings.ALLOWED_HOSTS[0]}:4222",
"user": db["USER"],
"pass": db["PASSWORD"],
"host": db["HOST"],
"port": int(db["PORT"]),
"dbname": db["NAME"],
}
conf = os.path.join(settings.BASE_DIR, "nats-api.conf")
with open(conf, "w") as f:
json.dump(config, f)

View File

@@ -1,7 +1,9 @@
import base64
from django.core.management.base import BaseCommand
from logs.models import PendingAction
from scripts.models import Script
from accounts.models import User
class Command(BaseCommand):
@@ -13,3 +15,21 @@ class Command(BaseCommand):
# load community scripts into the db
Script.load_community_scripts()
# make sure installer user is set to block_dashboard_logins
if User.objects.filter(is_installer_user=True).exists():
for user in User.objects.filter(is_installer_user=True):
user.block_dashboard_login = True
user.save()
# convert script base64 field to text field
user_scripts = Script.objects.exclude(script_type="builtin").filter(
script_body=""
)
for script in user_scripts:
# decode base64 string
script.script_body = base64.b64decode(
script.code_base64.encode("ascii", "ignore")
).decode("ascii", "ignore")
# script.hash_script_body() # also saves script
script.save(update_fields=["script_body"])

View File

@@ -0,0 +1,73 @@
# Generated by Django 3.2.6 on 2021-09-05 16:06
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0026_coresettings_audit_log_prune_days'),
]
operations = [
migrations.AddField(
model_name='customfield',
name='created_by',
field=models.CharField(blank=True, max_length=100, null=True),
),
migrations.AddField(
model_name='customfield',
name='created_time',
field=models.DateTimeField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='customfield',
name='modified_by',
field=models.CharField(blank=True, max_length=100, null=True),
),
migrations.AddField(
model_name='customfield',
name='modified_time',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AddField(
model_name='globalkvstore',
name='created_by',
field=models.CharField(blank=True, max_length=100, null=True),
),
migrations.AddField(
model_name='globalkvstore',
name='created_time',
field=models.DateTimeField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='globalkvstore',
name='modified_by',
field=models.CharField(blank=True, max_length=100, null=True),
),
migrations.AddField(
model_name='globalkvstore',
name='modified_time',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AddField(
model_name='urlaction',
name='created_by',
field=models.CharField(blank=True, max_length=100, null=True),
),
migrations.AddField(
model_name='urlaction',
name='created_time',
field=models.DateTimeField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='urlaction',
name='modified_by',
field=models.CharField(blank=True, max_length=100, null=True),
),
migrations.AddField(
model_name='urlaction',
name='modified_time',
field=models.DateTimeField(auto_now=True, null=True),
),
]

View File

@@ -0,0 +1,53 @@
# Generated by Django 3.2.6 on 2021-09-17 19:54
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0027_auto_20210905_1606"),
]
operations = [
migrations.AlterField(
model_name="coresettings",
name="created_by",
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AlterField(
model_name="coresettings",
name="modified_by",
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AlterField(
model_name="customfield",
name="created_by",
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AlterField(
model_name="customfield",
name="modified_by",
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AlterField(
model_name="globalkvstore",
name="created_by",
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AlterField(
model_name="globalkvstore",
name="modified_by",
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AlterField(
model_name="urlaction",
name="created_by",
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AlterField(
model_name="urlaction",
name="modified_by",
field=models.CharField(blank=True, max_length=255, null=True),
),
]

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
import requests
import smtplib
from email.message import EmailMessage
from django.db.models.enums import Choices
import pytz
from django.conf import settings
@@ -8,6 +8,7 @@ from django.contrib.postgres.fields import ArrayField
from django.core.exceptions import ValidationError
from django.db import models
from twilio.rest import Client as TwClient
from twilio.base.exceptions import TwilioRestException
from logs.models import BaseAuditModel, DebugLog, LOG_LEVEL_CHOICES
@@ -118,7 +119,6 @@ class CoreSettings(BaseAuditModel):
def sms_is_configured(self):
return all(
[
self.sms_alert_recipients,
self.twilio_auth_token,
self.twilio_account_sid,
self.twilio_number,
@@ -130,7 +130,6 @@ class CoreSettings(BaseAuditModel):
# smtp with username/password authentication
if (
self.smtp_requires_auth
and self.email_alert_recipients
and self.smtp_from_email
and self.smtp_host
and self.smtp_host_user
@@ -141,7 +140,6 @@ class CoreSettings(BaseAuditModel):
# smtp relay
elif (
not self.smtp_requires_auth
and self.email_alert_recipients
and self.smtp_from_email
and self.smtp_host
and self.smtp_port
@@ -151,10 +149,10 @@ class CoreSettings(BaseAuditModel):
return False
def send_mail(self, subject, body, alert_template=None, test=False):
if not alert_template and not self.email_is_configured:
if test:
return "Missing required fields (need at least 1 recipient)"
if test and not self.email_is_configured:
return "There needs to be at least one email recipient configured"
# return since email must be configured to continue
elif not self.email_is_configured:
return False
# override email from if alert_template is passed and is set
@@ -169,6 +167,9 @@ class CoreSettings(BaseAuditModel):
else:
email_recipients = ", ".join(self.email_alert_recipients)
if not email_recipients:
return "There needs to be at least one email recipient configured"
try:
msg = EmailMessage()
msg["Subject"] = subject
@@ -195,22 +196,29 @@ class CoreSettings(BaseAuditModel):
else:
return True
def send_sms(self, body, alert_template=None):
if not alert_template or not self.sms_is_configured:
return
def send_sms(self, body, alert_template=None, test=False):
if not self.sms_is_configured:
return "Sms alerting is not setup correctly."
# override email recipients if alert_template is passed and is set
if alert_template and alert_template.text_recipients:
text_recipients = alert_template.email_recipients
text_recipients = alert_template.text_recipients
else:
text_recipients = self.sms_alert_recipients
if not text_recipients:
return "No sms recipients found"
tw_client = TwClient(self.twilio_account_sid, self.twilio_auth_token)
for num in text_recipients:
try:
tw_client.messages.create(body=body, to=num, from_=self.twilio_number)
except Exception as e:
except TwilioRestException as e:
DebugLog.error(message=f"SMS failed to send: {e}")
if test:
return str(e)
return True
@staticmethod
def serialize(core):
@@ -232,7 +240,7 @@ FIELD_TYPE_CHOICES = (
MODEL_CHOICES = (("client", "Client"), ("site", "Site"), ("agent", "Agent"))
class CustomField(models.Model):
class CustomField(BaseAuditModel):
order = models.PositiveIntegerField(default=0)
model = models.CharField(max_length=25, choices=MODEL_CHOICES)
@@ -261,6 +269,12 @@ class CustomField(models.Model):
def __str__(self):
return self.name
@staticmethod
def serialize(field):
from .serializers import CustomFieldSerializer
return CustomFieldSerializer(field).data
@property
def default_value(self):
if self.type == "multiple":
@@ -300,26 +314,63 @@ class CodeSignToken(models.Model):
super(CodeSignToken, self).save(*args, **kwargs)
@property
def is_valid(self) -> bool:
if not self.token:
return False
errors = []
for url in settings.EXE_GEN_URLS:
try:
r = requests.post(
f"{url}/api/v1/checktoken",
json={"token": self.token},
headers={"Content-type": "application/json"},
timeout=15,
)
except Exception as e:
errors.append(str(e))
else:
errors = []
break
if errors:
return False
return r.status_code == 200
def __str__(self):
return "Code signing token"
class GlobalKVStore(models.Model):
class GlobalKVStore(BaseAuditModel):
name = models.CharField(max_length=25)
value = models.TextField()
def __str__(self):
return self.name
@staticmethod
def serialize(store):
from .serializers import KeyStoreSerializer
OPEN_ACTIONS = (("window", "New Window"), ("tab", "New Tab"))
return KeyStoreSerializer(store).data
class URLAction(models.Model):
class URLAction(BaseAuditModel):
name = models.CharField(max_length=25)
desc = models.CharField(max_length=100, null=True, blank=True)
pattern = models.TextField()
def __str__(self):
return self.name
@staticmethod
def serialize(action):
from .serializers import URLActionSerializer
return URLActionSerializer(action).data
RUN_ON_CHOICES = (
("client", "Client"),

View File

@@ -3,14 +3,17 @@ from rest_framework import permissions
from tacticalrmm.permissions import _has_perm
class ViewCoreSettingsPerms(permissions.BasePermission):
class CoreSettingsPerms(permissions.BasePermission):
def has_permission(self, r, view):
return _has_perm(r, "can_view_core_settings")
if r.method == "GET":
return _has_perm(r, "can_view_core_settings")
else:
return _has_perm(r, "can_edit_core_settings")
class EditCoreSettingsPerms(permissions.BasePermission):
class URLActionPerms(permissions.BasePermission):
def has_permission(self, r, view):
return _has_perm(r, "can_edit_core_settings")
return _has_perm(r, "can_run_urlactions")
class ServerMaintPerms(permissions.BasePermission):
@@ -21,3 +24,11 @@ class ServerMaintPerms(permissions.BasePermission):
class CodeSignPerms(permissions.BasePermission):
def has_permission(self, r, view):
return _has_perm(r, "can_code_sign")
class CustomFieldPerms(permissions.BasePermission):
def has_permission(self, r, view):
if r.method == "GET":
return _has_perm(r, "can_view_customfields")
else:
return _has_perm(r, "can_manage_customfields")

View File

@@ -9,6 +9,7 @@ from alerts.tasks import prune_resolved_alerts
from core.models import CoreSettings
from logs.tasks import prune_debug_log, prune_audit_log
from tacticalrmm.celery import app
from tacticalrmm.utils import AGENT_DEFER
@app.task
@@ -58,7 +59,7 @@ def core_maintenance_tasks():
def cache_db_fields_task():
from agents.models import Agent
for agent in Agent.objects.all():
for agent in Agent.objects.defer(*AGENT_DEFER):
agent.pending_actions_count = agent.pendingactions.filter(
status="pending"
).count()

View File

@@ -82,7 +82,7 @@ class TestCoreTasks(TacticalTestCase):
self.check_not_authenticated("get", url)
def test_get_core_settings(self):
url = "/core/getcoresettings/"
url = "/core/settings/"
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
@@ -90,7 +90,7 @@ class TestCoreTasks(TacticalTestCase):
@patch("automation.tasks.generate_agent_checks_task.delay")
def test_edit_coresettings(self, generate_agent_checks_task):
url = "/core/editsettings/"
url = "/core/settings/"
# setup
policies = baker.make("automation.Policy", _quantity=2)
@@ -99,7 +99,7 @@ class TestCoreTasks(TacticalTestCase):
"smtp_from_email": "newexample@example.com",
"mesh_token": "New_Mesh_Token",
}
r = self.client.patch(url, data)
r = self.client.put(url, data)
self.assertEqual(r.status_code, 200)
self.assertEqual(
CoreSettings.objects.first().smtp_from_email, data["smtp_from_email"]
@@ -113,7 +113,7 @@ class TestCoreTasks(TacticalTestCase):
"workstation_policy": policies[0].id, # type: ignore
"server_policy": policies[1].id, # type: ignore
}
r = self.client.patch(url, data)
r = self.client.put(url, data)
self.assertEqual(r.status_code, 200)
self.assertEqual(CoreSettings.objects.first().server_policy.id, policies[1].id) # type: ignore
self.assertEqual(
@@ -128,13 +128,13 @@ class TestCoreTasks(TacticalTestCase):
data = {
"workstation_policy": "",
}
r = self.client.patch(url, data)
r = self.client.put(url, data)
self.assertEqual(r.status_code, 200)
self.assertEqual(CoreSettings.objects.first().workstation_policy, None)
self.assertEqual(generate_agent_checks_task.call_count, 1)
self.check_not_authenticated("patch", url)
self.check_not_authenticated("put", url)
@patch("tacticalrmm.utils.reload_nats")
@patch("autotasks.tasks.remove_orphaned_win_tasks.delay")
@@ -404,10 +404,10 @@ class TestCoreTasks(TacticalTestCase):
url = "/core/urlaction/run/"
# test not found
r = self.client.patch(url, {"agent": 500, "action": 500})
r = self.client.patch(url, {"agent_id": 500, "action": 500})
self.assertEqual(r.status_code, 404)
data = {"agent": agent.id, "action": action.id} # type: ignore
data = {"agent_id": agent.agent_id, "action": action.id} # type: ignore
r = self.client.patch(url, data)
self.assertEqual(r.status_code, 200)
@@ -417,3 +417,9 @@ class TestCoreTasks(TacticalTestCase):
)
self.check_not_authenticated("patch", url)
class TestCorePermissions(TacticalTestCase):
def setUp(self):
self.client_setup()
self.setup_coresettings()

View File

@@ -4,8 +4,7 @@ from . import views
urlpatterns = [
path("uploadmesh/", views.UploadMeshAgent.as_view()),
path("getcoresettings/", views.get_core_settings),
path("editsettings/", views.edit_settings),
path("settings/", views.GetEditCoreSettings.as_view()),
path("version/", views.version),
path("emailtest/", views.email_test),
path("dashinfo/", views.dashboard_info),

View File

@@ -1,26 +1,31 @@
import os
import pprint
import re
from django.conf import settings
from django.shortcuts import get_object_or_404
from logs.models import AuditLog
from rest_framework import status
from rest_framework.decorators import api_view, permission_classes
from rest_framework.exceptions import ParseError
from rest_framework.exceptions import ParseError, PermissionDenied
from rest_framework.parsers import FileUploadParser
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from agents.permissions import MeshPerms
from tacticalrmm.utils import notify_error
from tacticalrmm.permissions import (
_has_perm_on_client,
_has_perm_on_agent,
_has_perm_on_site,
)
from .models import CodeSignToken, CoreSettings, CustomField, GlobalKVStore, URLAction
from .permissions import (
CodeSignPerms,
ViewCoreSettingsPerms,
EditCoreSettingsPerms,
CoreSettingsPerms,
ServerMaintPerms,
URLActionPerms,
CustomFieldPerms,
)
from .serializers import (
CodeSignTokenSerializer,
@@ -32,7 +37,7 @@ from .serializers import (
class UploadMeshAgent(APIView):
permission_classes = [IsAuthenticated, MeshPerms]
permission_classes = [IsAuthenticated, CoreSettingsPerms]
parser_class = (FileUploadParser,)
def put(self, request, format=None):
@@ -48,25 +53,25 @@ class UploadMeshAgent(APIView):
for chunk in f.chunks():
j.write(chunk)
return Response(status=status.HTTP_201_CREATED)
return Response(
"Mesh Agent uploaded successfully", status=status.HTTP_201_CREATED
)
@api_view()
@permission_classes([IsAuthenticated, ViewCoreSettingsPerms])
def get_core_settings(request):
settings = CoreSettings.objects.first()
return Response(CoreSettingsSerializer(settings).data)
class GetEditCoreSettings(APIView):
permission_classes = [IsAuthenticated, CoreSettingsPerms]
def get(self, request):
settings = CoreSettings.objects.first()
return Response(CoreSettingsSerializer(settings).data)
@api_view(["PATCH"])
@permission_classes([IsAuthenticated, EditCoreSettingsPerms])
def edit_settings(request):
coresettings = CoreSettings.objects.first()
serializer = CoreSettingsSerializer(instance=coresettings, data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
def put(self, request):
coresettings = CoreSettings.objects.first()
serializer = CoreSettingsSerializer(instance=coresettings, data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response("ok")
return Response("ok")
@api_view()
@@ -93,12 +98,13 @@ def dashboard_info(request):
"client_tree_splitter": request.user.client_tree_splitter,
"loading_bar_color": request.user.loading_bar_color,
"clear_search_when_switching": request.user.clear_search_when_switching,
"hosted": hasattr(settings, "HOSTED") and settings.HOSTED,
"hosted": getattr(settings, "HOSTED", False),
}
)
@api_view()
@api_view(["POST"])
@permission_classes([IsAuthenticated, CoreSettingsPerms])
def email_test(request):
core = CoreSettings.objects.first()
r = core.send_mail(
@@ -167,10 +173,13 @@ def server_maintenance(request):
class GetAddCustomFields(APIView):
permission_classes = [IsAuthenticated, EditCoreSettingsPerms]
permission_classes = [IsAuthenticated, CustomFieldPerms]
def get(self, request):
fields = CustomField.objects.all()
if "model" in request.query_params.keys():
fields = CustomField.objects.filter(model=request.query_params["model"])
else:
fields = CustomField.objects.all()
return Response(CustomFieldSerializer(fields, many=True).data)
def patch(self, request):
@@ -189,7 +198,7 @@ class GetAddCustomFields(APIView):
class GetUpdateDeleteCustomFields(APIView):
permission_classes = [IsAuthenticated, EditCoreSettingsPerms]
permission_classes = [IsAuthenticated, CustomFieldPerms]
def get(self, request, pk):
custom_field = get_object_or_404(CustomField, pk=pk)
@@ -272,13 +281,15 @@ class CodeSign(APIView):
if t is None or t == "":
return notify_error(err)
pks: list[int] = list(Agent.objects.only("pk").values_list("pk", flat=True))
force_code_sign.delay(pks=pks)
agent_ids: list[str] = list(
Agent.objects.only("pk", "agent_id").values_list("agent_id", flat=True)
)
force_code_sign.delay(agent_ids=agent_ids)
return Response("Agents will be code signed shortly")
class GetAddKeyStore(APIView):
permission_classes = [IsAuthenticated, EditCoreSettingsPerms]
permission_classes = [IsAuthenticated, CoreSettingsPerms]
def get(self, request):
keys = GlobalKVStore.objects.all()
@@ -293,7 +304,7 @@ class GetAddKeyStore(APIView):
class UpdateDeleteKeyStore(APIView):
permission_classes = [IsAuthenticated, EditCoreSettingsPerms]
permission_classes = [IsAuthenticated, CoreSettingsPerms]
def put(self, request, pk):
key = get_object_or_404(GlobalKVStore, pk=pk)
@@ -311,6 +322,8 @@ class UpdateDeleteKeyStore(APIView):
class GetAddURLAction(APIView):
permission_classes = [IsAuthenticated, CoreSettingsPerms]
def get(self, request):
actions = URLAction.objects.all()
return Response(URLActionSerializer(actions, many=True).data)
@@ -324,6 +337,8 @@ class GetAddURLAction(APIView):
class UpdateDeleteURLAction(APIView):
permission_classes = [IsAuthenticated, CoreSettingsPerms]
def put(self, request, pk):
action = get_object_or_404(URLAction, pk=pk)
@@ -342,6 +357,8 @@ class UpdateDeleteURLAction(APIView):
class RunURLAction(APIView):
permission_classes = [IsAuthenticated, URLActionPerms]
def patch(self, request):
from requests.utils import requote_uri
@@ -349,11 +366,20 @@ class RunURLAction(APIView):
from clients.models import Client, Site
from tacticalrmm.utils import replace_db_values
if "agent" in request.data.keys():
instance = get_object_or_404(Agent, pk=request.data["agent"])
if "agent_id" in request.data.keys():
if not _has_perm_on_agent(request.user, request.data["agent_id"]):
raise PermissionDenied()
instance = get_object_or_404(Agent, agent_id=request.data["agent_id"])
elif "site" in request.data.keys():
if not _has_perm_on_site(request.user, request.data["site"]):
raise PermissionDenied()
instance = get_object_or_404(Site, pk=request.data["site"])
elif "client" in request.data.keys():
if not _has_perm_on_client(request.user, request.data["client"]):
raise PermissionDenied()
instance = get_object_or_404(Client, pk=request.data["client"])
else:
return notify_error("received an incorrect request")
@@ -369,12 +395,20 @@ class RunURLAction(APIView):
url_pattern = re.sub("\\{\\{" + string + "\\}\\}", str(value), url_pattern)
AuditLog.audit_url_action(
username=request.user.username,
urlaction=action,
instance=instance,
debug_info={"ip": request._client_ip},
)
return Response(requote_uri(url_pattern))
class TwilioSMSTest(APIView):
def get(self, request):
from twilio.rest import Client as TwClient
permission_classes = [IsAuthenticated, CoreSettingsPerms]
def post(self, request):
core = CoreSettings.objects.first()
if not core.sms_is_configured:
@@ -382,14 +416,9 @@ class TwilioSMSTest(APIView):
"All fields are required, including at least 1 recipient"
)
try:
tw_client = TwClient(core.twilio_account_sid, core.twilio_auth_token)
tw_client.messages.create(
body="TacticalRMM Test SMS",
to=core.sms_alert_recipients[0],
from_=core.twilio_number,
)
except Exception as e:
return notify_error(pprint.pformat(e))
r = core.send_sms("TacticalRMM Test SMS", test=True)
return Response("SMS Test OK!")
if not isinstance(r, bool) and isinstance(r, str):
return notify_error(r)
return Response("SMS Test sent successfully!")

View File

@@ -0,0 +1,23 @@
# Generated by Django 3.2.6 on 2021-09-05 16:06
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('logs', '0017_auto_20210731_1707'),
]
operations = [
migrations.AlterField(
model_name='auditlog',
name='action',
field=models.CharField(choices=[('login', 'User Login'), ('failed_login', 'Failed User Login'), ('delete', 'Delete Object'), ('modify', 'Modify Object'), ('add', 'Add Object'), ('view', 'View Object'), ('check_run', 'Check Run'), ('task_run', 'Task Run'), ('agent_install', 'Agent Install'), ('remote_session', 'Remote Session'), ('execute_script', 'Execute Script'), ('execute_command', 'Execute Command'), ('bulk_action', 'Bulk Action'), ('url_action', 'URL Action')], max_length=100),
),
migrations.AlterField(
model_name='auditlog',
name='object_type',
field=models.CharField(choices=[('user', 'User'), ('script', 'Script'), ('agent', 'Agent'), ('policy', 'Policy'), ('winupdatepolicy', 'Patch Policy'), ('client', 'Client'), ('site', 'Site'), ('check', 'Check'), ('automatedtask', 'Automated Task'), ('coresettings', 'Core Settings'), ('bulk', 'Bulk'), ('alerttemplate', 'Alert Template'), ('role', 'Role'), ('urlaction', 'URL Action'), ('keystore', 'Global Key Store'), ('customfield', 'Custom Field')], max_length=100),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.6 on 2021-09-17 19:54
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("logs", "0018_auto_20210905_1606"),
]
operations = [
migrations.AlterField(
model_name="auditlog",
name="username",
field=models.CharField(max_length=255),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.6 on 2021-10-14 17:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('logs', '0019_alter_auditlog_username'),
]
operations = [
migrations.AlterField(
model_name='auditlog',
name='agent_id',
field=models.CharField(blank=True, max_length=50, null=True),
),
]

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 = [
('logs', '0020_alter_auditlog_agent_id'),
]
operations = [
migrations.AlterField(
model_name='auditlog',
name='agent_id',
field=models.CharField(blank=True, max_length=255, null=True),
),
]

View File

@@ -0,0 +1,26 @@
# Generated by Django 3.2.6 on 2021-11-05 01:58
from django.db import migrations
from django.core.exceptions import ObjectDoesNotExist
def update_agent_field(apps, schema_editor):
AuditLog = apps.get_model("logs", "AuditLog")
Agent = apps.get_model("agents", "Agent")
for log in AuditLog.objects.exclude(agent_id=None):
try:
log.agent_id = Agent.objects.get(pk=log.agent_id).agent_id
log.save()
except (ObjectDoesNotExist, ValueError):
pass
class Migration(migrations.Migration):
dependencies = [
('logs', '0021_alter_auditlog_agent_id'),
]
operations = [
migrations.RunPython(update_agent_field, migrations.RunPython.noop),
]

View File

@@ -3,6 +3,7 @@ from abc import abstractmethod
from django.db import models
from tacticalrmm.middleware import get_debug_info, get_username
from tacticalrmm.models import PermissionQuerySet
def get_debug_level():
@@ -36,6 +37,7 @@ AUDIT_ACTION_TYPE_CHOICES = [
("execute_script", "Execute Script"),
("execute_command", "Execute Command"),
("bulk_action", "Bulk Action"),
("url_action", "URL Action"),
]
AUDIT_OBJECT_TYPE_CHOICES = [
@@ -52,6 +54,9 @@ AUDIT_OBJECT_TYPE_CHOICES = [
("bulk", "Bulk"),
("alerttemplate", "Alert Template"),
("role", "Role"),
("urlaction", "URL Action"),
("keystore", "Global Key Store"),
("customfield", "Custom Field"),
]
STATUS_CHOICES = [
@@ -61,9 +66,9 @@ STATUS_CHOICES = [
class AuditLog(models.Model):
username = models.CharField(max_length=100)
username = models.CharField(max_length=255)
agent = models.CharField(max_length=255, null=True, blank=True)
agent_id = models.PositiveIntegerField(blank=True, null=True)
agent_id = models.CharField(max_length=255, blank=True, null=True)
entry_time = models.DateTimeField(auto_now_add=True)
action = models.CharField(max_length=100, choices=AUDIT_ACTION_TYPE_CHOICES)
object_type = models.CharField(max_length=100, choices=AUDIT_OBJECT_TYPE_CHOICES)
@@ -90,7 +95,7 @@ class AuditLog(models.Model):
AuditLog.objects.create(
username=username,
agent=agent.hostname,
agent_id=agent.id,
agent_id=agent.agent_id,
object_type="agent",
action="remote_session",
message=f"{username} used Mesh Central to initiate a remote session to {agent.hostname}.",
@@ -102,6 +107,7 @@ class AuditLog(models.Model):
AuditLog.objects.create(
username=username,
agent=agent.hostname,
agent_id=agent.agent_id,
object_type="agent",
action="execute_command",
message=f"{username} issued {shell} command on {agent.hostname}.",
@@ -116,7 +122,8 @@ class AuditLog(models.Model):
AuditLog.objects.create(
username=username,
object_type=object_type,
agent_id=before["id"] if object_type == "agent" else None,
agent=before["hostname"] if object_type == "agent" else None,
agent_id=before["agent_id"] if object_type == "agent" else None,
action="modify",
message=f"{username} modified {object_type} {name}",
before_value=before,
@@ -129,7 +136,8 @@ class AuditLog(models.Model):
AuditLog.objects.create(
username=username,
object_type=object_type,
agent=after["id"] if object_type == "agent" else None,
agent=after["hostname"] if object_type == "agent" else None,
agent_id=after["agent_id"] if object_type == "agent" else None,
action="add",
message=f"{username} added {object_type} {name}",
after_value=after,
@@ -141,7 +149,8 @@ class AuditLog(models.Model):
AuditLog.objects.create(
username=username,
object_type=object_type,
agent=before["id"] if object_type == "agent" else None,
agent=before["hostname"] if object_type == "agent" else None,
agent_id=before["agent_id"] if object_type == "agent" else None,
action="delete",
message=f"{username} deleted {object_type} {name}",
before_value=before,
@@ -152,7 +161,7 @@ class AuditLog(models.Model):
def audit_script_run(username, agent, script, debug_info={}):
AuditLog.objects.create(
agent=agent.hostname,
agent_id=agent.id,
agent_id=agent.agent_id,
username=username,
object_type="agent",
action="execute_script",
@@ -190,6 +199,21 @@ class AuditLog(models.Model):
debug_info=debug_info,
)
@staticmethod
def audit_url_action(username, urlaction, instance, debug_info={}):
name = instance.hostname if hasattr(instance, "hostname") else instance.name
classname = type(instance).__name__
AuditLog.objects.create(
username=username,
agent=instance.hostname if classname == "Agent" else None,
agent_id=instance.agent_id if classname == "Agent" else None,
object_type=classname.lower(),
action="url_action",
message=f"{username} ran url action: {urlaction.pattern} on {classname}: {name}",
debug_info=debug_info,
)
@staticmethod
def audit_bulk_action(username, action, affected, debug_info={}):
from agents.models import Agent
@@ -208,7 +232,7 @@ class AuditLog(models.Model):
site = Site.objects.get(pk=affected["site"])
target = f"on all agents within site: {site.client.name}\\{site.name}"
elif affected["target"] == "agents":
agents = Agent.objects.filter(pk__in=affected["agents"]).values_list(
agents = Agent.objects.filter(agent_id__in=affected["agents"]).values_list(
"hostname", flat=True
)
target = "on multiple agents"
@@ -247,6 +271,8 @@ LOG_TYPE_CHOICES = [
class DebugLog(models.Model):
objects = PermissionQuerySet.as_manager()
entry_time = models.DateTimeField(auto_now_add=True)
agent = models.ForeignKey(
"agents.Agent",
@@ -271,25 +297,34 @@ class DebugLog(models.Model):
log_type="system_issues",
):
if get_debug_level() in ["info"]:
cls(log_level="info", agent=agent, log_type=log_type, message=message)
cls.objects.create(
log_level="info", agent=agent, log_type=log_type, message=message
)
@classmethod
def warning(cls, message, agent=None, log_type="system_issues"):
if get_debug_level() in ["info", "warning"]:
cls(log_level="warning", agent=agent, log_type=log_type, message=message)
cls.objects.create(
log_level="warning", agent=agent, log_type=log_type, message=message
)
@classmethod
def error(cls, message, agent=None, log_type="system_issues"):
if get_debug_level() in ["info", "warning", "error"]:
cls(log_level="error", agent=agent, log_type=log_type, message=message)
cls.objects.create(
log_level="error", agent=agent, log_type=log_type, message=message
)
@classmethod
def critical(cls, message, agent=None, log_type="system_issues"):
if get_debug_level() in ["info", "warning", "error", "critical"]:
cls(log_level="critical", agent=agent, log_type=log_type, message=message)
cls.objects.create(
log_level="critical", agent=agent, log_type=log_type, message=message
)
class PendingAction(models.Model):
objects = PermissionQuerySet.as_manager()
agent = models.ForeignKey(
"agents.Agent",
@@ -350,9 +385,9 @@ class BaseAuditModel(models.Model):
abstract = True
# create audit fields
created_by = models.CharField(max_length=100, null=True, blank=True)
created_by = models.CharField(max_length=255, null=True, blank=True)
created_time = models.DateTimeField(auto_now_add=True, null=True, blank=True)
modified_by = models.CharField(max_length=100, null=True, blank=True)
modified_by = models.CharField(max_length=255, null=True, blank=True)
modified_time = models.DateTimeField(auto_now=True, null=True, blank=True)
@abstractmethod

View File

@@ -1,6 +1,6 @@
from rest_framework import permissions
from tacticalrmm.permissions import _has_perm
from tacticalrmm.permissions import _has_perm, _has_perm_on_agent
class AuditLogPerms(permissions.BasePermission):
@@ -8,12 +8,17 @@ class AuditLogPerms(permissions.BasePermission):
return _has_perm(r, "can_view_auditlogs")
class ManagePendingActionPerms(permissions.BasePermission):
class PendingActionPerms(permissions.BasePermission):
def has_permission(self, r, view):
if r.method == "PATCH":
return True
return _has_perm(r, "can_manage_pendingactions")
if r.method == "GET":
if "agent_id" in view.kwargs.keys():
return _has_perm(r, "can_list_pendingactions") and _has_perm_on_agent(
r.user, view.kwargs["agent_id"]
)
else:
return _has_perm(r, "can_list_pendingactions")
else:
return _has_perm(r, "can_manage_pendingactions")
class DebugLogPerms(permissions.BasePermission):

View File

@@ -1,6 +1,5 @@
from rest_framework import serializers
from tacticalrmm.utils import get_default_timezone
from .models import AuditLog, DebugLog, PendingAction
@@ -14,8 +13,8 @@ class AuditLogSerializer(serializers.ModelSerializer):
fields = "__all__"
def get_entry_time(self, log):
timezone = get_default_timezone()
return log.entry_time.astimezone(timezone).strftime("%m %d %Y %H:%M:%S")
tz = self.context["default_tz"]
return log.entry_time.astimezone(tz).strftime("%m %d %Y %H:%M:%S")
class PendingActionSerializer(serializers.ModelSerializer):
@@ -40,5 +39,5 @@ class DebugLogSerializer(serializers.ModelSerializer):
fields = "__all__"
def get_entry_time(self, log):
timezone = get_default_timezone()
return log.entry_time.astimezone(timezone).strftime("%m %d %Y %H:%M:%S")
tz = self.context["default_tz"]
return log.entry_time.astimezone(tz).strftime("%m %d %Y %H:%M:%S")

View File

@@ -5,7 +5,7 @@ from django.utils import timezone as djangotime
from model_bakery import baker, seq
from tacticalrmm.test import TacticalTestCase
from logs.models import PendingAction
base_url = "/logs"
class TestAuditViews(TacticalTestCase):
@@ -26,14 +26,14 @@ class TestAuditViews(TacticalTestCase):
"logs.agent_logs",
username="jim",
agent="AgentHostname1",
agent_id=agent1.id,
agent_id=agent1.agent_id,
_quantity=15,
)
baker.make_recipe(
"logs.agent_logs",
username="jim",
agent="AgentHostname2",
agent_id=agent2.id,
agent_id=agent2.agent_id,
_quantity=8,
)
@@ -42,14 +42,14 @@ class TestAuditViews(TacticalTestCase):
"logs.agent_logs",
username="james",
agent="AgentHostname1",
agent_id=agent1.id,
agent_id=agent1.agent_id,
_quantity=7,
)
baker.make_recipe(
"logs.agent_logs",
username="james",
agent="AgentHostname2",
agent_id=agent2.id,
agent_id=agent2.agent_id,
_quantity=10,
)
@@ -57,7 +57,7 @@ class TestAuditViews(TacticalTestCase):
baker.make_recipe(
"logs.agent_logs",
agent=seq("AgentHostname"),
agent_id=seq(agent1.id),
agent_id=seq(agent1.agent_id),
_quantity=5,
)
@@ -85,7 +85,7 @@ class TestAuditViews(TacticalTestCase):
return {"site": site, "agents": [agent0, agent1, agent2]}
def test_get_audit_logs(self):
url = "/logs/auditlogs/"
url = "/logs/audit/"
# create data
data = self.create_audit_records()
@@ -96,14 +96,14 @@ class TestAuditViews(TacticalTestCase):
{
"filter": {
"timeFilter": 45,
"agentFilter": [data["agents"][2].id],
"agentFilter": [data["agents"][2].agent_id],
},
"count": 19,
"count": 18,
},
{
"filter": {
"userFilter": ["jim"],
"agentFilter": [data["agents"][1].id],
"agentFilter": [data["agents"][1].agent_id],
},
"count": 15,
},
@@ -111,7 +111,7 @@ class TestAuditViews(TacticalTestCase):
"filter": {
"timeFilter": 180,
"userFilter": ["james"],
"agentFilter": [data["agents"][1].id],
"agentFilter": [data["agents"][1].agent_id],
},
"count": 7,
},
@@ -122,8 +122,8 @@ class TestAuditViews(TacticalTestCase):
"timeFilter": 35,
"userFilter": ["james", "jim"],
"agentFilter": [
data["agents"][1].id,
data["agents"][2].id,
data["agents"][1].agent_id,
data["agents"][2].agent_id,
],
},
"count": 40,
@@ -133,7 +133,7 @@ class TestAuditViews(TacticalTestCase):
{"filter": {"actionFilter": ["login"]}, "count": 12},
{
"filter": {"clientFilter": [data["site"].client.id]},
"count": 23,
"count": 22,
},
]
@@ -180,36 +180,15 @@ class TestAuditViews(TacticalTestCase):
_quantity=14,
)
data = {"showCompleted": False}
r = self.client.patch(url, data, format="json")
r = self.client.get(url, format="json")
self.assertEqual(r.status_code, 200)
self.assertEqual(len(r.data["actions"]), 12) # type: ignore
self.assertEqual(r.data["completed_count"], 14) # type: ignore
self.assertEqual(r.data["total"], 26) # type: ignore
self.assertEqual(len(r.data), 26) # type: ignore
PendingAction.objects.filter(action_type="chocoinstall").update(
status="completed"
)
data = {"showCompleted": True}
r = self.client.patch(url, data, format="json")
self.assertEqual(r.status_code, 200)
self.assertEqual(len(r.data["actions"]), 26) # type: ignore
self.assertEqual(r.data["completed_count"], 26) # type: ignore
self.assertEqual(r.data["total"], 26) # type: ignore
data = {"showCompleted": True, "agentPK": agent1.pk}
r = self.client.patch(url, data, format="json")
self.assertEqual(r.status_code, 200)
self.assertEqual(len(r.data["actions"]), 12) # type: ignore
self.assertEqual(r.data["completed_count"], 12) # type: ignore
self.assertEqual(r.data["total"], 12) # type: ignore
self.check_not_authenticated("patch", url)
self.check_not_authenticated("get", url)
@patch("agents.models.Agent.nats_cmd")
def test_cancel_pending_action(self, nats_cmd):
nats_cmd.return_value = "ok"
url = "/logs/pendingactions/"
agent = baker.make_recipe("agents.online_agent")
action = baker.make(
"logs.PendingAction",
@@ -221,8 +200,9 @@ class TestAuditViews(TacticalTestCase):
},
)
data = {"pk": action.pk} # type: ignore
r = self.client.delete(url, data, format="json")
url = f"{base_url}/pendingactions/{action.id}/"
r = self.client.delete(url, format="json")
self.assertEqual(r.status_code, 200)
nats_data = {
"func": "delschedtask",
@@ -231,7 +211,7 @@ class TestAuditViews(TacticalTestCase):
nats_cmd.assert_called_with(nats_data, timeout=10)
# try request again and it should 404 since pending action doesn't exist
r = self.client.delete(url, data, format="json")
r = self.client.delete(url, format="json")
self.assertEqual(r.status_code, 404)
nats_cmd.reset_mock()
@@ -246,16 +226,17 @@ class TestAuditViews(TacticalTestCase):
},
)
data = {"pk": action2.pk} # type: ignore
nats_cmd.return_value = "error deleting sched task"
r = self.client.delete(url, data, format="json")
r = self.client.delete(
f"{base_url}/pendingactions/{action2.id}/", format="json"
)
self.assertEqual(r.status_code, 400)
self.assertEqual(r.data, "error deleting sched task") # type: ignore
self.check_not_authenticated("delete", url)
def test_get_debug_log(self):
url = "/logs/debuglog/"
url = "/logs/debug/"
# create data
agent = baker.make_recipe("agents.agent")
@@ -275,13 +256,13 @@ class TestAuditViews(TacticalTestCase):
)
# test agent filter
data = {"agentFilter": agent.id}
data = {"agentFilter": agent.agent_id}
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
self.assertEqual(len(resp.data), 4) # type: ignore
# test log type filter and agent
data = {"agentFilter": agent.id, "logLevelFilter": "warning"}
data = {"agentFilter": agent.agent_id, "logLevelFilter": "warning"}
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
self.assertEqual(len(resp.data), 1) # type: ignore
@@ -294,6 +275,203 @@ class TestAuditViews(TacticalTestCase):
self.check_not_authenticated("patch", url)
def test_auditlog_permissions(self):
site = self.create_audit_records()["site"]
url = f"{base_url}/audit/"
data = {
"pagination": {
"rowsPerPage": 100,
"page": 1,
"sortBy": "entry_time",
"descending": True,
}
}
# test superuser access
self.check_authorized_superuser("patch", url, data)
user = self.create_user_with_roles([])
self.client.force_authenticate(user=user) # type: ignore
# test user without role
self.check_not_authorized("patch", url, data)
# add user to role and test
user.role.can_view_auditlogs = True
user.role.save()
response = self.check_authorized("patch", url, data)
self.assertEqual(len(response.data["audit_logs"]), 86) # type: ignore
# limit user to client if agent check
user.role.can_view_sites.set([site])
response = self.check_authorized("patch", url, data)
self.assertEqual(len(response.data["audit_logs"]), 63) # type: ignore
# limit user to client if agent check
user.role.can_view_clients.set([site.client])
response = self.check_authorized("patch", url, data)
self.assertEqual(len(response.data["audit_logs"]), 63) # type: ignore
def test_debuglog_permissions(self):
# create data
agent = baker.make_recipe("agents.agent")
agent2 = baker.make_recipe("agents.agent")
baker.make(
"logs.DebugLog",
log_level=cycle(["error", "info", "warning", "critical"]),
log_type="agent_issues",
agent=agent,
_quantity=4,
)
baker.make(
"logs.DebugLog",
log_level=cycle(["error", "info", "warning", "critical"]),
log_type="agent_issues",
agent=agent2,
_quantity=8,
)
baker.make(
"logs.DebugLog",
log_type="system_issues",
log_level=cycle(["error", "info", "warning", "critical"]),
_quantity=15,
)
url = f"{base_url}/debug/"
# test superuser access
self.check_authorized_superuser(
"patch",
url,
)
user = self.create_user_with_roles([])
self.client.force_authenticate(user=user) # type: ignore
# test user without role
self.check_not_authorized("patch", url)
# add user to role and test
user.role.can_view_debuglogs = True
user.role.save()
response = self.check_authorized("patch", url)
self.assertEqual(len(response.data), 27) # type: ignore
# limit user to site
user.role.can_view_sites.set([agent.site])
response = self.check_authorized("patch", url)
self.assertEqual(len(response.data), 19) # type: ignore
# limit user to client
user.role.can_view_sites.clear()
user.role.can_view_clients.set([agent2.site.client])
response = self.check_authorized("patch", url)
self.assertEqual(len(response.data), 23) # type: ignore
# limit user to client and site
user.role.can_view_sites.set([agent.site])
user.role.can_view_clients.set([agent2.site.client])
response = self.check_authorized("patch", url)
self.assertEqual(len(response.data), 27) # type: ignore
def test_get_pendingaction_permissions(self):
agent = baker.make_recipe("agents.agent")
unauthorized_agent = baker.make_recipe("agents.agent")
actions = baker.make("logs.PendingAction", agent=agent, _quantity=5)
unauthorized_actions = baker.make(
"logs.PendingAction", agent=unauthorized_agent, _quantity=7
)
# test super user access
self.check_authorized_superuser("get", f"{base_url}/pendingactions/")
self.check_authorized_superuser(
"get", f"/agents/{agent.agent_id}/pendingactions/"
)
self.check_authorized_superuser(
"get", f"/agents/{unauthorized_agent.agent_id}/pendingactions/"
)
user = self.create_user_with_roles([])
self.client.force_authenticate(user=user) # type: ignore
self.check_not_authorized("get", f"{base_url}/pendingactions/")
self.check_not_authorized("get", f"/agents/{agent.agent_id}/pendingactions/")
self.check_not_authorized(
"get", f"/agents/{unauthorized_agent.agent_id}/pendingactions/"
)
# add list software role to user
user.role.can_list_pendingactions = True
user.role.save()
r = self.check_authorized("get", f"{base_url}/pendingactions/")
self.assertEqual(len(r.data), 12) # type: ignore
r = self.check_authorized("get", f"/agents/{agent.agent_id}/pendingactions/")
self.assertEqual(len(r.data), 5) # type: ignore
r = self.check_authorized(
"get", f"/agents/{unauthorized_agent.agent_id}/pendingactions/"
)
self.assertEqual(len(r.data), 7) # type: ignore
# test limiting to client
user.role.can_view_clients.set([agent.client])
self.check_not_authorized(
"get", f"/agents/{unauthorized_agent.agent_id}/pendingactions/"
)
self.check_authorized("get", f"/agents/{agent.agent_id}/pendingactions/")
# make sure queryset is limited too
r = self.client.get(f"{base_url}/pendingactions/")
self.assertEqual(len(r.data), 5) # type: ignore
@patch("agents.models.Agent.nats_cmd", return_value="ok")
@patch("logs.models.PendingAction.delete")
def test_delete_pendingaction_permissions(self, delete, nats_cmd):
agent = baker.make_recipe("agents.agent")
unauthorized_agent = baker.make_recipe("agents.agent")
action = baker.make(
"logs.PendingAction", agent=agent, details={"taskname": "Task"}
)
unauthorized_action = baker.make(
"logs.PendingAction", agent=unauthorized_agent, details={"taskname": "Task"}
)
url = f"{base_url}/pendingactions/{action.id}/"
unauthorized_url = f"{base_url}/pendingactions/{unauthorized_action.id}/"
# test superuser access
self.check_authorized_superuser("delete", url)
self.check_authorized_superuser("delete", unauthorized_url)
user = self.create_user_with_roles([])
self.client.force_authenticate(user=user) # type: ignore
# test user without role
self.check_not_authorized("delete", url)
self.check_not_authorized("delete", unauthorized_url)
# add user to role and test
user.role.can_manage_pendingactions = True
user.role.save()
self.check_authorized("delete", url)
self.check_authorized("delete", unauthorized_url)
# limit user to site
user.role.can_view_sites.set([agent.site])
self.check_authorized("delete", url)
self.check_not_authorized("delete", unauthorized_url)
class TestLogTasks(TacticalTestCase):
def test_prune_debug_log(self):

View File

@@ -4,6 +4,7 @@ from . import views
urlpatterns = [
path("pendingactions/", views.PendingActions.as_view()),
path("auditlogs/", views.GetAuditLogs.as_view()),
path("debuglog/", views.GetDebugLog.as_view()),
path("pendingactions/<int:pk>/", views.PendingActions.as_view()),
path("audit/", views.GetAuditLogs.as_view()),
path("debug/", views.GetDebugLog.as_view()),
]

View File

@@ -1,22 +1,20 @@
import asyncio
from datetime import datetime as dt
from accounts.models import User
from accounts.serializers import UserSerializer
from agents.models import Agent
from agents.serializers import AgentHostnameSerializer
from django.core.paginator import Paginator
from django.db.models import Q
from django.shortcuts import get_object_or_404
from django.utils import timezone as djangotime
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from tacticalrmm.utils import notify_error
from rest_framework.exceptions import PermissionDenied
from tacticalrmm.utils import notify_error, get_default_timezone, AGENT_DEFER
from tacticalrmm.permissions import _audit_log_filter, _has_perm_on_agent
from .models import AuditLog, PendingAction, DebugLog
from .permissions import AuditLogPerms, DebugLogPerms, ManagePendingActionPerms
from agents.models import Agent
from .permissions import AuditLogPerms, DebugLogPerms, PendingActionPerms
from .serializers import AuditLogSerializer, DebugLogSerializer, PendingActionSerializer
@@ -46,13 +44,11 @@ class GetAuditLogs(APIView):
agentFilter = Q(agent_id__in=request.data["agentFilter"])
elif "clientFilter" in request.data:
clients = Client.objects.filter(
pk__in=request.data["clientFilter"]
).values_list("id")
agents = Agent.objects.filter(site__client_id__in=clients).values_list(
"hostname"
clients = Client.objects.filter(pk__in=request.data["clientFilter"])
agents = Agent.objects.filter(site__client__in=clients).values_list(
"agent_id"
)
clientFilter = Q(agent__in=agents)
clientFilter = Q(agent_id__in=agents)
if "userFilter" in request.data:
userFilter = Q(username__in=request.data["userFilter"])
@@ -76,14 +72,16 @@ class GetAuditLogs(APIView):
.filter(actionFilter)
.filter(objectFilter)
.filter(timeFilter)
.filter(_audit_log_filter(request.user))
).order_by(order_by)
paginator = Paginator(audit_logs, pagination["rowsPerPage"])
ctx = {"default_tz": get_default_timezone()}
return Response(
{
"audit_logs": AuditLogSerializer(
paginator.get_page(pagination["page"]), many=True
paginator.get_page(pagination["page"]), many=True, context=ctx
).data,
"total": paginator.count,
}
@@ -91,37 +89,29 @@ class GetAuditLogs(APIView):
class PendingActions(APIView):
permission_classes = [IsAuthenticated, ManagePendingActionPerms]
permission_classes = [IsAuthenticated, PendingActionPerms]
def patch(self, request):
status_filter = "completed" if request.data["showCompleted"] else "pending"
if "agentPK" in request.data.keys():
actions = PendingAction.objects.filter(
agent__pk=request.data["agentPK"], status=status_filter
def get(self, request, agent_id=None):
if agent_id:
agent = get_object_or_404(
Agent.objects.defer(*AGENT_DEFER), agent_id=agent_id
)
total = PendingAction.objects.filter(
agent__pk=request.data["agentPK"]
).count()
completed = PendingAction.objects.filter(
agent__pk=request.data["agentPK"], status="completed"
).count()
actions = PendingAction.objects.filter(agent=agent)
else:
actions = PendingAction.objects.filter(status=status_filter).select_related(
"agent"
actions = (
PendingAction.objects.select_related("agent")
.defer("agent__services", "agent__wmi_detail")
.filter_by_role(request.user) # type: ignore
)
total = PendingAction.objects.count()
completed = PendingAction.objects.filter(status="completed").count()
ret = {
"actions": PendingActionSerializer(actions, many=True).data,
"completed_count": completed,
"total": total,
}
return Response(ret)
return Response(PendingActionSerializer(actions, many=True).data)
def delete(self, request, pk):
action = get_object_or_404(PendingAction, pk=pk)
if not _has_perm_on_agent(request.user, action.agent.agent_id):
raise PermissionDenied()
def delete(self, request):
action = get_object_or_404(PendingAction, pk=request.data["pk"])
nats_data = {
"func": "delschedtask",
"schedtaskpayload": {"name": action.details["taskname"]},
@@ -138,7 +128,6 @@ class GetDebugLog(APIView):
permission_classes = [IsAuthenticated, DebugLogPerms]
def patch(self, request):
agentFilter = Q()
logTypeFilter = Q()
logLevelFilter = Q()
@@ -150,12 +139,18 @@ class GetDebugLog(APIView):
logLevelFilter = Q(log_level=request.data["logLevelFilter"])
if "agentFilter" in request.data:
agentFilter = Q(agent=request.data["agentFilter"])
agentFilter = Q(agent__agent_id=request.data["agentFilter"])
debug_logs = (
DebugLog.objects.filter(logLevelFilter)
DebugLog.objects.prefetch_related("agent")
.filter_by_role(request.user)
.filter(logLevelFilter)
.filter(agentFilter)
.filter(logTypeFilter)
)
return Response(DebugLogSerializer(debug_logs, many=True).data)
ctx = {"default_tz": get_default_timezone()}
ret = DebugLogSerializer(
debug_logs.order_by("-entry_time")[0:1000], many=True, context=ctx
).data
return Response(ret)

View File

@@ -8,4 +8,3 @@ Pygments
isort
mypy
types-pytz
types-pytz

View File

@@ -1,3 +1,2 @@
coverage
coveralls
coveralls==3.2.0
model_bakery

View File

@@ -1,37 +1,38 @@
asgiref==3.4.1
asyncio-nats-client==0.11.4
celery==5.1.2
certifi==2021.5.30
cffi==1.14.6
asyncio-nats-client==0.11.5
celery==5.2.1
certifi==2021.10.8
cffi==1.15.0
channels==3.0.4
channels_redis==3.3.0
channels_redis==3.3.1
chardet==4.0.0
cryptography==3.4.8
cryptography==36.0.1
daphne==3.0.2
Django==3.2.6
django-cors-headers==3.8.0
django-ipware==3.0.2
Django==3.2.10
django-cors-headers==3.10.1
django-ipware==4.0.2
django-rest-knox==4.1.0
djangorestframework==3.12.4
djangorestframework==3.13.1
future==0.18.2
loguru==0.5.3
msgpack==1.0.2
packaging==21.0
psycopg2-binary==2.9.1
pycparser==2.20
pycryptodome==3.10.1
msgpack==1.0.3
packaging==21.3
psycopg2-binary==2.9.2
pycparser==2.21
pycryptodome==3.12.0
pyotp==2.6.0
pyparsing==2.4.7
pytz==2021.1
pyparsing==3.0.6
pytz==2021.3
qrcode==6.1
redis==3.5.3
redis==4.0.2
requests==2.26.0
six==1.16.0
sqlparse==0.4.1
twilio==6.63.1
urllib3==1.26.6
uWSGI==2.0.19.1
sqlparse==0.4.2
twilio==7.4.0
urllib3==1.26.7
uWSGI==2.0.20
validators==0.18.2
vine==5.0.0
websockets==9.1
zipp==3.5.0
zipp==3.6.0
drf_spectacular==0.21.0

View File

@@ -9,6 +9,16 @@
"category": "TRMM (Win):Browsers",
"default_timeout": "300"
},
{
"guid": "720edbb7-8faf-4a77-9283-29935e8880d0",
"filename": "Win_Printer_ClearandRestart.bat",
"submittedBy": "https://github.com/wh1te909",
"name": "Printers - Clear all print jobs",
"description": "This script will stop the spooler, delete all pending print jobs and restart the spooler",
"shell": "cmd",
"category": "TRMM (Win):Printing",
"default_timeout": "300"
},
{
"guid": "3ff6a386-11d1-4f9d-8cca-1b0563bb6443",
"filename": "Win_Google_Chrome_Clear_Cache.ps1",
@@ -19,6 +29,16 @@
"category": "TRMM (Win):Browsers",
"default_timeout": "300"
},
{
"guid": "d3c74105-d1e5-40d8-94ff-b4d6b216fe0f",
"filename": "Win_Chocolatey_List_Installed.bat",
"submittedBy": "https://github.com/silversword411",
"name": "Chocolatey - List Installed apps",
"description": "Lists apps locally installed by chocolatey",
"shell": "cmd",
"category": "TRMM (Win):3rd Party Software>Chocolatey",
"default_timeout": "90"
},
{
"guid": "be1de837-f677-4ac5-aa0c-37a0fc9991fc",
"filename": "Win_Install_Adobe_Reader.ps1",
@@ -30,18 +50,37 @@
"default_timeout": "300"
},
{
"guid": "2ee134d5-76aa-4160-b334-a1efbc62079f",
"filename": "Win_Install_Duplicati.ps1",
"submittedBy": "https://github.com/Omnicef",
"name": "Duplicati - Install",
"description": "This script installs Duplicati 2.0.5.1 as a service.",
"shell": "powershell",
"guid": "7b1d90a1-3eda-48ab-9c49-20e714c9e82a",
"filename": "Win_Duplicati_Install.bat",
"submittedBy": "https://github.com/dinger1986",
"name": "Duplicati - Install 2.0.6.100 to work with Community Check Status",
"description": "This script installs Duplicati 2.0.6.100 as a service and creates status files to be used with commuity check",
"shell": "cmd",
"category": "TRMM (Win):3rd Party Software",
"default_timeout": "300"
},
{
"guid": "7c14beb4-d1c3-41aa-8e70-92a267d6e080",
"filename": "Win_Duplicati_Status.ps1",
"submittedBy": "https://github.com/dinger1986",
"name": "Duplicati - Check Status",
"description": "Checks Duplicati Backup is running properly over the last 24 hours",
"shell": "powershell",
"category": "TRMM (Win):3rd Party Software>Monitoring"
},
{
"guid": "5a60c13b-1882-4a92-bdfb-6dd1f6a11dd14",
"filename": "Win_Windows_Update_RevertToDefault.ps1",
"submittedBy": "https://github.com/silversword411",
"name": "Windows Update - Re-enable Microsoft managed Windows Update",
"description": "TRMM agent will set registry key to disable Windows Auto Updates. This will re-enable Windows standard update settings",
"shell": "powershell",
"category": "TRMM (Win):Updates",
"default_timeout": "90"
},
{
"guid": "81cc5bcb-01bf-4b0c-89b9-0ac0f3fe0c04",
"filename": "Win_Reset_Windows_Update.ps1",
"filename": "Win_Windows_Update_Reset.ps1",
"submittedBy": "https://github.com/Omnicef",
"name": "Windows Update - Reset",
"description": "This script will reset all of the Windows Updates components to DEFAULT SETTINGS.",
@@ -54,7 +93,7 @@
"filename": "Win_Start_Cleanup.ps1",
"submittedBy": "https://github.com/Omnicef",
"name": "Disk - Cleanup C: drive",
"description": "Cleans the C: drive's Window Temperary files, Windows SoftwareDistribution folder, the local users Temperary folder, IIS logs (if applicable) and empties the recycling bin. All deleted files will go into a log transcript in $env:TEMP. By default this script leaves files that are newer than 7 days old however this variable can be edited.",
"description": "Cleans the C: drive's Window Temporary files, Windows SoftwareDistribution folder, the local users Temperary folder, IIS logs (if applicable) and empties the recycling bin. All deleted files will go into a log transcript in $env:TEMP. By default this script leaves files that are newer than 7 days old however this variable can be edited.",
"shell": "powershell",
"category": "TRMM (Win):Maintenance",
"default_timeout": "25000"
@@ -91,17 +130,19 @@
"guid": "9d34f482-1f0c-4b2f-b65f-a9cf3c13ef5f",
"filename": "Win_TRMM_Rename_Installed_App.ps1",
"submittedBy": "https://github.com/bradhawkins85",
"name": "TacticalRMM Agent Rename",
"name": "TacticalRMM - Agent Rename",
"description": "Updates the DisplayName registry entry for the Tactical RMM windows agent to your desired name. This script takes 1 required argument: the name you wish to set.",
"syntax": "<string>",
"shell": "powershell",
"category": "TRMM (Win):TacticalRMM Related"
},
{
"guid": "525ae965-1dcf-4c17-92b3-5da3cf6819f5",
"filename": "Win_Bitlocker_Encrypted_Drive_c.ps1",
"submittedBy": "https://github.com/ThatsNASt",
"name": "Bitlocker - Check C Drive for Status",
"description": "Runs a check on drive C for Bitlocker status.",
"filename": "Win_Bitlocker_Drive_Check_Status.ps1",
"submittedBy": "https://github.com/silversword411",
"name": "Bitlocker - Check Drive for Status",
"description": "Runs a check on drive for Bitlocker status. Returns 0 if Bitlocker is not enabled, 1 if Bitlocker is enabled",
"syntax": "[Drive <string>]",
"shell": "powershell",
"category": "TRMM (Win):Storage"
},
@@ -132,6 +173,15 @@
"shell": "powershell",
"category": "TRMM (Win):Storage"
},
{
"guid": "11be7136-0416-47b4-a6dd-9776fa857dca",
"filename": "Win_Storage_CheckPools.ps1",
"submittedBy": "https://github.com/wh1te909",
"name": "Storage Pools - Check Health",
"description": "Checks all storage pools for health, returns error 1 if unhealthy",
"shell": "powershell",
"category": "TRMM (Win):Monitoring"
},
{
"guid": "cfa14c28-4dfc-4d4e-95ee-a380652e058d",
"filename": "Win_Bios_Check.ps1",
@@ -173,19 +223,31 @@
"filename": "Win_Screenconnect_GetGUID.ps1",
"submittedBy": "https://github.com/silversword411",
"name": "Screenconnect - Get GUID for client",
"description": "Returns Screenconnect GUID for client - Use with Custom Fields for later use. ",
"description": "Returns Screenconnect GUID for client - Use with Custom Fields for later use.",
"args": [
"{{client.ScreenConnectService}}"
],
"shell": "powershell",
"category": "TRMM (Win):Collectors"
},
{
"guid": "bbe5645f-c8d8-4d86-bddd-c8dbea45c974",
"filename": "Win_Splashtop_Get_ID.ps1",
"submittedBy": "https://github.com/r3die",
"name": "Splashtop - Get SUUID for client",
"description": "Returns Splashtop SUUID for client - Use with Custom Fields for later use.",
"args": [
"{{agent.SplashtopSUUID}}"
],
"shell": "powershell",
"category": "TRMM (Win):Collectors"
},
{
"guid": "9cfdfe8f-82bf-4081-a59f-576d694f4649",
"filename": "Win_Teamviewer_Get_ID.ps1",
"submittedBy": "https://github.com/silversword411",
"name": "TeamViewer - Get ClientID for client",
"description": "Returns Teamviwer ClientID for client - Use with Custom Fields for later use. ",
"description": "Returns Teamviwer ClientID for client - Use with Custom Fields for later use.",
"shell": "powershell",
"category": "TRMM (Win):Collectors"
},
@@ -194,7 +256,7 @@
"filename": "Win_AnyDesk_Get_Anynet_ID.ps1",
"submittedBy": "https://github.com/meuchels",
"name": "AnyDesk - Get AnyNetID for client",
"description": "Returns AnyNetID for client - Use with Custom Fields for later use. ",
"description": "Returns AnyNetID for client - Use with Custom Fields for later use.",
"shell": "powershell",
"category": "TRMM (Win):Collectors"
},
@@ -226,23 +288,35 @@
"category": "TRMM (Win):Updates",
"default_timeout": "25000"
},
{
"guid": "4d0ba685-2259-44be-9010-8ed2fa48bf74",
"filename": "Win_Win11_Ready.ps1",
"submittedBy": "https://github.com/adamjrberry/",
"name": "Windows 11 Upgrade capable check",
"description": "Checks to see if machine is Win11 capable",
"shell": "powershell",
"category": "TRMM (Win):Updates",
"default_timeout": "3600"
},
{
"guid": "375323e5-cac6-4f35-a304-bb7cef35902d",
"filename": "Win_Disk_Status.ps1",
"filename": "Win_Disk_Volume_Status.ps1",
"submittedBy": "https://github.com/dinger1986",
"name": "Disk Hardware Health Check (using Event Viewer errors)",
"description": "Checks local disks for errors reported in event viewer within the last 24 hours",
"name": "Disk Drive Volume Health Check (using Event Viewer errors)",
"description": "Checks Drive Volumes for errors reported in event viewer within the last 24 hours",
"shell": "powershell",
"category": "TRMM (Win):Hardware"
},
{
"guid": "7c14beb4-d1c3-41aa-8e70-92a267d6e080",
"filename": "Win_Duplicati_Status.ps1",
"submittedBy": "https://github.com/dinger1986",
"name": "Duplicati - Check Status",
"description": "Checks Duplicati Backup is running properly over the last 24 hours",
"guid": "4ace28ee-98f7-4931-9ac9-0adaf1a757ed",
"filename": "Win_Software_Install_Report.ps1",
"submittedBy": "https://github.com/silversword",
"name": "Software Install - Reports new installs",
"description": "This will check for software install events in the application Event Viewer log. If a number is provided as a command parameter it will search that number of days back.",
"syntax": "[<int>]",
"shell": "powershell",
"category": "TRMM (Win):3rd Party Software"
"category": "TRMM (Win):Monitoring",
"default_timeout": "90"
},
{
"guid": "907652a5-9ec1-4759-9871-a7743f805ff2",
@@ -250,6 +324,7 @@
"submittedBy": "https://github.com/subzdev",
"name": "Software Uninstaller - list, find, and uninstall most software",
"description": "Allows listing, finding and uninstalling most software on Windows. There will be a best effort to uninstall silently if the silent uninstall string is not provided.",
"syntax": "-list <string>\n[-u <uninstall string>]\n[-u quiet <uninstall string>]",
"shell": "powershell",
"category": "TRMM (Win):3rd Party Software",
"default_timeout": "600"
@@ -260,6 +335,7 @@
"submittedBy": "https://github.com/jhtechIL/",
"name": "BitDefender Gravity Zone Install",
"description": "Installs BitDefender Gravity Zone, requires client custom field setup. See script comments for details",
"syntax": "[-log]",
"args": [
"-url {{client.bdurl}}",
"-exe {{client.bdexe}}"
@@ -268,10 +344,37 @@
"shell": "powershell",
"category": "TRMM (Win):3rd Party Software"
},
{
"guid": "bfd61545-839b-45da-8b3d-75ffc4d43272",
"filename": "Win_Sophos_EndpointProtection_Install.ps1",
"submittedBy": "https://github.com/bc24fl/",
"name": "Sophos Endpoint Protection Install",
"description": "Installs Sophos Endpoint Protection via the Sophos API. Products include Antivirus, InterceptX, MDR, Device Encryption. The script requires API credentials, Custom Fields, and Arguments passed to script. See script comments for details",
"args": [
"-ClientId {{client.SophosClientId}}",
"-ClientSecret {{client.SophosClientSecret}}",
"-TenantName {{client.SophosTenantName}}",
"-Products antivirus,intercept"
],
"default_timeout": "3600",
"shell": "powershell",
"category": "TRMM (Win):3rd Party Software"
},
{
"guid": "a9d2a6c0-8afa-4d69-8faf-f83b49c11702",
"filename": "Win_Printer_Restart_Jobs.ps1",
"submittedBy": "https://github.com/bc24fl/",
"name": "Printers - Restarts stuck printer jobs.",
"description": "Cycles through each printer and restarts any jobs that are stuck with error status.",
"shell": "powershell",
"category": "TRMM (Win):Printing",
"default_timeout": "90"
},
{
"guid": "da51111c-aff6-4d87-9d76-0608e1f67fe5",
"filename": "Win_Defender_Enable.ps1",
"submittedBy": "https://github.com/dinger1986",
"syntax": "[-NoControlledFolders]",
"name": "Defender - Enable",
"description": "Enables Windows Defender and sets preferences",
"shell": "powershell",
@@ -317,7 +420,7 @@
},
{
"guid": "a821975c-60df-4d58-8990-6cf8a55b4ee0",
"filename": "Win_Sync_Time.bat",
"filename": "Win_Time_Sync.bat",
"submittedBy": "https://github.com/dinger1986",
"name": "ADDC - Sync DC Time",
"description": "Syncs time with domain controller",
@@ -362,6 +465,7 @@
"submittedBy": "https://github.com/dinger1986",
"name": "Defender - Status Report",
"description": "This will check for Malware and Antispyware within the last 24 hours and display, otherwise will report as Healthy. Command Parameter: (number) if provided will check that number of days back in the log.",
"syntax": "[<int>]",
"shell": "powershell",
"category": "TRMM (Win):Security>Antivirus"
},
@@ -397,6 +501,7 @@
"filename": "Win_Display_Message_To_User.ps1",
"submittedBy": "https://github.com/bradhawkins85",
"name": "Message Popup To User",
"syntax": "<string>",
"description": "Displays a popup message to the currently logged on user",
"shell": "powershell",
"category": "TRMM (Win):Other"
@@ -406,6 +511,7 @@
"filename": "Win_Antivirus_Verify.ps1",
"submittedBy": "https://github.com/beejayzed",
"name": "Antivirus - Verify Status",
"syntax": "[-antivirusName <string>]",
"description": "Verify and display status for all installed Antiviruses",
"shell": "powershell",
"category": "TRMM (Win):Security>Antivirus"
@@ -425,6 +531,7 @@
"submittedBy": "https://github.com/silversword411",
"name": "Chocolatey - Install, Uninstall and Upgrade Software",
"description": "This script installs, uninstalls and updates software using Chocolatey with logic to slow tasks to minimize hitting community limits. Mode install/uninstall/upgrade Hosts x",
"syntax": "-$PackageName <string>\n[-Hosts <string>]\n[-mode {(install) | update | uninstall}]",
"shell": "powershell",
"category": "TRMM (Win):3rd Party Software>Chocolatey",
"default_timeout": "600"
@@ -449,10 +556,11 @@
},
{
"guid": "71090fc4-faa6-460b-adb0-95d7863544e1",
"filename": "Win_Check_Events_for_Bluescreens.ps1",
"submittedBy": "https://github.com/dinger1986",
"filename": "Win_Bluescreen_Report.ps1",
"submittedBy": "https://github.com/bbrendon",
"name": "Event Viewer - Bluescreen Notification",
"description": "Event Viewer Monitor - Notify Bluescreen events on your system",
"syntax": "[<int>]",
"shell": "powershell",
"category": "TRMM (Win):Monitoring"
},
@@ -461,7 +569,8 @@
"filename": "Win_Local_User_Created_Monitor.ps1",
"submittedBy": "https://github.com/dinger1986",
"name": "Event Viewer - New User Notification",
"description": "Event Viewer Monitor - Notify when new Local user is created",
"description": "Event Viewer Monitor - Notify when new Local user is created. If parameter provided will search back that number of days",
"syntax": "[<int>]",
"shell": "powershell",
"category": "TRMM (Win):Monitoring"
},
@@ -471,6 +580,7 @@
"submittedBy": "https://github.com/dinger1986",
"name": "Event Viewer - Task Scheduler New Item Notification",
"description": "Event Viewer Monitor - Notify when new Task Scheduler item is created",
"syntax": "[<int>]",
"shell": "powershell",
"category": "TRMM (Win):Monitoring"
},
@@ -478,17 +588,18 @@
"guid": "08ca81f2-f044-4dfc-ad47-090b19b19d76",
"filename": "Win_User_Logged_in_with_Temp_Profile.ps1",
"submittedBy": "https://github.com/dinger1986",
"name": "User Logged in with temp profile check",
"name": "User Check - See if user logged in with temp profile",
"description": "Check if users are logged in with a temp profile",
"shell": "powershell",
"category": "TRMM (Win):Other"
},
{
"guid": "5d905886-9eb1-4129-8b81-a013f842eb24",
"filename": "Win_Rename_Computer.ps1",
"filename": "Win_Computer_Rename.ps1",
"submittedBy": "https://github.com/silversword411",
"name": "Rename Computer",
"description": "Rename computer. First parameter will be new PC name. 2nd parameter if yes will auto-reboot machine",
"syntax": "-NewName <string>\n[-Username <string>]\n[-Password <string>]\n[-Restart]",
"shell": "powershell",
"category": "TRMM (Win):Other",
"default_timeout": 30
@@ -499,6 +610,7 @@
"submittedBy": "https://github.com/tremor021",
"name": "Power - Restart or Shutdown PC",
"description": "Restart PC. Add parameter: shutdown if you want to shutdown computer",
"syntax": "[shutdown]",
"shell": "powershell",
"category": "TRMM (Win):Updates"
},
@@ -523,7 +635,7 @@
"-url {{client.ScreenConnectInstaller}}",
"-clientname {{client.name}}",
"-sitename {{site.name}}",
"-action install"
"-action {(install) | uninstall | start | stop}"
],
"default_timeout": "90",
"shell": "powershell",
@@ -573,7 +685,7 @@
"guid": "7c0c7e37-60ff-462f-9c34-b5cd4c4796a7",
"filename": "Win_Wifi_SSID_and_Password_Retrieval.ps1",
"submittedBy": "https://github.com/silversword411",
"name": "Network Wireless - Retrieve Saved passwords",
"name": "Network Wireless - Retrieve Saved WiFi passwords",
"description": "Returns all saved wifi passwords stored on the computer",
"shell": "powershell",
"category": "TRMM (Win):Network",
@@ -624,7 +736,7 @@
"filename": "Win_Network_TCP_Reset_Stack.bat",
"submittedBy": "https://github.com/silversword411",
"name": "Network - Reset tcp using netsh",
"description": "resets tcp stack using netsh",
"description": "Resets TCP stack using netsh",
"shell": "cmd",
"category": "TRMM (Win):Network",
"default_timeout": "120"
@@ -633,7 +745,7 @@
"guid": "6ce5682a-49db-4c0b-9417-609cf905ac43",
"filename": "Win_Win10_Change_Key_and_Activate.ps1",
"submittedBy": "https://github.com/silversword411",
"name": "Product Key in Win10 Change and Activate",
"name": "Product Key in Win10 - Change and Activate",
"description": "Insert new product key and Activate. Requires 1 parameter the product key you want to use",
"shell": "powershell",
"category": "TRMM (Win):Other",
@@ -653,7 +765,7 @@
"guid": "83f6c6ea-6120-4fd3-bec8-d3abc505dcdf",
"filename": "Win_TRMM_Start_Menu_Delete_Shortcut.ps1",
"submittedBy": "https://github.com/silversword411",
"name": "TacticalRMM Delete Start Menu Shortcut for App",
"name": "TacticalRMM - Delete Start Menu Shortcut for App",
"description": "Delete its application shortcut that's installed in the start menu by default",
"shell": "powershell",
"category": "TRMM (Win):TacticalRMM Related",
@@ -677,6 +789,15 @@
"shell": "powershell",
"category": "TRMM (Win):Security"
},
{
"guid": "43a3206d-f1cb-44ef-8405-aae4d33a0bad",
"filename": "Win_Security_Audit.ps1",
"submittedBy": "theinterwebs",
"name": "Windows Security - Security Audit",
"description": "Runs an Audit on many components of windows to check for security issues",
"shell": "powershell",
"category": "TRMM (Win):Security"
},
{
"guid": "7ea6a11a-05c0-4151-b5c1-cb8af029299f",
"filename": "Win_AzureAD_Check_Connection_Status.ps1",
@@ -735,19 +856,30 @@
"guid": "6a52f495-d43e-40f4-91a9-bbe4f578e6d1",
"filename": "Win_User_Create.ps1",
"submittedBy": "https://github.com/brodur",
"name": "Create Local User",
"name": "User - Create Local",
"description": "Create a local user. Parameters are: username, password and optional: description, fullname, group (adds to Users if not specified)",
"syntax": "-username <string>\n-password <string>\n[-description <string>]\n[-fullname <string>]\n[-group <string>]",
"shell": "powershell",
"category": "TRMM (Win):Other"
"category": "TRMM (Win):User Management"
},
{
"guid": "6e27d5341-88fa-4c2f-9c91-c3aeb1740e85",
"filename": "Win_User_EnableDisable.ps1",
"submittedBy": "https://github.com/silversword411",
"name": "User - Enable or disable a user",
"description": "Used to enable or disable local user",
"syntax": "-Name <string>\n-Enabled { yes | no }",
"shell": "powershell",
"category": "TRMM (Win):User Management"
},
{
"guid": "57997ec7-b293-4fd5-9f90-a25426d0eb90",
"filename": "Win_Users_List.ps1",
"submittedBy": "https://github.com/tremor021",
"name": "Get Computer Users",
"name": "Users - List Local Users and Enabled/Disabled Status",
"description": "Get list of computer users and show which one is enabled",
"shell": "powershell",
"category": "TRMM (Win):Other"
"category": "TRMM (Win):User Management"
},
{
"guid": "77da9c87-5a7a-4ba1-bdde-3eeb3b01d62d",
@@ -783,6 +915,7 @@
"submittedBy": "https://github.com/tremor021",
"name": "EXAMPLE File Copying using powershell",
"description": "Reference Script: Will need manual tweaking, for copying files/folders from paths/websites to local",
"syntax": "-source <string>\n-destination <string>\n[-recursive {True | False}]",
"shell": "powershell",
"category": "TRMM (Win):Misc>Reference",
"default_timeout": "1"
@@ -802,6 +935,7 @@
"filename": "Win_AD_Join_Computer.ps1",
"submittedBy": "https://github.com/rfost52",
"name": "AD - Join Computer to Domain",
"syntax": "-domain <string>\n-password <string>\n-UserAccount ADMINaccount\n[-OUPath <OU=testOU,DC=test,DC=local>]",
"description": "Join computer to a domain in Active Directory",
"shell": "powershell",
"category": "TRMM (Win):Active Directory",
@@ -812,6 +946,7 @@
"filename": "Win_Collect_System_Report_And_Email.ps1",
"submittedBy": "https://github.com/rfost52",
"name": "Collect System Report and Email",
"syntax": "-agentname <string>\n-file <string enter file name with the extension .HTM or .HTML>\n-fromaddress <string>\n-toaddress <string>\n-smtpserver <string>\n-password <string>\n-port <int 587 is the standard port for sending mail over TLS>",
"description": "Generates a system report in HTML format, then emails it",
"shell": "powershell",
"category": "TRMM (Win):Other",

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 = [
("scripts", "0011_auto_20210731_1707"),
]
operations = [
migrations.AlterField(
model_name="script",
name="created_by",
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AlterField(
model_name="script",
name="modified_by",
field=models.CharField(blank=True, max_length=255, null=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.6 on 2021-11-13 16:25
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('scripts', '0012_auto_20210917_1954'),
]
operations = [
migrations.AddField(
model_name='script',
name='syntax',
field=models.TextField(blank=True, null=True),
),
]

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