Compare commits

...

318 Commits

Author SHA1 Message Date
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
378 changed files with 22804 additions and 18594 deletions

View File

@@ -25,7 +25,6 @@ POSTGRES_PASS=postgrespass
# DEV SETTINGS
APP_PORT=80
API_PORT=80
API_PROTOCOL=https://
HTTP_PROTOCOL=https
DOCKER_NETWORK=172.21.0.0/24
DOCKER_NGINX_IP=172.21.0.20

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,7 +209,7 @@ services:
CERT_PRIV_KEY: ${CERT_PRIV_KEY}
APP_PORT: ${APP_PORT}
API_PORT: ${API_PORT}
API_PROTOCOL: ${API_PROTOCOL}
DEV: 1
networks:
dev:
ipv4_address: ${DOCKER_NGINX_IP}

View File

@@ -96,6 +96,7 @@ EOF
"${VIRTUAL_ENV}"/bin/python manage.py load_chocos
"${VIRTUAL_ENV}"/bin/python manage.py load_community_scripts
"${VIRTUAL_ENV}"/bin/python manage.py reload_nats
"${VIRTUAL_ENV}"/bin/python manage.py create_natsapi_conf
"${VIRTUAL_ENV}"/bin/python manage.py create_installer_user
# create super user

3
.gitignore vendored
View File

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

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

@@ -64,7 +64,7 @@ class User(AbstractUser, BaseAuditModel):
"accounts.Role",
null=True,
blank=True,
related_name="roles",
related_name="users",
on_delete=models.SET_NULL,
)
@@ -81,6 +81,8 @@ class Role(BaseAuditModel):
is_superuser = models.BooleanField(default=False)
# agents
can_list_agents = models.BooleanField(default=False)
can_ping_agents = models.BooleanField(default=False)
can_use_mesh = models.BooleanField(default=False)
can_uninstall_agents = models.BooleanField(default=False)
can_update_agents = models.BooleanField(default=False)
@@ -92,55 +94,82 @@ class Role(BaseAuditModel):
can_install_agents = models.BooleanField(default=False)
can_run_scripts = models.BooleanField(default=False)
can_run_bulk = models.BooleanField(default=False)
can_recover_agents = models.BooleanField(default=False)
can_list_agent_history = models.BooleanField(default=False)
# core
can_list_notes = models.BooleanField(default=False)
can_manage_notes = models.BooleanField(default=False)
can_view_core_settings = models.BooleanField(default=False)
can_edit_core_settings = models.BooleanField(default=False)
can_do_server_maint = models.BooleanField(default=False)
can_code_sign = models.BooleanField(default=False)
can_run_urlactions = models.BooleanField(default=False)
can_view_customfields = models.BooleanField(default=False)
can_manage_customfields = models.BooleanField(default=False)
# checks
can_list_checks = models.BooleanField(default=False)
can_manage_checks = models.BooleanField(default=False)
can_run_checks = models.BooleanField(default=False)
# clients
can_list_clients = models.BooleanField(default=False)
can_manage_clients = models.BooleanField(default=False)
can_list_sites = models.BooleanField(default=False)
can_manage_sites = models.BooleanField(default=False)
can_list_deployments = models.BooleanField(default=False)
can_manage_deployments = models.BooleanField(default=False)
can_view_clients = models.ManyToManyField(
"clients.Client", related_name="role_clients", blank=True
)
can_view_sites = models.ManyToManyField(
"clients.Site", related_name="role_sites", blank=True
)
# automation
can_list_automation_policies = models.BooleanField(default=False)
can_manage_automation_policies = models.BooleanField(default=False)
# automated tasks
can_list_autotasks = models.BooleanField(default=False)
can_manage_autotasks = models.BooleanField(default=False)
can_run_autotasks = models.BooleanField(default=False)
# logs
can_view_auditlogs = models.BooleanField(default=False)
can_list_pendingactions = models.BooleanField(default=False)
can_manage_pendingactions = models.BooleanField(default=False)
can_view_debuglogs = models.BooleanField(default=False)
# scripts
can_list_scripts = models.BooleanField(default=False)
can_manage_scripts = models.BooleanField(default=False)
# alerts
can_list_alerts = models.BooleanField(default=False)
can_manage_alerts = models.BooleanField(default=False)
can_list_alerttemplates = models.BooleanField(default=False)
can_manage_alerttemplates = models.BooleanField(default=False)
# win services
can_manage_winsvcs = models.BooleanField(default=False)
# software
can_list_software = models.BooleanField(default=False)
can_manage_software = models.BooleanField(default=False)
# windows updates
can_manage_winupdates = models.BooleanField(default=False)
# accounts
can_list_accounts = models.BooleanField(default=False)
can_manage_accounts = models.BooleanField(default=False)
can_list_roles = models.BooleanField(default=False)
can_manage_roles = models.BooleanField(default=False)
# authentication
can_list_api_keys = models.BooleanField(default=False)
can_manage_api_keys = models.BooleanField(default=False)
def __str__(self):
@@ -153,47 +182,6 @@ class Role(BaseAuditModel):
return RoleAuditSerializer(role).data
@staticmethod
def perms():
return [
"is_superuser",
"can_use_mesh",
"can_uninstall_agents",
"can_update_agents",
"can_edit_agent",
"can_manage_procs",
"can_view_eventlogs",
"can_send_cmd",
"can_reboot_agents",
"can_install_agents",
"can_run_scripts",
"can_run_bulk",
"can_manage_notes",
"can_view_core_settings",
"can_edit_core_settings",
"can_do_server_maint",
"can_code_sign",
"can_manage_checks",
"can_run_checks",
"can_manage_clients",
"can_manage_sites",
"can_manage_deployments",
"can_manage_automation_policies",
"can_manage_autotasks",
"can_run_autotasks",
"can_view_auditlogs",
"can_manage_pendingactions",
"can_view_debuglogs",
"can_manage_scripts",
"can_manage_alerts",
"can_manage_winsvcs",
"can_manage_software",
"can_manage_winupdates",
"can_manage_accounts",
"can_manage_roles",
"can_manage_api_keys",
]
class APIKey(BaseAuditModel):
name = CharField(unique=True, max_length=25)

View File

@@ -6,35 +6,38 @@ from tacticalrmm.permissions import _has_perm
class AccountsPerms(permissions.BasePermission):
def has_permission(self, r, view):
if r.method == "GET":
return True
return _has_perm(r, "can_list_accounts")
else:
# allow users to reset their own password/2fa see issue #686
base_path = "/accounts/users/"
paths = ["reset/", "reset_totp/"]
# allow users to reset their own password/2fa see issue #686
base_path = "/accounts/users/"
paths = ["reset/", "reset_totp/"]
if r.path in [base_path + i for i in paths]:
from accounts.models import User
if r.path in [base_path + i for i in paths]:
from accounts.models import User
try:
user = User.objects.get(pk=r.data["id"])
except User.DoesNotExist:
pass
else:
if user == r.user:
return True
try:
user = User.objects.get(pk=r.data["id"])
except User.DoesNotExist:
pass
else:
if user == r.user:
return True
return _has_perm(r, "can_manage_accounts")
return _has_perm(r, "can_manage_accounts")
class RolesPerms(permissions.BasePermission):
def has_permission(self, r, view):
if r.method == "GET":
return True
return _has_perm(r, "can_manage_roles")
return _has_perm(r, "can_list_roles")
else:
return _has_perm(r, "can_manage_roles")
class APIKeyPerms(permissions.BasePermission):
def has_permission(self, r, view):
if r.method == "GET":
return _has_perm(r, "can_list_api_keys")
return _has_perm(r, "can_manage_api_keys")

View File

@@ -61,10 +61,15 @@ class TOTPSetupSerializer(ModelSerializer):
class RoleSerializer(ModelSerializer):
user_count = SerializerMethodField()
class Meta:
model = Role
fields = "__all__"
def get_user_count(self, obj):
return obj.users.count()
class RoleAuditSerializer(ModelSerializer):
class Meta:

View File

@@ -27,12 +27,12 @@ class TestAccounts(TacticalTestCase):
data = {"username": "bob", "password": "a3asdsa2314"}
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 400)
self.assertEqual(r.data, "bad credentials")
self.assertEqual(r.data, "Bad credentials")
data = {"username": "billy", "password": "hunter2"}
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 400)
self.assertEqual(r.data, "bad credentials")
self.assertEqual(r.data, "Bad credentials")
self.bob.totp_key = "AB5RI6YPFTZAS52G"
self.bob.save()
@@ -61,7 +61,7 @@ class TestAccounts(TacticalTestCase):
mock_verify.return_value = False
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 400)
self.assertEqual(r.data, "bad credentials")
self.assertEqual(r.data, "Bad credentials")
mock_verify.return_value = True
data = {"username": "bob", "password": "asd234234asd", "twofactor": "123456"}
@@ -396,7 +396,7 @@ class TestAPIAuthentication(TacticalTestCase):
self.client_setup()
def test_api_auth(self):
url = "/clients/clients/"
url = "/clients/"
# auth should fail if no header set
self.check_not_authenticated("get", url)

View File

@@ -9,9 +9,8 @@ urlpatterns = [
path("users/reset_totp/", views.UserActions.as_view()),
path("users/setup_totp/", views.TOTPSetup.as_view()),
path("users/ui/", views.UserUI.as_view()),
path("permslist/", views.PermsList.as_view()),
path("roles/", views.GetAddRoles.as_view()),
path("<int:pk>/role/", views.GetUpdateDeleteRole.as_view()),
path("roles/<int:pk>/", views.GetUpdateDeleteRole.as_view()),
path("apikeys/", views.GetAddAPIKeys.as_view()),
path("apikeys/<int:pk>/", views.GetUpdateDeleteAPIKey.as_view()),
]

View File

@@ -44,12 +44,12 @@ class CheckCreds(KnoxLoginView):
AuditLog.audit_user_failed_login(
request.data["username"], debug_info={"ip": request._client_ip}
)
return Response("bad credentials", status=status.HTTP_400_BAD_REQUEST)
return notify_error("Bad credentials")
user = serializer.validated_data["user"]
if user.block_dashboard_login:
return Response("bad credentials", status=status.HTTP_400_BAD_REQUEST)
return notify_error("Bad credentials")
# if totp token not set modify response to notify frontend
if not user.totp_key:
@@ -72,6 +72,9 @@ class LoginView(KnoxLoginView):
serializer.is_valid(raise_exception=True)
user = serializer.validated_data["user"]
if user.block_dashboard_login:
return notify_error("Bad credentials")
token = request.data["twofactor"]
totp = pyotp.TOTP(user.totp_key)
@@ -96,7 +99,7 @@ class LoginView(KnoxLoginView):
AuditLog.audit_user_failed_twofactor(
request.data["username"], debug_info={"ip": request._client_ip}
)
return Response("bad credentials", status=status.HTTP_400_BAD_REQUEST)
return notify_error("Bad credentials")
class GetAddUsers(APIView):
@@ -221,11 +224,6 @@ class UserUI(APIView):
return Response("ok")
class PermsList(APIView):
def get(self, request):
return Response(Role.perms())
class GetAddRoles(APIView):
permission_classes = [IsAuthenticated, RolesPerms]
@@ -237,7 +235,7 @@ class GetAddRoles(APIView):
serializer = RoleSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response("ok")
return Response("Role was added")
class GetUpdateDeleteRole(APIView):
@@ -252,12 +250,12 @@ class GetUpdateDeleteRole(APIView):
serializer = RoleSerializer(instance=role, data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response("ok")
return Response("Role was edited")
def delete(self, request, pk):
role = get_object_or_404(Role, pk=pk)
role.delete()
return Response("ok")
return Response("Role was removed")
class GetAddAPIKeys(APIView):
@@ -269,15 +267,9 @@ class GetAddAPIKeys(APIView):
def post(self, request):
# generate a random API Key
# https://stackoverflow.com/questions/2257441/random-string-generation-with-upper-case-letters-and-digits/23728630#23728630
import random
import string
request.data["key"] = "".join(
random.SystemRandom().choice(string.ascii_uppercase + string.digits)
for _ in range(32)
)
from django.utils.crypto import get_random_string
request.data["key"] = get_random_string(length=32).upper()
serializer = APIKeySerializer(data=request.data)
serializer.is_valid(raise_exception=True)
obj = serializer.save()

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,20 @@
from django.conf import settings
from django.core.management.base import BaseCommand
from packaging import version as pyver
from agents.models import Agent
from agents.tasks import send_agent_update_task
from tacticalrmm.utils import AGENT_DEFER
class Command(BaseCommand):
help = "Triggers an agent update task to run"
def handle(self, *args, **kwargs):
q = Agent.objects.defer(*AGENT_DEFER).exclude(version=settings.LATEST_AGENT_VER)
agent_ids: list[str] = [
i.agent_id
for i in q
if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER)
]
send_agent_update_task.delay(agent_ids=agent_ids)

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
@@ -103,8 +107,7 @@ class Agent(BaseAuditModel):
or (old_agent.monitoring_type != self.monitoring_type)
or (old_agent.block_policy_inheritance != self.block_policy_inheritance)
):
self.generate_checks_from_policies()
self.generate_tasks_from_policies()
generate_agent_checks_task.delay(agents=[self.pk], create_tasks=True)
# calculate alert template for new agents
if not old_agent:
@@ -417,12 +420,6 @@ class Agent(BaseAuditModel):
update.action = "approve"
update.save(update_fields=["action"])
DebugLog.info(
agent=self,
log_type="windows_updates",
message=f"Approving windows updates on {self.hostname}",
)
# returns agent policy merged with a client or site specific policy
def get_patch_policy(self):
@@ -709,7 +706,7 @@ class Agent(BaseAuditModel):
key1 = key[0:48]
key2 = key[48:]
msg = '{{"a":{}, "u":"{}","time":{}}}'.format(
action, user, int(time.time())
action, user.lower(), int(time.time())
)
iv = get_random_bytes(16)
@@ -751,8 +748,8 @@ class Agent(BaseAuditModel):
try:
ret = msgpack.loads(msg.data) # type: ignore
except Exception as e:
DebugLog.error(agent=self, log_type="agent_issues", message=e)
ret = str(e)
DebugLog.error(agent=self, log_type="agent_issues", message=ret)
await nc.close()
return ret
@@ -871,6 +868,8 @@ RECOVERY_CHOICES = [
class RecoveryAction(models.Model):
objects = PermissionQuerySet.as_manager()
agent = models.ForeignKey(
Agent,
related_name="recoveryactions",
@@ -885,6 +884,8 @@ class RecoveryAction(models.Model):
class Note(models.Model):
objects = PermissionQuerySet.as_manager()
agent = models.ForeignKey(
Agent,
related_name="notes",
@@ -905,6 +906,8 @@ class Note(models.Model):
class AgentCustomField(models.Model):
objects = PermissionQuerySet.as_manager()
agent = models.ForeignKey(
Agent,
related_name="custom_fields",
@@ -965,6 +968,8 @@ AGENT_HISTORY_STATUS = (("success", "Success"), ("failure", "Failure"))
class AgentHistory(models.Model):
objects = PermissionQuerySet.as_manager()
agent = models.ForeignKey(
Agent,
related_name="history",
@@ -978,7 +983,7 @@ class AgentHistory(models.Model):
status = models.CharField(
max_length=50, choices=AGENT_HISTORY_STATUS, default="success"
)
username = models.CharField(max_length=50, default="system")
username = models.CharField(max_length=255, default="system")
results = models.TextField(null=True, blank=True)
script = models.ForeignKey(
"scripts.Script",

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

File diff suppressed because it is too large Load Diff

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",
@@ -461,7 +464,7 @@ class Alert(models.Model):
try:
temp_args.append(re.sub("\\{\\{.*\\}\\}", value, arg)) # type: ignore
except Exception as e:
DebugLog.error(log_type="scripting", message=e)
DebugLog.error(log_type="scripting", message=str(e))
continue
else:

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

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

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

@@ -2,6 +2,7 @@ from django.core.management.base import BaseCommand
from logs.models import PendingAction
from scripts.models import Script
from accounts.models import User
class Command(BaseCommand):
@@ -13,3 +14,9 @@ class Command(BaseCommand):
# load community scripts into the db
Script.load_community_scripts()
# make sure installer user is set to block_dashboard_logins
if User.objects.filter(is_installer_user=True).exists():
for user in User.objects.filter(is_installer_user=True):
user.block_dashboard_login = True
user.save()

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):
@@ -306,6 +314,31 @@ class CodeSignToken(models.Model):
super(CodeSignToken, self).save(*args, **kwargs)
@property
def is_valid(self) -> bool:
if not self.token:
return False
errors = []
for url in settings.EXE_GEN_URLS:
try:
r = requests.post(
f"{url}/api/v1/checktoken",
json={"token": self.token},
headers={"Content-type": "application/json"},
timeout=15,
)
except Exception as e:
errors.append(str(e))
else:
errors = []
break
if errors:
return False
return r.status_code == 200
def __str__(self):
return "Code signing token"

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

@@ -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,28 +1,31 @@
import os
import pprint
import re
from django.conf import settings
from django.db.models.fields import IPAddressField
from django.shortcuts import get_object_or_404
from logs.models import AuditLog
from rest_framework import status
from rest_framework.decorators import api_view, permission_classes
from rest_framework.exceptions import ParseError
from rest_framework.exceptions import ParseError, PermissionDenied
from rest_framework.parsers import FileUploadParser
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from agents.permissions import MeshPerms
from tacticalrmm.utils import notify_error
from tacticalrmm.permissions import (
_has_perm_on_client,
_has_perm_on_agent,
_has_perm_on_site,
)
from .models import CodeSignToken, CoreSettings, CustomField, GlobalKVStore, URLAction
from .permissions import (
CodeSignPerms,
ViewCoreSettingsPerms,
EditCoreSettingsPerms,
CoreSettingsPerms,
ServerMaintPerms,
URLActionPerms,
CustomFieldPerms,
)
from .serializers import (
CodeSignTokenSerializer,
@@ -34,7 +37,7 @@ from .serializers import (
class UploadMeshAgent(APIView):
permission_classes = [IsAuthenticated, MeshPerms]
permission_classes = [IsAuthenticated, CoreSettingsPerms]
parser_class = (FileUploadParser,)
def put(self, request, format=None):
@@ -50,25 +53,25 @@ class UploadMeshAgent(APIView):
for chunk in f.chunks():
j.write(chunk)
return Response(status=status.HTTP_201_CREATED)
return Response(
"Mesh Agent uploaded successfully", status=status.HTTP_201_CREATED
)
@api_view()
@permission_classes([IsAuthenticated, ViewCoreSettingsPerms])
def get_core_settings(request):
settings = CoreSettings.objects.first()
return Response(CoreSettingsSerializer(settings).data)
class GetEditCoreSettings(APIView):
permission_classes = [IsAuthenticated, CoreSettingsPerms]
def get(self, request):
settings = CoreSettings.objects.first()
return Response(CoreSettingsSerializer(settings).data)
@api_view(["PATCH"])
@permission_classes([IsAuthenticated, EditCoreSettingsPerms])
def edit_settings(request):
coresettings = CoreSettings.objects.first()
serializer = CoreSettingsSerializer(instance=coresettings, data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
def put(self, request):
coresettings = CoreSettings.objects.first()
serializer = CoreSettingsSerializer(instance=coresettings, data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response("ok")
return Response("ok")
@api_view()
@@ -100,7 +103,8 @@ def dashboard_info(request):
)
@api_view()
@api_view(["POST"])
@permission_classes([IsAuthenticated, CoreSettingsPerms])
def email_test(request):
core = CoreSettings.objects.first()
r = core.send_mail(
@@ -169,10 +173,13 @@ def server_maintenance(request):
class GetAddCustomFields(APIView):
permission_classes = [IsAuthenticated, EditCoreSettingsPerms]
permission_classes = [IsAuthenticated, CustomFieldPerms]
def get(self, request):
fields = CustomField.objects.all()
if "model" in request.query_params.keys():
fields = CustomField.objects.filter(model=request.query_params["model"])
else:
fields = CustomField.objects.all()
return Response(CustomFieldSerializer(fields, many=True).data)
def patch(self, request):
@@ -191,7 +198,7 @@ class GetAddCustomFields(APIView):
class GetUpdateDeleteCustomFields(APIView):
permission_classes = [IsAuthenticated, EditCoreSettingsPerms]
permission_classes = [IsAuthenticated, CustomFieldPerms]
def get(self, request, pk):
custom_field = get_object_or_404(CustomField, pk=pk)
@@ -274,13 +281,15 @@ class CodeSign(APIView):
if t is None or t == "":
return notify_error(err)
pks: list[int] = list(Agent.objects.only("pk").values_list("pk", flat=True))
force_code_sign.delay(pks=pks)
agent_ids: list[str] = list(
Agent.objects.only("pk", "agent_id").values_list("agent_id", flat=True)
)
force_code_sign.delay(agent_ids=agent_ids)
return Response("Agents will be code signed shortly")
class GetAddKeyStore(APIView):
permission_classes = [IsAuthenticated, EditCoreSettingsPerms]
permission_classes = [IsAuthenticated, CoreSettingsPerms]
def get(self, request):
keys = GlobalKVStore.objects.all()
@@ -295,7 +304,7 @@ class GetAddKeyStore(APIView):
class UpdateDeleteKeyStore(APIView):
permission_classes = [IsAuthenticated, EditCoreSettingsPerms]
permission_classes = [IsAuthenticated, CoreSettingsPerms]
def put(self, request, pk):
key = get_object_or_404(GlobalKVStore, pk=pk)
@@ -313,6 +322,8 @@ class UpdateDeleteKeyStore(APIView):
class GetAddURLAction(APIView):
permission_classes = [IsAuthenticated, CoreSettingsPerms]
def get(self, request):
actions = URLAction.objects.all()
return Response(URLActionSerializer(actions, many=True).data)
@@ -326,6 +337,8 @@ class GetAddURLAction(APIView):
class UpdateDeleteURLAction(APIView):
permission_classes = [IsAuthenticated, CoreSettingsPerms]
def put(self, request, pk):
action = get_object_or_404(URLAction, pk=pk)
@@ -344,6 +357,8 @@ class UpdateDeleteURLAction(APIView):
class RunURLAction(APIView):
permission_classes = [IsAuthenticated, URLActionPerms]
def patch(self, request):
from requests.utils import requote_uri
@@ -351,11 +366,20 @@ class RunURLAction(APIView):
from clients.models import Client, Site
from tacticalrmm.utils import replace_db_values
if "agent" in request.data.keys():
instance = get_object_or_404(Agent, pk=request.data["agent"])
if "agent_id" in request.data.keys():
if not _has_perm_on_agent(request.user, request.data["agent_id"]):
raise PermissionDenied()
instance = get_object_or_404(Agent, agent_id=request.data["agent_id"])
elif "site" in request.data.keys():
if not _has_perm_on_site(request.user, request.data["site"]):
raise PermissionDenied()
instance = get_object_or_404(Site, pk=request.data["site"])
elif "client" in request.data.keys():
if not _has_perm_on_client(request.user, request.data["client"]):
raise PermissionDenied()
instance = get_object_or_404(Client, pk=request.data["client"])
else:
return notify_error("received an incorrect request")
@@ -382,8 +406,9 @@ class RunURLAction(APIView):
class TwilioSMSTest(APIView):
def get(self, request):
from twilio.rest import Client as TwClient
permission_classes = [IsAuthenticated, CoreSettingsPerms]
def post(self, request):
core = CoreSettings.objects.first()
if not core.sms_is_configured:
@@ -391,14 +416,9 @@ class TwilioSMSTest(APIView):
"All fields are required, including at least 1 recipient"
)
try:
tw_client = TwClient(core.twilio_account_sid, core.twilio_auth_token)
tw_client.messages.create(
body="TacticalRMM Test SMS",
to=core.sms_alert_recipients[0],
from_=core.twilio_number,
)
except Exception as e:
return notify_error(pprint.pformat(e))
r = core.send_sms("TacticalRMM Test SMS", test=True)
return Response("SMS Test OK!")
if not isinstance(r, bool) and isinstance(r, str):
return notify_error(r)
return Response("SMS Test sent successfully!")

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():
@@ -65,9 +66,9 @@ STATUS_CHOICES = [
class AuditLog(models.Model):
username = models.CharField(max_length=100)
username = models.CharField(max_length=255)
agent = models.CharField(max_length=255, null=True, blank=True)
agent_id = models.PositiveIntegerField(blank=True, null=True)
agent_id = models.CharField(max_length=255, blank=True, null=True)
entry_time = models.DateTimeField(auto_now_add=True)
action = models.CharField(max_length=100, choices=AUDIT_ACTION_TYPE_CHOICES)
object_type = models.CharField(max_length=100, choices=AUDIT_OBJECT_TYPE_CHOICES)
@@ -94,7 +95,7 @@ class AuditLog(models.Model):
AuditLog.objects.create(
username=username,
agent=agent.hostname,
agent_id=agent.id,
agent_id=agent.agent_id,
object_type="agent",
action="remote_session",
message=f"{username} used Mesh Central to initiate a remote session to {agent.hostname}.",
@@ -106,6 +107,7 @@ class AuditLog(models.Model):
AuditLog.objects.create(
username=username,
agent=agent.hostname,
agent_id=agent.agent_id,
object_type="agent",
action="execute_command",
message=f"{username} issued {shell} command on {agent.hostname}.",
@@ -120,7 +122,8 @@ class AuditLog(models.Model):
AuditLog.objects.create(
username=username,
object_type=object_type,
agent_id=before["id"] if object_type == "agent" else None,
agent=before["hostname"] if object_type == "agent" else None,
agent_id=before["agent_id"] if object_type == "agent" else None,
action="modify",
message=f"{username} modified {object_type} {name}",
before_value=before,
@@ -133,7 +136,8 @@ class AuditLog(models.Model):
AuditLog.objects.create(
username=username,
object_type=object_type,
agent=after["id"] if object_type == "agent" else None,
agent=after["hostname"] if object_type == "agent" else None,
agent_id=after["agent_id"] if object_type == "agent" else None,
action="add",
message=f"{username} added {object_type} {name}",
after_value=after,
@@ -145,7 +149,8 @@ class AuditLog(models.Model):
AuditLog.objects.create(
username=username,
object_type=object_type,
agent=before["id"] if object_type == "agent" else None,
agent=before["hostname"] if object_type == "agent" else None,
agent_id=before["agent_id"] if object_type == "agent" else None,
action="delete",
message=f"{username} deleted {object_type} {name}",
before_value=before,
@@ -156,7 +161,7 @@ class AuditLog(models.Model):
def audit_script_run(username, agent, script, debug_info={}):
AuditLog.objects.create(
agent=agent.hostname,
agent_id=agent.id,
agent_id=agent.agent_id,
username=username,
object_type="agent",
action="execute_script",
@@ -202,7 +207,7 @@ class AuditLog(models.Model):
AuditLog.objects.create(
username=username,
agent=instance.hostname if classname == "Agent" else None,
agent_id=instance.id if classname == "Agent" else None,
agent_id=instance.agent_id if classname == "Agent" else None,
object_type=classname.lower(),
action="url_action",
message=f"{username} ran url action: {urlaction.pattern} on {classname}: {name}",
@@ -227,7 +232,7 @@ class AuditLog(models.Model):
site = Site.objects.get(pk=affected["site"])
target = f"on all agents within site: {site.client.name}\\{site.name}"
elif affected["target"] == "agents":
agents = Agent.objects.filter(pk__in=affected["agents"]).values_list(
agents = Agent.objects.filter(agent_id__in=affected["agents"]).values_list(
"hostname", flat=True
)
target = "on multiple agents"
@@ -266,6 +271,8 @@ LOG_TYPE_CHOICES = [
class DebugLog(models.Model):
objects = PermissionQuerySet.as_manager()
entry_time = models.DateTimeField(auto_now_add=True)
agent = models.ForeignKey(
"agents.Agent",
@@ -317,6 +324,7 @@ class DebugLog(models.Model):
class PendingAction(models.Model):
objects = PermissionQuerySet.as_manager()
agent = models.ForeignKey(
"agents.Agent",
@@ -377,9 +385,9 @@ class BaseAuditModel(models.Model):
abstract = True
# create audit fields
created_by = models.CharField(max_length=100, null=True, blank=True)
created_by = models.CharField(max_length=255, null=True, blank=True)
created_time = models.DateTimeField(auto_now_add=True, null=True, blank=True)
modified_by = models.CharField(max_length=100, null=True, blank=True)
modified_by = models.CharField(max_length=255, null=True, blank=True)
modified_time = models.DateTimeField(auto_now=True, null=True, blank=True)
@abstractmethod

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
from tacticalrmm.permissions import _audit_log_filter, _has_perm_on_agent
from .models import AuditLog, PendingAction, DebugLog
from .permissions import AuditLogPerms, DebugLogPerms, ManagePendingActionPerms
from agents.models import Agent
from .permissions import AuditLogPerms, DebugLogPerms, PendingActionPerms
from .serializers import AuditLogSerializer, DebugLogSerializer, PendingActionSerializer
@@ -46,13 +44,11 @@ class GetAuditLogs(APIView):
agentFilter = Q(agent_id__in=request.data["agentFilter"])
elif "clientFilter" in request.data:
clients = Client.objects.filter(
pk__in=request.data["clientFilter"]
).values_list("id")
agents = Agent.objects.filter(site__client_id__in=clients).values_list(
"hostname"
clients = Client.objects.filter(pk__in=request.data["clientFilter"])
agents = Agent.objects.filter(site__client__in=clients).values_list(
"agent_id"
)
clientFilter = Q(agent__in=agents)
clientFilter = Q(agent_id__in=agents)
if "userFilter" in request.data:
userFilter = Q(username__in=request.data["userFilter"])
@@ -76,14 +72,16 @@ class GetAuditLogs(APIView):
.filter(actionFilter)
.filter(objectFilter)
.filter(timeFilter)
.filter(_audit_log_filter(request.user))
).order_by(order_by)
paginator = Paginator(audit_logs, pagination["rowsPerPage"])
ctx = {"default_tz": get_default_timezone()}
return Response(
{
"audit_logs": AuditLogSerializer(
paginator.get_page(pagination["page"]), many=True
paginator.get_page(pagination["page"]), many=True, context=ctx
).data,
"total": paginator.count,
}
@@ -91,37 +89,23 @@ class GetAuditLogs(APIView):
class PendingActions(APIView):
permission_classes = [IsAuthenticated, ManagePendingActionPerms]
def patch(self, request):
status_filter = "completed" if request.data["showCompleted"] else "pending"
if "agentPK" in request.data.keys():
actions = PendingAction.objects.filter(
agent__pk=request.data["agentPK"], status=status_filter
)
total = PendingAction.objects.filter(
agent__pk=request.data["agentPK"]
).count()
completed = PendingAction.objects.filter(
agent__pk=request.data["agentPK"], status="completed"
).count()
permission_classes = [IsAuthenticated, PendingActionPerms]
def get(self, request, agent_id=None):
if agent_id:
agent = get_object_or_404(Agent, agent_id=agent_id)
actions = PendingAction.objects.filter(agent=agent)
else:
actions = PendingAction.objects.filter(status=status_filter).select_related(
"agent"
)
total = PendingAction.objects.count()
completed = PendingAction.objects.filter(status="completed").count()
actions = PendingAction.objects.filter_by_role(request.user)
ret = {
"actions": PendingActionSerializer(actions, many=True).data,
"completed_count": completed,
"total": total,
}
return Response(ret)
return Response(PendingActionSerializer(actions, many=True).data)
def delete(self, request, pk):
action = get_object_or_404(PendingAction, pk=pk)
if not _has_perm_on_agent(request.user, action.agent.agent_id):
raise PermissionDenied()
def delete(self, request):
action = get_object_or_404(PendingAction, pk=request.data["pk"])
nats_data = {
"func": "delschedtask",
"schedtaskpayload": {"name": action.details["taskname"]},
@@ -138,7 +122,6 @@ class GetDebugLog(APIView):
permission_classes = [IsAuthenticated, DebugLogPerms]
def patch(self, request):
agentFilter = Q()
logTypeFilter = Q()
logLevelFilter = Q()
@@ -150,12 +133,18 @@ class GetDebugLog(APIView):
logLevelFilter = Q(log_level=request.data["logLevelFilter"])
if "agentFilter" in request.data:
agentFilter = Q(agent=request.data["agentFilter"])
agentFilter = Q(agent__agent_id=request.data["agentFilter"])
debug_logs = (
DebugLog.objects.filter(logLevelFilter)
DebugLog.objects.prefetch_related("agent")
.filter_by_role(request.user)
.filter(logLevelFilter)
.filter(agentFilter)
.filter(logTypeFilter)
)
return Response(DebugLogSerializer(debug_logs, many=True).data)
ctx = {"default_tz": get_default_timezone()}
ret = DebugLogSerializer(
debug_logs.order_by("-entry_time")[0:1000], many=True, context=ctx
).data
return Response(ret)

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
celery==5.2.1
certifi==2021.10.8
cffi==1.15.0
channels==3.0.4
channels_redis==3.3.0
channels_redis==3.3.1
chardet==4.0.0
cryptography==3.4.8
cryptography==35.0.0
daphne==3.0.2
Django==3.2.7
django-cors-headers==3.8.0
django-ipware==3.0.2
Django==3.2.9
django-cors-headers==3.10.0
django-ipware==4.0.0
django-rest-knox==4.1.0
djangorestframework==3.12.4
future==0.18.2
loguru==0.5.3
msgpack==1.0.2
packaging==21.0
psycopg2-binary==2.9.1
pycparser==2.20
pycryptodome==3.10.1
packaging==21.3
psycopg2-binary==2.9.2
pycparser==2.21
pycryptodome==3.11.0
pyotp==2.6.0
pyparsing==2.4.7
pytz==2021.1
pytz==2021.3
qrcode==6.1
redis==3.5.3
requests==2.26.0
six==1.16.0
sqlparse==0.4.1
twilio==6.63.1
urllib3==1.26.6
uWSGI==2.0.19.1
sqlparse==0.4.2
twilio==7.3.1
urllib3==1.26.7
uWSGI==2.0.20
validators==0.18.2
vine==5.0.0
websockets==9.1
zipp==3.5.0
zipp==3.6.0
drf_spectacular==0.21.0

View File

@@ -30,18 +30,27 @@
"default_timeout": "300"
},
{
"guid": "2ee134d5-76aa-4160-b334-a1efbc62079f",
"filename": "Win_Install_Duplicati.ps1",
"submittedBy": "https://github.com/Omnicef",
"name": "Duplicati - Install",
"description": "This script installs Duplicati 2.0.5.1 as a service.",
"shell": "powershell",
"guid": "7b1d90a1-3eda-48ab-9c49-20e714c9e82a",
"filename": "Win_Duplicati_Install.bat",
"submittedBy": "https://github.com/dinger1986",
"name": "Duplicati - Install 2.0.6.100 to work with Community Check Status",
"description": "This script installs Duplicati 2.0.6.100 as a service and creates status files to be used with commuity check",
"shell": "cmd",
"category": "TRMM (Win):3rd Party Software",
"default_timeout": "300"
},
{
"guid": "7c14beb4-d1c3-41aa-8e70-92a267d6e080",
"filename": "Win_Duplicati_Status.ps1",
"submittedBy": "https://github.com/dinger1986",
"name": "Duplicati - Check Status",
"description": "Checks Duplicati Backup is running properly over the last 24 hours",
"shell": "powershell",
"category": "TRMM (Win):3rd Party Software>Monitoring"
},
{
"guid": "81cc5bcb-01bf-4b0c-89b9-0ac0f3fe0c04",
"filename": "Win_Reset_Windows_Update.ps1",
"filename": "Win_Windows_Update_Reset.ps1",
"submittedBy": "https://github.com/Omnicef",
"name": "Windows Update - Reset",
"description": "This script will reset all of the Windows Updates components to DEFAULT SETTINGS.",
@@ -91,17 +100,19 @@
"guid": "9d34f482-1f0c-4b2f-b65f-a9cf3c13ef5f",
"filename": "Win_TRMM_Rename_Installed_App.ps1",
"submittedBy": "https://github.com/bradhawkins85",
"name": "TacticalRMM Agent Rename",
"name": "TacticalRMM - Agent Rename",
"description": "Updates the DisplayName registry entry for the Tactical RMM windows agent to your desired name. This script takes 1 required argument: the name you wish to set.",
"syntax": "<string>",
"shell": "powershell",
"category": "TRMM (Win):TacticalRMM Related"
},
{
"guid": "525ae965-1dcf-4c17-92b3-5da3cf6819f5",
"filename": "Win_Bitlocker_Encrypted_Drive_c.ps1",
"submittedBy": "https://github.com/ThatsNASt",
"name": "Bitlocker - Check C Drive for Status",
"description": "Runs a check on drive C for Bitlocker status.",
"filename": "Win_Bitlocker_Drive_Check_Status.ps1",
"submittedBy": "https://github.com/silversword411",
"name": "Bitlocker - Check Drive for Status",
"description": "Runs a check on drive for Bitlocker status. Returns 0 if Bitlocker is not enabled, 1 if Bitlocker is enabled",
"syntax": "[Drive <string>]",
"shell": "powershell",
"category": "TRMM (Win):Storage"
},
@@ -227,22 +238,23 @@
"default_timeout": "25000"
},
{
"guid": "375323e5-cac6-4f35-a304-bb7cef35902d",
"filename": "Win_Disk_Status.ps1",
"submittedBy": "https://github.com/dinger1986",
"name": "Disk Hardware Health Check (using Event Viewer errors)",
"description": "Checks local disks for errors reported in event viewer within the last 24 hours",
"guid": "4d0ba685-2259-44be-9010-8ed2fa48bf74",
"filename": "Win_Win11_Ready.ps1",
"submittedBy": "https://github.com/adamjrberry/",
"name": "Windows 11 Upgrade capable check",
"description": "Checks to see if machine is Win11 capable",
"shell": "powershell",
"category": "TRMM (Win):Hardware"
"category": "TRMM (Win):Updates",
"default_timeout": "3600"
},
{
"guid": "7c14beb4-d1c3-41aa-8e70-92a267d6e080",
"filename": "Win_Duplicati_Status.ps1",
"guid": "375323e5-cac6-4f35-a304-bb7cef35902d",
"filename": "Win_Disk_Volume_Status.ps1",
"submittedBy": "https://github.com/dinger1986",
"name": "Duplicati - Check Status",
"description": "Checks Duplicati Backup is running properly over the last 24 hours",
"name": "Disk Drive Volume Health Check (using Event Viewer errors)",
"description": "Checks Drive Volumes for errors reported in event viewer within the last 24 hours",
"shell": "powershell",
"category": "TRMM (Win):3rd Party Software"
"category": "TRMM (Win):Hardware"
},
{
"guid": "907652a5-9ec1-4759-9871-a7743f805ff2",
@@ -317,7 +329,7 @@
},
{
"guid": "a821975c-60df-4d58-8990-6cf8a55b4ee0",
"filename": "Win_Sync_Time.bat",
"filename": "Win_Time_Sync.bat",
"submittedBy": "https://github.com/dinger1986",
"name": "ADDC - Sync DC Time",
"description": "Syncs time with domain controller",
@@ -425,6 +437,7 @@
"submittedBy": "https://github.com/silversword411",
"name": "Chocolatey - Install, Uninstall and Upgrade Software",
"description": "This script installs, uninstalls and updates software using Chocolatey with logic to slow tasks to minimize hitting community limits. Mode install/uninstall/upgrade Hosts x",
"syntax": "-$PackageName <string>\n[-Hosts <string>]\n[-mode {(install) | update | uninstall}]",
"shell": "powershell",
"category": "TRMM (Win):3rd Party Software>Chocolatey",
"default_timeout": "600"
@@ -478,17 +491,18 @@
"guid": "08ca81f2-f044-4dfc-ad47-090b19b19d76",
"filename": "Win_User_Logged_in_with_Temp_Profile.ps1",
"submittedBy": "https://github.com/dinger1986",
"name": "User Logged in with temp profile check",
"name": "User Check - See if user logged in with temp profile",
"description": "Check if users are logged in with a temp profile",
"shell": "powershell",
"category": "TRMM (Win):Other"
},
{
"guid": "5d905886-9eb1-4129-8b81-a013f842eb24",
"filename": "Win_Rename_Computer.ps1",
"filename": "Win_Computer_Rename.ps1",
"submittedBy": "https://github.com/silversword411",
"name": "Rename Computer",
"description": "Rename computer. First parameter will be new PC name. 2nd parameter if yes will auto-reboot machine",
"syntax": "-NewName <string>\n[-Username <string>]\n[-Password <string>]\n[-Restart]",
"shell": "powershell",
"category": "TRMM (Win):Other",
"default_timeout": 30
@@ -499,6 +513,7 @@
"submittedBy": "https://github.com/tremor021",
"name": "Power - Restart or Shutdown PC",
"description": "Restart PC. Add parameter: shutdown if you want to shutdown computer",
"syntax": "[shutdown]",
"shell": "powershell",
"category": "TRMM (Win):Updates"
},
@@ -523,7 +538,7 @@
"-url {{client.ScreenConnectInstaller}}",
"-clientname {{client.name}}",
"-sitename {{site.name}}",
"-action install"
"-action {(install) | uninstall | start | stop}"
],
"default_timeout": "90",
"shell": "powershell",
@@ -573,7 +588,7 @@
"guid": "7c0c7e37-60ff-462f-9c34-b5cd4c4796a7",
"filename": "Win_Wifi_SSID_and_Password_Retrieval.ps1",
"submittedBy": "https://github.com/silversword411",
"name": "Network Wireless - Retrieve Saved passwords",
"name": "Network Wireless - Retrieve Saved WiFi passwords",
"description": "Returns all saved wifi passwords stored on the computer",
"shell": "powershell",
"category": "TRMM (Win):Network",
@@ -624,7 +639,7 @@
"filename": "Win_Network_TCP_Reset_Stack.bat",
"submittedBy": "https://github.com/silversword411",
"name": "Network - Reset tcp using netsh",
"description": "resets tcp stack using netsh",
"description": "Resets TCP stack using netsh",
"shell": "cmd",
"category": "TRMM (Win):Network",
"default_timeout": "120"
@@ -633,7 +648,7 @@
"guid": "6ce5682a-49db-4c0b-9417-609cf905ac43",
"filename": "Win_Win10_Change_Key_and_Activate.ps1",
"submittedBy": "https://github.com/silversword411",
"name": "Product Key in Win10 Change and Activate",
"name": "Product Key in Win10 - Change and Activate",
"description": "Insert new product key and Activate. Requires 1 parameter the product key you want to use",
"shell": "powershell",
"category": "TRMM (Win):Other",
@@ -653,7 +668,7 @@
"guid": "83f6c6ea-6120-4fd3-bec8-d3abc505dcdf",
"filename": "Win_TRMM_Start_Menu_Delete_Shortcut.ps1",
"submittedBy": "https://github.com/silversword411",
"name": "TacticalRMM Delete Start Menu Shortcut for App",
"name": "TacticalRMM - Delete Start Menu Shortcut for App",
"description": "Delete its application shortcut that's installed in the start menu by default",
"shell": "powershell",
"category": "TRMM (Win):TacticalRMM Related",
@@ -735,19 +750,20 @@
"guid": "6a52f495-d43e-40f4-91a9-bbe4f578e6d1",
"filename": "Win_User_Create.ps1",
"submittedBy": "https://github.com/brodur",
"name": "Create Local User",
"name": "User - Create Local",
"description": "Create a local user. Parameters are: username, password and optional: description, fullname, group (adds to Users if not specified)",
"syntax": "-username <string>\n-password <string>\n[-description <string>]\n[-fullname <string>]\n[-group <string>]",
"shell": "powershell",
"category": "TRMM (Win):Other"
"category": "TRMM (Win):User Management"
},
{
"guid": "57997ec7-b293-4fd5-9f90-a25426d0eb90",
"filename": "Win_Users_List.ps1",
"submittedBy": "https://github.com/tremor021",
"name": "Get Computer Users",
"name": "Users - List Local Users and Enabled/Disabled Status",
"description": "Get list of computer users and show which one is enabled",
"shell": "powershell",
"category": "TRMM (Win):Other"
"category": "TRMM (Win):User Management"
},
{
"guid": "77da9c87-5a7a-4ba1-bdde-3eeb3b01d62d",

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

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.6 on 2021-11-19 15:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('scripts', '0013_script_syntax'),
]
operations = [
migrations.AlterField(
model_name='script',
name='filename',
field=models.CharField(blank=True, max_length=255, null=True),
),
]

View File

@@ -24,7 +24,7 @@ class Script(BaseAuditModel):
guid = models.CharField(max_length=64, null=True, blank=True)
name = models.CharField(max_length=255)
description = models.TextField(null=True, blank=True, default="")
filename = models.CharField(max_length=255) # deprecated
filename = models.CharField(max_length=255, null=True, blank=True)
shell = models.CharField(
max_length=100, choices=SCRIPT_SHELLS, default="powershell"
)
@@ -37,6 +37,7 @@ class Script(BaseAuditModel):
blank=True,
default=list,
)
syntax = TextField(null=True, blank=True)
favorite = models.BooleanField(default=False)
category = models.CharField(max_length=100, null=True, blank=True)
code_base64 = models.TextField(null=True, blank=True, default="")
@@ -115,6 +116,8 @@ class Script(BaseAuditModel):
args = script["args"] if "args" in script.keys() else []
syntax = script["syntax"] if "syntax" in script.keys() else ""
if s.exists():
i = s.first()
i.name = script["name"] # type: ignore
@@ -123,6 +126,8 @@ class Script(BaseAuditModel):
i.shell = script["shell"] # type: ignore
i.default_timeout = default_timeout # type: ignore
i.args = args # type: ignore
i.syntax = syntax # type: ignore
i.filename = script["filename"] # type: ignore
with open(os.path.join(scripts_dir, script["filename"]), "rb") as f:
script_bytes = (
@@ -139,6 +144,8 @@ class Script(BaseAuditModel):
"code_base64",
"shell",
"args",
"filename",
"syntax",
]
)
@@ -157,6 +164,8 @@ class Script(BaseAuditModel):
s.shell = script["shell"]
s.default_timeout = default_timeout
s.args = args
s.filename = script["filename"]
s.syntax = syntax
with open(
os.path.join(scripts_dir, script["filename"]), "rb"
@@ -178,6 +187,8 @@ class Script(BaseAuditModel):
"code_base64",
"shell",
"args",
"filename",
"syntax",
]
)
@@ -200,6 +211,8 @@ class Script(BaseAuditModel):
category=category,
default_timeout=default_timeout,
args=args,
filename=script["filename"],
syntax=syntax,
).save()
# delete community scripts that had their name changed

View File

@@ -3,9 +3,9 @@ from rest_framework import permissions
from tacticalrmm.permissions import _has_perm
class ManageScriptsPerms(permissions.BasePermission):
class ScriptsPerms(permissions.BasePermission):
def has_permission(self, r, view):
if r.method == "GET":
return True
return _has_perm(r, "can_manage_scripts")
return _has_perm(r, "can_list_scripts")
else:
return _has_perm(r, "can_manage_scripts")

View File

@@ -16,6 +16,8 @@ class ScriptTableSerializer(ModelSerializer):
"category",
"favorite",
"default_timeout",
"syntax",
"filename",
]
@@ -32,6 +34,8 @@ class ScriptSerializer(ModelSerializer):
"favorite",
"code_base64",
"default_timeout",
"syntax",
"filename",
]

View File

@@ -118,14 +118,12 @@ class TestScriptViews(TacticalTestCase):
self.check_not_authenticated("get", url)
@patch("agents.models.Agent.nats_cmd")
@patch("agents.models.Agent.nats_cmd", return_value="return value")
def test_test_script(self, run_script):
url = "/scripts/testscript/"
run_script.return_value = "return value"
agent = baker.make_recipe("agents.agent")
url = f"/scripts/{agent.agent_id}/test/"
data = {
"agent": agent.pk,
"code": "some_code",
"timeout": 90,
"args": [],
@@ -161,7 +159,7 @@ class TestScriptViews(TacticalTestCase):
def test_download_script(self):
# test a call where script doesn't exist
resp = self.client.get("/scripts/download/500/", format="json")
resp = self.client.get("/scripts/500/download/", format="json")
self.assertEqual(resp.status_code, 404)
# return script code property should be "Test"
@@ -170,7 +168,7 @@ class TestScriptViews(TacticalTestCase):
script = baker.make(
"scripts.Script", code_base64="VGVzdA==", shell="powershell"
)
url = f"/scripts/download/{script.pk}/" # type: ignore
url = f"/scripts/{script.pk}/download/" # type: ignore
resp = self.client.get(url, format="json")
self.assertEqual(resp.status_code, 200)
@@ -178,7 +176,7 @@ class TestScriptViews(TacticalTestCase):
# test batch file
script = baker.make("scripts.Script", code_base64="VGVzdA==", shell="cmd")
url = f"/scripts/download/{script.pk}/" # type: ignore
url = f"/scripts/{script.pk}/download/" # type: ignore
resp = self.client.get(url, format="json")
self.assertEqual(resp.status_code, 200)
@@ -186,7 +184,7 @@ class TestScriptViews(TacticalTestCase):
# test python file
script = baker.make("scripts.Script", code_base64="VGVzdA==", shell="python")
url = f"/scripts/download/{script.pk}/" # type: ignore
url = f"/scripts/{script.pk}/download/" # type: ignore
resp = self.client.get(url, format="json")
self.assertEqual(resp.status_code, 200)

View File

@@ -7,6 +7,6 @@ urlpatterns = [
path("<int:pk>/", views.GetUpdateDeleteScript.as_view()),
path("snippets/", views.GetAddScriptSnippets.as_view()),
path("snippets/<int:pk>/", views.GetUpdateDeleteScriptSnippet.as_view()),
path("testscript/", views.TestScript.as_view()),
path("download/<int:pk>/", views.download),
path("<agent:agent_id>/test/", views.TestScript.as_view()),
path("<int:pk>/download/", views.download),
]

View File

@@ -9,7 +9,7 @@ from rest_framework.views import APIView
from tacticalrmm.utils import notify_error
from .models import Script, ScriptSnippet
from .permissions import ManageScriptsPerms
from .permissions import ScriptsPerms
from agents.permissions import RunScriptPerms
from .serializers import (
ScriptSerializer,
@@ -19,7 +19,7 @@ from .serializers import (
class GetAddScripts(APIView):
permission_classes = [IsAuthenticated, ManageScriptsPerms]
permission_classes = [IsAuthenticated, ScriptsPerms]
def get(self, request):
@@ -41,7 +41,7 @@ class GetAddScripts(APIView):
class GetUpdateDeleteScript(APIView):
permission_classes = [IsAuthenticated, ManageScriptsPerms]
permission_classes = [IsAuthenticated, ScriptsPerms]
def get(self, request, pk):
script = get_object_or_404(Script, pk=pk)
@@ -78,7 +78,7 @@ class GetUpdateDeleteScript(APIView):
class GetAddScriptSnippets(APIView):
permission_classes = [IsAuthenticated, ManageScriptsPerms]
permission_classes = [IsAuthenticated, ScriptsPerms]
def get(self, request):
snippets = ScriptSnippet.objects.all()
@@ -94,7 +94,7 @@ class GetAddScriptSnippets(APIView):
class GetUpdateDeleteScriptSnippet(APIView):
permission_classes = [IsAuthenticated, ManageScriptsPerms]
permission_classes = [IsAuthenticated, ScriptsPerms]
def get(self, request, pk):
snippet = get_object_or_404(ScriptSnippet, pk=pk)
@@ -121,11 +121,11 @@ class GetUpdateDeleteScriptSnippet(APIView):
class TestScript(APIView):
permission_classes = [IsAuthenticated, RunScriptPerms]
def post(self, request):
def post(self, request, agent_id):
from .models import Script
from agents.models import Agent
agent = get_object_or_404(Agent, pk=request.data["agent"])
agent = get_object_or_404(Agent, agent_id=agent_id)
parsed_args = Script.parse_script_args(
agent, request.data["shell"], request.data["args"]
@@ -148,8 +148,8 @@ class TestScript(APIView):
return Response(r)
@api_view()
@permission_classes([IsAuthenticated, ManageScriptsPerms])
@api_view(["GET"])
@permission_classes([IsAuthenticated, ScriptsPerms])
def download(request, pk):
script = get_object_or_404(Script, pk=pk)

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,13 @@
from rest_framework import permissions
from tacticalrmm.permissions import _has_perm
from tacticalrmm.permissions import _has_perm, _has_perm_on_agent
class ManageWinSvcsPerms(permissions.BasePermission):
class WinSvcsPerms(permissions.BasePermission):
def has_permission(self, r, view):
return _has_perm(r, "can_manage_winsvcs")
if "agent_id" in view.kwargs.keys():
return _has_perm(r, "can_manage_winsvcs") and _has_perm_on_agent(
r.user, view.kwargs["agent_id"]
)
else:
return _has_perm(r, "can_manage_winsvcs")

View File

@@ -1,13 +0,0 @@
from rest_framework import serializers
from agents.models import Agent
class ServicesSerializer(serializers.ModelSerializer):
class Meta:
model = Agent
fields = (
"hostname",
"pk",
"services",
)

View File

@@ -5,28 +5,22 @@ from model_bakery import baker
from agents.models import Agent
from tacticalrmm.test import TacticalTestCase
base_url = "/services"
class TestServiceViews(TacticalTestCase):
def setUp(self):
self.authenticate()
self.setup_coresettings()
def test_default_services(self):
url = "/services/defaultservices/"
resp = self.client.get(url, format="json")
self.assertEqual(resp.status_code, 200)
self.assertEqual(type(resp.data), list)
self.check_not_authenticated("get", url)
@patch("agents.models.Agent.nats_cmd")
def test_get_services(self, nats_cmd):
# test a call where agent doesn't exist
resp = self.client.get("/services/500/services/", format="json")
resp = self.client.get("/services/500234hjk348982h/", format="json")
self.assertEqual(resp.status_code, 404)
agent = baker.make_recipe("agents.agent_with_services")
url = f"/services/{agent.pk}/services/"
url = f"{base_url}/{agent.agent_id}/"
nats_return = [
{
@@ -69,16 +63,16 @@ class TestServiceViews(TacticalTestCase):
@patch("agents.models.Agent.nats_cmd")
def test_service_action(self, nats_cmd):
url = "/services/serviceaction/"
invalid_data = {"pk": 500, "sv_name": "AeLookupSvc", "sv_action": "restart"}
data = {"sv_action": "restart"}
# test a call where agent doesn't exist
resp = self.client.post(url, invalid_data, format="json")
resp = self.client.post(
f"{base_url}/kjhj4hj4khj34h34j/AeLookupSvc/", data, format="json"
)
self.assertEqual(resp.status_code, 404)
agent = baker.make_recipe("agents.agent_with_services")
data = {"pk": agent.pk, "sv_name": "AeLookupSvc", "sv_action": "restart"}
url = f"/services/{agent.agent_id}/AeLookupSvc/"
# test failed attempt
nats_cmd.return_value = "timeout"
@@ -107,7 +101,7 @@ class TestServiceViews(TacticalTestCase):
def test_service_detail(self, nats_cmd):
# test a call where agent doesn't exist
resp = self.client.get(
"/services/500/doesntexist/servicedetail/", format="json"
f"{base_url}/34kjhj3h4jh3kjh34/service_name/", format="json"
)
self.assertEqual(resp.status_code, 404)
@@ -123,7 +117,7 @@ class TestServiceViews(TacticalTestCase):
}
agent = baker.make_recipe("agents.agent")
url = f"/services/{agent.pk}/alg/servicedetail/"
url = f"{base_url}/{agent.agent_id}/alg/"
# test failed attempt
nats_cmd.return_value = "timeout"
@@ -147,25 +141,25 @@ class TestServiceViews(TacticalTestCase):
@patch("agents.models.Agent.nats_cmd")
def test_edit_service(self, nats_cmd):
url = "/services/editservice/"
agent = baker.make_recipe("agents.agent_with_services")
url = f"{base_url}/{agent.agent_id}/AeLookupSvc/"
invalid_data = {"pk": 500, "sv_name": "AeLookupSvc", "edit_action": "autodelay"}
data = {"startType": "autodelay"}
# test a call where agent doesn't exist
resp = self.client.post(url, invalid_data, format="json")
resp = self.client.put(
f"{base_url}/234kjh2k3hkj23h4kj3h4k3jh/service/", data, format="json"
)
self.assertEqual(resp.status_code, 404)
data = {"pk": agent.pk, "sv_name": "AeLookupSvc", "edit_action": "autodelay"}
# test timeout
nats_cmd.return_value = "timeout"
resp = self.client.post(url, data, format="json")
resp = self.client.put(url, data, format="json")
self.assertEqual(resp.status_code, 400)
nats_cmd.reset_mock()
# test successful attempt autodelay
nats_cmd.return_value = {"success": True, "errormsg": ""}
resp = self.client.post(url, data, format="json")
resp = self.client.put(url, data, format="json")
self.assertEqual(resp.status_code, 200)
nats_cmd.assert_called_with(
{
@@ -180,20 +174,61 @@ class TestServiceViews(TacticalTestCase):
nats_cmd.reset_mock()
# test error message from agent
data = {"pk": agent.pk, "sv_name": "AeLookupSvc", "edit_action": "auto"}
data = {"startType": "auto"}
nats_cmd.return_value = {
"success": False,
"errormsg": "The parameter is incorrect",
}
resp = self.client.post(url, data, format="json")
resp = self.client.put(url, data, format="json")
self.assertEqual(resp.status_code, 400)
nats_cmd.reset_mock()
# test catch all
data = {"pk": agent.pk, "sv_name": "AeLookupSvc", "edit_action": "auto"}
nats_cmd.return_value = {"success": False, "errormsg": ""}
resp = self.client.post(url, data, format="json")
resp = self.client.put(url, data, format="json")
self.assertEqual(resp.status_code, 400)
self.assertEqual(resp.data, "Something went wrong")
self.check_not_authenticated("post", url)
self.check_not_authenticated("put", url)
class TestServicePermissions(TacticalTestCase):
def setUp(self):
self.setup_coresettings()
self.client_setup()
@patch("agents.models.Agent.nats_cmd", return_value="ok")
def test_services_permissions(self, nats_cmd):
agent = baker.make_recipe("agents.agent_with_services")
unauthorized_agent = baker.make_recipe("agents.agent_with_services")
test_data = [
{"url": f"{base_url}/{agent.agent_id}/", "method": "get"},
{"url": f"{base_url}/{agent.agent_id}/service_name/", "method": "get"},
{"url": f"{base_url}/{agent.agent_id}/service_name/", "method": "post"},
{"url": f"{base_url}/{agent.agent_id}/service_name/", "method": "put"},
]
for data in test_data:
# test superuser
self.check_authorized_superuser(data["method"], data["url"])
# test user with no roles
user = self.create_user_with_roles([])
self.client.force_authenticate(user=user)
self.check_not_authorized(data["method"], data["url"])
# test with correct role
user.role.can_manage_winsvcs = True
user.role.save()
self.check_authorized(data["method"], data["url"])
# test limiting user to client
user.role.can_view_clients.set([agent.client])
self.check_authorized(data["method"], data["url"])
user.role.can_view_clients.set([unauthorized_agent.client])
self.client.logout()

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