Compare commits
	
		
			659 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					62ec8c8f76 | ||
| 
						 | 
					b84d4a99b8 | ||
| 
						 | 
					cce9dfe585 | ||
| 
						 | 
					166be395b9 | ||
| 
						 | 
					fa3f5f8d68 | ||
| 
						 | 
					2926b68c32 | ||
| 
						 | 
					a55f187958 | ||
| 
						 | 
					c76d263375 | ||
| 
						 | 
					6740d97f8f | ||
| 
						 | 
					b079eebe79 | ||
| 
						 | 
					363e48a1e8 | ||
| 
						 | 
					f60e4e3e4f | ||
| 
						 | 
					1b02974efa | ||
| 
						 | 
					496abdd230 | ||
| 
						 | 
					bc495d77d1 | ||
| 
						 | 
					fb54d4bb64 | ||
| 
						 | 
					0786163dc3 | ||
| 
						 | 
					ed85611e75 | ||
| 
						 | 
					86ebfce44a | ||
| 
						 | 
					dae51cff51 | ||
| 
						 | 
					358a2e7220 | ||
| 
						 | 
					d45353e8c8 | ||
| 
						 | 
					2f56e4e3a1 | ||
| 
						 | 
					0e503f8273 | ||
| 
						 | 
					876fe803f5 | ||
| 
						 | 
					6adb9678b6 | ||
| 
						 | 
					39bf7ba4a9 | ||
| 
						 | 
					5da6e2ff99 | ||
| 
						 | 
					44603c41a2 | ||
| 
						 | 
					0feb982a73 | ||
| 
						 | 
					d93cb32f2e | ||
| 
						 | 
					40c47eace2 | ||
| 
						 | 
					509bdd879c | ||
| 
						 | 
					b98ebb6e9f | ||
| 
						 | 
					924ddecff0 | ||
| 
						 | 
					ca64fd218d | ||
| 
						 | 
					9b12b55acd | ||
| 
						 | 
					450239564a | ||
| 
						 | 
					bb1cc62d2a | ||
| 
						 | 
					b4875c1e2d | ||
| 
						 | 
					a21440d663 | ||
| 
						 | 
					eb6836b63c | ||
| 
						 | 
					b39a2690c1 | ||
| 
						 | 
					706902da1c | ||
| 
						 | 
					d5104b5d27 | ||
| 
						 | 
					a13ae5c4b1 | ||
| 
						 | 
					a92d1d9958 | ||
| 
						 | 
					10852a9427 | ||
| 
						 | 
					b757ce1e38 | ||
| 
						 | 
					91e75f3fa2 | ||
| 
						 | 
					6c8e55eb2f | ||
| 
						 | 
					f821f700fa | ||
| 
						 | 
					d76d24408f | ||
| 
						 | 
					7ad85dfe1c | ||
| 
						 | 
					7d8be0a719 | ||
| 
						 | 
					bac15c18e4 | ||
| 
						 | 
					2f266d39e6 | ||
| 
						 | 
					5726d1fc52 | ||
| 
						 | 
					69aee1823e | ||
| 
						 | 
					e6a0ae5f57 | ||
| 
						 | 
					e5df566c7a | ||
| 
						 | 
					81e173b609 | ||
| 
						 | 
					d0ebcc6606 | ||
| 
						 | 
					99c3fcf42a | ||
| 
						 | 
					794666e7cc | ||
| 
						 | 
					45abe4955d | ||
| 
						 | 
					7eed421c70 | ||
| 
						 | 
					69f7c397c2 | ||
| 
						 | 
					d2d136e922 | ||
| 
						 | 
					396e435ae0 | ||
| 
						 | 
					45d8e9102a | ||
| 
						 | 
					12a51deffa | ||
| 
						 | 
					f2f69abec2 | ||
| 
						 | 
					02b7f962e9 | ||
| 
						 | 
					eb813e6b22 | ||
| 
						 | 
					5ddc604341 | ||
| 
						 | 
					313e672e93 | ||
| 
						 | 
					ce77ad6de4 | ||
| 
						 | 
					bea22690b1 | ||
| 
						 | 
					c9a52bd7d0 | ||
| 
						 | 
					a244a341ec | ||
| 
						 | 
					2b47870032 | ||
| 
						 | 
					de9e35ae6a | ||
| 
						 | 
					1a6fec8ca9 | ||
| 
						 | 
					094054cd99 | ||
| 
						 | 
					f85b8a81f1 | ||
| 
						 | 
					a44eaebf7c | ||
| 
						 | 
					f37b3c063e | ||
| 
						 | 
					6e5d5a3b82 | ||
| 
						 | 
					bf0562d619 | ||
| 
						 | 
					ecaa81be3c | ||
| 
						 | 
					d98ae48935 | ||
| 
						 | 
					f52a76b16c | ||
| 
						 | 
					d421c27602 | ||
| 
						 | 
					70e4cd4de1 | ||
| 
						 | 
					29767e9265 | ||
| 
						 | 
					46d4c7f96d | ||
| 
						 | 
					161a6f3923 | ||
| 
						 | 
					53e912341b | ||
| 
						 | 
					19396ea11a | ||
| 
						 | 
					1d9a5e742b | ||
| 
						 | 
					e8dfdd03f7 | ||
| 
						 | 
					2f5b15dac7 | ||
| 
						 | 
					525e1f5136 | ||
| 
						 | 
					7d63d188af | ||
| 
						 | 
					87889c12ea | ||
| 
						 | 
					53d023f5ee | ||
| 
						 | 
					1877ab8c67 | ||
| 
						 | 
					72a5a8cab7 | ||
| 
						 | 
					221e49a978 | ||
| 
						 | 
					1a4c67d173 | ||
| 
						 | 
					42fd23ece3 | ||
| 
						 | 
					3035c0712a | ||
| 
						 | 
					61315f8bfd | ||
| 
						 | 
					52683124d8 | ||
| 
						 | 
					1f77390366 | ||
| 
						 | 
					322d492540 | ||
| 
						 | 
					f977d8cca9 | ||
| 
						 | 
					a9aedea2bd | ||
| 
						 | 
					5560bbeecb | ||
| 
						 | 
					f226206703 | ||
| 
						 | 
					170687226d | ||
| 
						 | 
					d56d3dc271 | ||
| 
						 | 
					32a202aff4 | ||
| 
						 | 
					6ee75e6e60 | ||
| 
						 | 
					13d74cae3b | ||
| 
						 | 
					88651916b0 | ||
| 
						 | 
					be12505d2f | ||
| 
						 | 
					23fcf3b045 | ||
| 
						 | 
					9e7459b204 | ||
| 
						 | 
					4f0eb1d566 | ||
| 
						 | 
					ce00481f47 | ||
| 
						 | 
					f596af90ba | ||
| 
						 | 
					5c74d1d021 | ||
| 
						 | 
					aff659b6b6 | ||
| 
						 | 
					58724d95fa | ||
| 
						 | 
					8d61fcd5c9 | ||
| 
						 | 
					3e1be53c36 | ||
| 
						 | 
					f3754588bd | ||
| 
						 | 
					c4ffffeec8 | ||
| 
						 | 
					5b69f6a358 | ||
| 
						 | 
					1af89a7447 | ||
| 
						 | 
					90abd81035 | ||
| 
						 | 
					898824b13f | ||
| 
						 | 
					9d093aa7f8 | ||
| 
						 | 
					1770549f6c | ||
| 
						 | 
					d21be77fd2 | ||
| 
						 | 
					41a1c19877 | ||
| 
						 | 
					9b6571ce68 | ||
| 
						 | 
					88e98e4e35 | ||
| 
						 | 
					10c56ffbfa | ||
| 
						 | 
					cb2c8d6f3c | ||
| 
						 | 
					ca62b850ce | ||
| 
						 | 
					5a75d4e140 | ||
| 
						 | 
					e0972b7c24 | ||
| 
						 | 
					0db497916d | ||
| 
						 | 
					23a0ad3c4e | ||
| 
						 | 
					2b4e1c4b67 | ||
| 
						 | 
					9b1b9244cf | ||
| 
						 | 
					ad570e9b16 | ||
| 
						 | 
					812ba6de62 | ||
| 
						 | 
					8f97124adb | ||
| 
						 | 
					28289838f9 | ||
| 
						 | 
					cca8a010c3 | ||
| 
						 | 
					91ab296692 | ||
| 
						 | 
					ee6c9c4272 | ||
| 
						 | 
					21cd36fa92 | ||
| 
						 | 
					b1aafe3dbc | ||
| 
						 | 
					5cd832de89 | ||
| 
						 | 
					24dd9d0518 | ||
| 
						 | 
					aab6ab810a | ||
| 
						 | 
					d1d6d5e71e | ||
| 
						 | 
					e67dd68522 | ||
| 
						 | 
					e25eae846d | ||
| 
						 | 
					995eeaa455 | ||
| 
						 | 
					240c61b967 | ||
| 
						 | 
					2d8b0753b4 | ||
| 
						 | 
					44eab3de7f | ||
| 
						 | 
					007be5bf95 | ||
| 
						 | 
					ee19c7c51f | ||
| 
						 | 
					ce56afbdf9 | ||
| 
						 | 
					51012695a1 | ||
| 
						 | 
					0eef2d2cc5 | ||
| 
						 | 
					487f9f2815 | ||
| 
						 | 
					d065adcd8e | ||
| 
						 | 
					0d9a1dc5eb | ||
| 
						 | 
					8f9ad15108 | ||
| 
						 | 
					e538e9b843 | ||
| 
						 | 
					4a702b6813 | ||
| 
						 | 
					1e6fd2c57a | ||
| 
						 | 
					600b959d89 | ||
| 
						 | 
					b96de9eb13 | ||
| 
						 | 
					93be19b647 | ||
| 
						 | 
					74f45f6f1d | ||
| 
						 | 
					54ba3d2888 | ||
| 
						 | 
					65d5149f60 | ||
| 
						 | 
					917ebb3771 | ||
| 
						 | 
					7e66b1f545 | ||
| 
						 | 
					05837dca35 | ||
| 
						 | 
					53be2ebe59 | ||
| 
						 | 
					0341efcaea | ||
| 
						 | 
					ec75210fd3 | ||
| 
						 | 
					e6afe3e806 | ||
| 
						 | 
					5aa46f068e | ||
| 
						 | 
					a11a5b28bc | ||
| 
						 | 
					907aa566ca | ||
| 
						 | 
					5c21f099a8 | ||
| 
						 | 
					b91201ae3e | ||
| 
						 | 
					56d7e19968 | ||
| 
						 | 
					cf91c6c90e | ||
| 
						 | 
					9011148adf | ||
| 
						 | 
					897d0590d2 | ||
| 
						 | 
					33b33e8458 | ||
| 
						 | 
					7758f5c187 | ||
| 
						 | 
					83d7a03ba4 | ||
| 
						 | 
					a9a0df9699 | ||
| 
						 | 
					df44f8f5f8 | ||
| 
						 | 
					216a9ed035 | ||
| 
						 | 
					35d61b6a6c | ||
| 
						 | 
					5fb72cea53 | ||
| 
						 | 
					d54d021e9f | ||
| 
						 | 
					06e78311df | ||
| 
						 | 
					df720f95ca | ||
| 
						 | 
					00faff34d3 | ||
| 
						 | 
					2b5b3ea4f3 | ||
| 
						 | 
					95e608d0b4 | ||
| 
						 | 
					1d55bf87dd | ||
| 
						 | 
					1220ce53eb | ||
| 
						 | 
					2006218f87 | ||
| 
						 | 
					40f427a387 | ||
| 
						 | 
					445e95baed | ||
| 
						 | 
					67fbc9ad33 | ||
| 
						 | 
					1253e9e465 | ||
| 
						 | 
					21069432e8 | ||
| 
						 | 
					6facf6a324 | ||
| 
						 | 
					7556197485 | ||
| 
						 | 
					8dddd2d896 | ||
| 
						 | 
					f319c95c2b | ||
| 
						 | 
					8e972b0907 | ||
| 
						 | 
					395e400215 | ||
| 
						 | 
					3685e3111f | ||
| 
						 | 
					7bb1c75dc6 | ||
| 
						 | 
					b20834929c | ||
| 
						 | 
					181891757e | ||
| 
						 | 
					b16feeae44 | ||
| 
						 | 
					684e049f27 | ||
| 
						 | 
					8cebd901b2 | ||
| 
						 | 
					3c96beb8fb | ||
| 
						 | 
					8a46459cf9 | ||
| 
						 | 
					be5c3e9daa | ||
| 
						 | 
					e44453877c | ||
| 
						 | 
					f772a4ec56 | ||
| 
						 | 
					44182ec683 | ||
| 
						 | 
					b9ab13fa53 | ||
| 
						 | 
					2ad6721c95 | ||
| 
						 | 
					b7d0604e62 | ||
| 
						 | 
					a7518b4b26 | ||
| 
						 | 
					50613f5d3e | ||
| 
						 | 
					f814767703 | ||
| 
						 | 
					4af86d6456 | ||
| 
						 | 
					f0a4f00c2d | ||
| 
						 | 
					4321affddb | ||
| 
						 | 
					926ed55b9b | ||
| 
						 | 
					2ebf308565 | ||
| 
						 | 
					1c5e736dce | ||
| 
						 | 
					b591f9f5b7 | ||
| 
						 | 
					9724882578 | ||
| 
						 | 
					ddef2df101 | ||
| 
						 | 
					8af69c4284 | ||
| 
						 | 
					6ebe1ab467 | ||
| 
						 | 
					24e4d9cf6d | ||
| 
						 | 
					f35fa0aa58 | ||
| 
						 | 
					4942f262f1 | ||
| 
						 | 
					a20b1a973e | ||
| 
						 | 
					eae5e00706 | ||
| 
						 | 
					403762d862 | ||
| 
						 | 
					5c92d4b454 | ||
| 
						 | 
					38179b9d38 | ||
| 
						 | 
					8f510dde5a | ||
| 
						 | 
					be42d56e37 | ||
| 
						 | 
					6294530fa3 | ||
| 
						 | 
					c5c8f5fab1 | ||
| 
						 | 
					3d41d79078 | ||
| 
						 | 
					3005061a11 | ||
| 
						 | 
					65ea46f457 | ||
| 
						 | 
					eca8f32570 | ||
| 
						 | 
					8d1ef19c61 | ||
| 
						 | 
					71d87d866b | ||
| 
						 | 
					c4f88bdce7 | ||
| 
						 | 
					f722a115b1 | ||
| 
						 | 
					1583beea7b | ||
| 
						 | 
					5b388c587b | ||
| 
						 | 
					e254923167 | ||
| 
						 | 
					b0dbdd7803 | ||
| 
						 | 
					aa6ebe0122 | ||
| 
						 | 
					c5f179bab8 | ||
| 
						 | 
					e65cb86638 | ||
| 
						 | 
					a349998640 | ||
| 
						 | 
					43f60610b8 | ||
| 
						 | 
					46d042087a | ||
| 
						 | 
					ee214727f6 | ||
| 
						 | 
					b4c1ec55ec | ||
| 
						 | 
					0fdd54f710 | ||
| 
						 | 
					4f0cdeaec0 | ||
| 
						 | 
					e5cc38857c | ||
| 
						 | 
					fe4b9d71c0 | ||
| 
						 | 
					5c1181e40e | ||
| 
						 | 
					8b71832bc2 | ||
| 
						 | 
					8412ed6065 | ||
| 
						 | 
					207f6cdc7c | ||
| 
						 | 
					b0b51f5730 | ||
| 
						 | 
					def6833ef0 | ||
| 
						 | 
					c528dd3de1 | ||
| 
						 | 
					544270e35d | ||
| 
						 | 
					657e029fee | ||
| 
						 | 
					49469d7689 | ||
| 
						 | 
					4f0dd452c8 | ||
| 
						 | 
					3f741eab11 | ||
| 
						 | 
					190368788f | ||
| 
						 | 
					8306a3f566 | ||
| 
						 | 
					988c134c09 | ||
| 
						 | 
					af0a4d578b | ||
| 
						 | 
					9bc0abc831 | ||
| 
						 | 
					41410e99e7 | ||
| 
						 | 
					deae04d5ff | ||
| 
						 | 
					7d6eeffd66 | ||
| 
						 | 
					629858e095 | ||
| 
						 | 
					dfdb628347 | ||
| 
						 | 
					6e48b28fc9 | ||
| 
						 | 
					3ba450e837 | ||
| 
						 | 
					688ed93500 | ||
| 
						 | 
					7268ba20a2 | ||
| 
						 | 
					63d9e73098 | ||
| 
						 | 
					564c048f90 | ||
| 
						 | 
					5f801c74d5 | ||
| 
						 | 
					b405fbc09a | ||
| 
						 | 
					7a64c2eb49 | ||
| 
						 | 
					c93cbac3b1 | ||
| 
						 | 
					8b0f67b8a6 | ||
| 
						 | 
					0d96129f2d | ||
| 
						 | 
					54ee12d2b3 | ||
| 
						 | 
					92fc042103 | ||
| 
						 | 
					9bb7016fa7 | ||
| 
						 | 
					3ad56feafb | ||
| 
						 | 
					14d59c3dec | ||
| 
						 | 
					443f419770 | ||
| 
						 | 
					ddbb58755e | ||
| 
						 | 
					524283b9ff | ||
| 
						 | 
					fb178d2944 | ||
| 
						 | 
					52f4ad9403 | ||
| 
						 | 
					ba0c08ef1f | ||
| 
						 | 
					9e19b1e04c | ||
| 
						 | 
					b2118201b1 | ||
| 
						 | 
					b4346aa056 | ||
| 
						 | 
					b599f05aab | ||
| 
						 | 
					93d78a0200 | ||
| 
						 | 
					449957b2eb | ||
| 
						 | 
					0a6d44bad3 | ||
| 
						 | 
					17ceaaa503 | ||
| 
						 | 
					d70803b416 | ||
| 
						 | 
					aa414d4702 | ||
| 
						 | 
					f24e1b91ea | ||
| 
						 | 
					1df8163090 | ||
| 
						 | 
					659ddf6a45 | ||
| 
						 | 
					e110068da4 | ||
| 
						 | 
					c943f6f936 | ||
| 
						 | 
					cb1fe7fe54 | ||
| 
						 | 
					593f1f63cc | ||
| 
						 | 
					66aa70cf75 | ||
| 
						 | 
					304be99067 | ||
| 
						 | 
					9a01ec35f4 | ||
| 
						 | 
					bfa5b4fba5 | ||
| 
						 | 
					d2f63ef353 | ||
| 
						 | 
					50f334425e | ||
| 
						 | 
					f78212073c | ||
| 
						 | 
					5c655f5a82 | ||
| 
						 | 
					6a6446bfcb | ||
| 
						 | 
					b60a3a5e50 | ||
| 
						 | 
					02ccbab8e5 | ||
| 
						 | 
					023ff3f964 | ||
| 
						 | 
					7c5e8df3b8 | ||
| 
						 | 
					56fdab260b | ||
| 
						 | 
					7cce49dc1a | ||
| 
						 | 
					2dfaafb20b | ||
| 
						 | 
					6138a5bf54 | ||
| 
						 | 
					828c67cc00 | ||
| 
						 | 
					e70cd44e18 | ||
| 
						 | 
					efa5ac5edd | ||
| 
						 | 
					788b11e759 | ||
| 
						 | 
					d049d7a61f | ||
| 
						 | 
					075c833b58 | ||
| 
						 | 
					e9309c2a96 | ||
| 
						 | 
					a592d2b397 | ||
| 
						 | 
					3ad1805ac0 | ||
| 
						 | 
					dbc2bab698 | ||
| 
						 | 
					79eec5c299 | ||
| 
						 | 
					7754b0c575 | ||
| 
						 | 
					be4289ce76 | ||
| 
						 | 
					67f5226270 | ||
| 
						 | 
					b6d77c581b | ||
| 
						 | 
					d84bf47d04 | ||
| 
						 | 
					aba3a7bb9e | ||
| 
						 | 
					6281736d89 | ||
| 
						 | 
					94d96f89d3 | ||
| 
						 | 
					4b55f9dead | ||
| 
						 | 
					5c6dce94df | ||
| 
						 | 
					f7d8f9c7f5 | ||
| 
						 | 
					053df24f9c | ||
| 
						 | 
					1dc470e434 | ||
| 
						 | 
					cfd8773267 | ||
| 
						 | 
					67045cf6c1 | ||
| 
						 | 
					ddfb9e7239 | ||
| 
						 | 
					9f6eed5472 | ||
| 
						 | 
					15a1e2ebcb | ||
| 
						 | 
					fcfe450b07 | ||
| 
						 | 
					a69bbb3bc9 | ||
| 
						 | 
					6d2559cfc1 | ||
| 
						 | 
					b3a62615f3 | ||
| 
						 | 
					57f5cca1cb | ||
| 
						 | 
					6b9851f540 | ||
| 
						 | 
					36fd203a88 | ||
| 
						 | 
					3f5cb5d61c | ||
| 
						 | 
					862fc6a946 | ||
| 
						 | 
					92c386ac0e | ||
| 
						 | 
					98a11a3645 | ||
| 
						 | 
					62be0ed936 | ||
| 
						 | 
					b7de73fd8a | ||
| 
						 | 
					e2413f1af2 | ||
| 
						 | 
					0e77d575c4 | ||
| 
						 | 
					ba42c5e367 | ||
| 
						 | 
					6a06734192 | ||
| 
						 | 
					5e26a406b7 | ||
| 
						 | 
					b6dd03138d | ||
| 
						 | 
					cf03ee03ee | ||
| 
						 | 
					0e665b6bf0 | ||
| 
						 | 
					e3d0de7313 | ||
| 
						 | 
					bcf3a543a1 | ||
| 
						 | 
					b27f17c74a | ||
| 
						 | 
					75d864771e | ||
| 
						 | 
					6420060f2a | ||
| 
						 | 
					c149ae71b9 | ||
| 
						 | 
					3a49dd034c | ||
| 
						 | 
					b26d7e82e3 | ||
| 
						 | 
					415abdf0ce | ||
| 
						 | 
					f7f6f6ecb2 | ||
| 
						 | 
					43d54f134a | ||
| 
						 | 
					0d2606a13b | ||
| 
						 | 
					1deb10dc88 | ||
| 
						 | 
					1236d55544 | ||
| 
						 | 
					ecccf39455 | ||
| 
						 | 
					8e0316825a | ||
| 
						 | 
					aa45fa87af | ||
| 
						 | 
					71e78bd0c5 | ||
| 
						 | 
					4766477c58 | ||
| 
						 | 
					d97e49ff2b | ||
| 
						 | 
					6b9d775cb9 | ||
| 
						 | 
					e521f580d7 | ||
| 
						 | 
					25e7cf7db0 | ||
| 
						 | 
					0cab33787d | ||
| 
						 | 
					bc6faf817f | ||
| 
						 | 
					d46ae55863 | ||
| 
						 | 
					bbd900ab25 | ||
| 
						 | 
					129ae93e2b | ||
| 
						 | 
					44dd59fa3f | ||
| 
						 | 
					ec4e7559b0 | ||
| 
						 | 
					dce40611cf | ||
| 
						 | 
					e71b8546f9 | ||
| 
						 | 
					f827348467 | ||
| 
						 | 
					f3978343db | ||
| 
						 | 
					2654a7ea70 | ||
| 
						 | 
					1068bf4ef7 | ||
| 
						 | 
					e7fccc97cc | ||
| 
						 | 
					733e289852 | ||
| 
						 | 
					29d71a104c | ||
| 
						 | 
					05200420ad | ||
| 
						 | 
					eb762d4bfd | ||
| 
						 | 
					58ace9eda1 | ||
| 
						 | 
					eeb2623be0 | ||
| 
						 | 
					cfa242c2fe | ||
| 
						 | 
					ec0441ccc2 | ||
| 
						 | 
					ae2782a8fe | ||
| 
						 | 
					58ff570251 | ||
| 
						 | 
					7b554b12c7 | ||
| 
						 | 
					58f7603d4f | ||
| 
						 | 
					8895994c54 | ||
| 
						 | 
					de8f7e36d5 | ||
| 
						 | 
					88d7a50265 | ||
| 
						 | 
					21e19fc7e5 | ||
| 
						 | 
					faf4935a69 | ||
| 
						 | 
					71a1f9d74a | ||
| 
						 | 
					bd8d523e10 | ||
| 
						 | 
					60cae0e3ac | ||
| 
						 | 
					5a342ac012 | ||
| 
						 | 
					bb8767dfc3 | ||
| 
						 | 
					fcb2779c15 | ||
| 
						 | 
					77dd6c1f61 | ||
| 
						 | 
					8118eef300 | ||
| 
						 | 
					802d1489fe | ||
| 
						 | 
					443a029185 | ||
| 
						 | 
					4ee508fdd0 | ||
| 
						 | 
					aa5608f7e8 | ||
| 
						 | 
					cc472b4613 | ||
| 
						 | 
					764b945ddc | ||
| 
						 | 
					fd2206ce4c | ||
| 
						 | 
					48c0ac9f00 | ||
| 
						 | 
					84eb4fe9ed | ||
| 
						 | 
					4a5428812c | ||
| 
						 | 
					023f98a89d | ||
| 
						 | 
					66893dd0c1 | ||
| 
						 | 
					25a6666e35 | ||
| 
						 | 
					19d75309b5 | ||
| 
						 | 
					11110d65c1 | ||
| 
						 | 
					a348f58fe2 | ||
| 
						 | 
					13851dd976 | ||
| 
						 | 
					2ec37c5da9 | ||
| 
						 | 
					8c127160de | ||
| 
						 | 
					2af820de9a | ||
| 
						 | 
					55fb0bb3a0 | ||
| 
						 | 
					9f9ecc521f | ||
| 
						 | 
					dfd01df5ba | ||
| 
						 | 
					474090698c | ||
| 
						 | 
					6b71cdeea4 | ||
| 
						 | 
					581e974236 | ||
| 
						 | 
					ba3c3a42ce | ||
| 
						 | 
					c8bc5671c5 | ||
| 
						 | 
					ff9401a040 | ||
| 
						 | 
					5e1bc1989f | ||
| 
						 | 
					a1dc91cd7d | ||
| 
						 | 
					99f2772bb3 | ||
| 
						 | 
					e5d0e42655 | ||
| 
						 | 
					2c914cc374 | ||
| 
						 | 
					9bceb62381 | ||
| 
						 | 
					de7518a800 | ||
| 
						 | 
					304fb63453 | ||
| 
						 | 
					0f7ef60ca0 | ||
| 
						 | 
					07c74e4641 | ||
| 
						 | 
					de7f325cfb | ||
| 
						 | 
					42cdf70cb4 | ||
| 
						 | 
					6beb6be131 | ||
| 
						 | 
					fa4fc2a708 | ||
| 
						 | 
					2db9758260 | ||
| 
						 | 
					715982e40a | ||
| 
						 | 
					d00cd4453a | ||
| 
						 | 
					429c08c24a | ||
| 
						 | 
					6a71490e20 | ||
| 
						 | 
					9bceda0646 | ||
| 
						 | 
					a1027a6773 | ||
| 
						 | 
					302d4b75f9 | ||
| 
						 | 
					5f6ee0e883 | ||
| 
						 | 
					27f9720de1 | ||
| 
						 | 
					22aa3fdbbc | ||
| 
						 | 
					069ecdd33f | ||
| 
						 | 
					dd545ae933 | ||
| 
						 | 
					6650b705c4 | ||
| 
						 | 
					59b0350289 | ||
| 
						 | 
					1ad159f820 | ||
| 
						 | 
					0bf42190e9 | ||
| 
						 | 
					d2fa836232 | ||
| 
						 | 
					c387774093 | ||
| 
						 | 
					e99736ba3c | ||
| 
						 | 
					16cb54fcc9 | ||
| 
						 | 
					5aa15c51ec | ||
| 
						 | 
					a8aedd9cf3 | ||
| 
						 | 
					b851b632bc | ||
| 
						 | 
					541e07fb65 | ||
| 
						 | 
					6ad16a897d | ||
| 
						 | 
					72f1053a93 | ||
| 
						 | 
					fb15a2762c | ||
| 
						 | 
					9165248b91 | ||
| 
						 | 
					add18b29db | ||
| 
						 | 
					1971653548 | ||
| 
						 | 
					392cd64d7b | ||
| 
						 | 
					b5affbb7c8 | ||
| 
						 | 
					71d1206277 | ||
| 
						 | 
					26e6a8c409 | ||
| 
						 | 
					eb54fae11a | ||
| 
						 | 
					ee773e5966 | ||
| 
						 | 
					7218ccdba8 | ||
| 
						 | 
					332400e48a | ||
| 
						 | 
					ad1a5d3702 | ||
| 
						 | 
					3006b4184d | ||
| 
						 | 
					84eb84a080 | ||
| 
						 | 
					60beea548b | ||
| 
						 | 
					5f9c149e59 | ||
| 
						 | 
					53367c6f04 | ||
| 
						 | 
					d7f817ee44 | ||
| 
						 | 
					d33a87da54 | ||
| 
						 | 
					3aebfb12b7 | ||
| 
						 | 
					1d6c55ffa6 | ||
| 
						 | 
					5e7080aac3 | ||
| 
						 | 
					fad739bc01 | ||
| 
						 | 
					c6b7f23884 | ||
| 
						 | 
					a6f7e446de | ||
| 
						 | 
					89d95d3ae1 | ||
| 
						 | 
					764208698f | ||
| 
						 | 
					57129cf934 | ||
| 
						 | 
					aae1a842d5 | ||
| 
						 | 
					623f35aec7 | ||
| 
						 | 
					870bf842cf | ||
| 
						 | 
					07f2d7dd5c | ||
| 
						 | 
					f223f2edc5 | ||
| 
						 | 
					e848a9a577 | ||
| 
						 | 
					7569d98e07 | ||
| 
						 | 
					596dee2f24 | ||
| 
						 | 
					9970403964 | ||
| 
						 | 
					07a88ae00d | ||
| 
						 | 
					5475b4d287 | ||
| 
						 | 
					6631dcfd3e | ||
| 
						 | 
					0dd3f337f3 | ||
| 
						 | 
					8eb27b5875 | ||
| 
						 | 
					2d1863031c | ||
| 
						 | 
					9feb76ca81 | ||
| 
						 | 
					993e8f4ab3 | ||
| 
						 | 
					e08ae95d4f | ||
| 
						 | 
					15359e8846 | ||
| 
						 | 
					d1457b312b | ||
| 
						 | 
					c9dd2af196 | ||
| 
						 | 
					564ef4e688 | ||
| 
						 | 
					a33e6e8bb5 | ||
| 
						 | 
					cf34f33f04 | ||
| 
						 | 
					827cfe4e8f | ||
| 
						 | 
					2ce1c2383c | ||
| 
						 | 
					6fc0a665ae | ||
| 
						 | 
					4f16d01263 | ||
| 
						 | 
					67cc37354a | ||
| 
						 | 
					e388243ef4 | ||
| 
						 | 
					3dc92763c7 | ||
| 
						 | 
					dfe97dd466 | ||
| 
						 | 
					2803cee29b | ||
| 
						 | 
					3a03020e54 | ||
| 
						 | 
					64443cc703 | ||
| 
						 | 
					4d1aa6ed18 | ||
| 
						 | 
					84837e88d2 | ||
| 
						 | 
					ff49c936ea | ||
| 
						 | 
					e6e0901329 | ||
| 
						 | 
					23b6284b51 | ||
| 
						 | 
					33dfbcbe32 | ||
| 
						 | 
					700c23d537 | ||
| 
						 | 
					369fac9e38 | ||
| 
						 | 
					2229eb1167 | ||
| 
						 | 
					a3dec841b6 | ||
| 
						 | 
					b17620bdb6 | ||
| 
						 | 
					f39cd5ae2f | ||
| 
						 | 
					83a19e005b | ||
| 
						 | 
					a9dd01b0c8 | ||
| 
						 | 
					eb59afa1d1 | ||
| 
						 | 
					2adcfce9d0 | ||
| 
						 | 
					314ab9b304 | ||
| 
						 | 
					8576fb82c7 | ||
| 
						 | 
					0f95a6bb2f | ||
| 
						 | 
					ad5104567d | ||
| 
						 | 
					ece68ba1d5 | ||
| 
						 | 
					acccd3a586 | ||
| 
						 | 
					8ebef1c1ca | ||
| 
						 | 
					28abc0d5ed | ||
| 
						 | 
					1efe25d3ec | ||
| 
						 | 
					c40e4f8e4b | ||
| 
						 | 
					baca84092d | ||
| 
						 | 
					346d4da059 | 
@@ -26,3 +26,6 @@ POSTGRES_PASS=postgrespass
 | 
			
		||||
APP_PORT=80
 | 
			
		||||
API_PORT=80
 | 
			
		||||
HTTP_PROTOCOL=https
 | 
			
		||||
DOCKER_NETWORK=172.21.0.0/24
 | 
			
		||||
DOCKER_NGINX_IP=172.21.0.20
 | 
			
		||||
NATS_PORTS=4222:4222
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
FROM python:3.9.2-slim
 | 
			
		||||
FROM python:3.9.6-slim
 | 
			
		||||
 | 
			
		||||
ENV TACTICAL_DIR /opt/tactical
 | 
			
		||||
ENV TACTICAL_READY_FILE ${TACTICAL_DIR}/tmp/tactical.ready
 | 
			
		||||
@@ -13,12 +13,17 @@ EXPOSE 8000 8383 8005
 | 
			
		||||
RUN groupadd -g 1000 tactical && \
 | 
			
		||||
    useradd -u 1000 -g 1000 tactical
 | 
			
		||||
 | 
			
		||||
# Copy Dev python reqs
 | 
			
		||||
COPY ./requirements.txt /
 | 
			
		||||
# Copy nats-api file
 | 
			
		||||
COPY natsapi/bin/nats-api /usr/local/bin/
 | 
			
		||||
RUN chmod +x /usr/local/bin/nats-api
 | 
			
		||||
 | 
			
		||||
# Copy Docker Entrypoint
 | 
			
		||||
COPY ./entrypoint.sh /
 | 
			
		||||
# Copy dev python reqs
 | 
			
		||||
COPY .devcontainer/requirements.txt  /
 | 
			
		||||
 | 
			
		||||
# Copy docker entrypoint.sh
 | 
			
		||||
COPY .devcontainer/entrypoint.sh /
 | 
			
		||||
RUN chmod +x /entrypoint.sh
 | 
			
		||||
 | 
			
		||||
ENTRYPOINT ["/entrypoint.sh"]
 | 
			
		||||
 | 
			
		||||
WORKDIR ${WORKSPACE_DIR}/api/tacticalrmm
 | 
			
		||||
 
 | 
			
		||||
@@ -6,8 +6,8 @@ services:
 | 
			
		||||
    image: api-dev
 | 
			
		||||
    restart: always
 | 
			
		||||
    build:
 | 
			
		||||
      context: .
 | 
			
		||||
      dockerfile: ./api.dockerfile
 | 
			
		||||
      context: ..
 | 
			
		||||
      dockerfile: .devcontainer/api.dockerfile
 | 
			
		||||
    command: ["tactical-api"]
 | 
			
		||||
    environment:
 | 
			
		||||
      API_PORT: ${API_PORT}
 | 
			
		||||
@@ -46,7 +46,7 @@ services:
 | 
			
		||||
      API_PORT: ${API_PORT}
 | 
			
		||||
      DEV: 1
 | 
			
		||||
    ports:
 | 
			
		||||
      - "4222:4222"
 | 
			
		||||
      - "${NATS_PORTS}"
 | 
			
		||||
    volumes:
 | 
			
		||||
      - tactical-data-dev:/opt/tactical
 | 
			
		||||
      - ..:/workspace:cached
 | 
			
		||||
@@ -67,7 +67,7 @@ services:
 | 
			
		||||
      MESH_PASS: ${MESH_PASS}
 | 
			
		||||
      MONGODB_USER: ${MONGODB_USER}
 | 
			
		||||
      MONGODB_PASSWORD: ${MONGODB_PASSWORD}
 | 
			
		||||
      NGINX_HOST_IP: 172.21.0.20
 | 
			
		||||
      NGINX_HOST_IP: ${DOCKER_NGINX_IP}
 | 
			
		||||
    networks:
 | 
			
		||||
      dev:
 | 
			
		||||
        aliases:
 | 
			
		||||
@@ -115,7 +115,10 @@ services:
 | 
			
		||||
  redis-dev:
 | 
			
		||||
    container_name: trmm-redis-dev
 | 
			
		||||
    restart: always
 | 
			
		||||
    command: redis-server --appendonly yes
 | 
			
		||||
    image: redis:6.0-alpine
 | 
			
		||||
    volumes: 
 | 
			
		||||
      - redis-data-dev:/data
 | 
			
		||||
    networks:
 | 
			
		||||
      dev:
 | 
			
		||||
        aliases:
 | 
			
		||||
@@ -124,9 +127,6 @@ services:
 | 
			
		||||
  init-dev:
 | 
			
		||||
    container_name: trmm-init-dev
 | 
			
		||||
    image: api-dev
 | 
			
		||||
    build:
 | 
			
		||||
      context: .
 | 
			
		||||
      dockerfile: ./api.dockerfile
 | 
			
		||||
    restart: on-failure
 | 
			
		||||
    command: ["tactical-init-dev"]
 | 
			
		||||
    environment:
 | 
			
		||||
@@ -153,9 +153,6 @@ services:
 | 
			
		||||
  celery-dev:
 | 
			
		||||
    container_name: trmm-celery-dev
 | 
			
		||||
    image: api-dev
 | 
			
		||||
    build:
 | 
			
		||||
      context: .
 | 
			
		||||
      dockerfile: ./api.dockerfile
 | 
			
		||||
    command: ["tactical-celery-dev"]
 | 
			
		||||
    restart: always
 | 
			
		||||
    networks:
 | 
			
		||||
@@ -171,9 +168,6 @@ services:
 | 
			
		||||
  celerybeat-dev:
 | 
			
		||||
    container_name: trmm-celerybeat-dev
 | 
			
		||||
    image: api-dev
 | 
			
		||||
    build:
 | 
			
		||||
      context: .
 | 
			
		||||
      dockerfile: ./api.dockerfile
 | 
			
		||||
    command: ["tactical-celerybeat-dev"]
 | 
			
		||||
    restart: always
 | 
			
		||||
    networks:
 | 
			
		||||
@@ -189,9 +183,6 @@ services:
 | 
			
		||||
  websockets-dev:
 | 
			
		||||
    container_name: trmm-websockets-dev
 | 
			
		||||
    image: api-dev
 | 
			
		||||
    build:
 | 
			
		||||
      context: .
 | 
			
		||||
      dockerfile: ./api.dockerfile
 | 
			
		||||
    command: ["tactical-websockets-dev"]
 | 
			
		||||
    restart: always
 | 
			
		||||
    networks:
 | 
			
		||||
@@ -218,9 +209,10 @@ services:
 | 
			
		||||
      CERT_PRIV_KEY: ${CERT_PRIV_KEY}
 | 
			
		||||
      APP_PORT: ${APP_PORT}
 | 
			
		||||
      API_PORT: ${API_PORT}
 | 
			
		||||
      DEV: 1
 | 
			
		||||
    networks:
 | 
			
		||||
      dev:
 | 
			
		||||
        ipv4_address: 172.21.0.20
 | 
			
		||||
        ipv4_address: ${DOCKER_NGINX_IP}
 | 
			
		||||
    ports:
 | 
			
		||||
      - "80:80"
 | 
			
		||||
      - "443:443"
 | 
			
		||||
@@ -231,9 +223,6 @@ services:
 | 
			
		||||
    container_name: trmm-mkdocs-dev
 | 
			
		||||
    image: api-dev
 | 
			
		||||
    restart: always
 | 
			
		||||
    build:
 | 
			
		||||
      context: .
 | 
			
		||||
      dockerfile: ./api.dockerfile
 | 
			
		||||
    command: ["tactical-mkdocs-dev"]
 | 
			
		||||
    ports:
 | 
			
		||||
      - "8005:8005"
 | 
			
		||||
@@ -247,6 +236,7 @@ volumes:
 | 
			
		||||
  postgres-data-dev:
 | 
			
		||||
  mongo-dev-data:
 | 
			
		||||
  mesh-data-dev:
 | 
			
		||||
  redis-data-dev:
 | 
			
		||||
 | 
			
		||||
networks:
 | 
			
		||||
  dev:
 | 
			
		||||
@@ -254,4 +244,4 @@ networks:
 | 
			
		||||
    ipam:
 | 
			
		||||
      driver: default
 | 
			
		||||
      config:
 | 
			
		||||
        - subnet: 172.21.0.0/24  
 | 
			
		||||
        - subnet: ${DOCKER_NETWORK}
 | 
			
		||||
 
 | 
			
		||||
@@ -78,24 +78,6 @@ DATABASES = {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
REST_FRAMEWORK = {
 | 
			
		||||
    'DATETIME_FORMAT': '%b-%d-%Y - %H:%M',
 | 
			
		||||
 | 
			
		||||
    'DEFAULT_PERMISSION_CLASSES': (
 | 
			
		||||
        'rest_framework.permissions.IsAuthenticated',
 | 
			
		||||
    ),
 | 
			
		||||
    'DEFAULT_AUTHENTICATION_CLASSES': (
 | 
			
		||||
        'knox.auth.TokenAuthentication',
 | 
			
		||||
    ),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
if not DEBUG:
 | 
			
		||||
    REST_FRAMEWORK.update({
 | 
			
		||||
        'DEFAULT_RENDERER_CLASSES': (
 | 
			
		||||
            'rest_framework.renderers.JSONRenderer',
 | 
			
		||||
        )
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
MESH_USERNAME = '${MESH_USER}'
 | 
			
		||||
MESH_SITE = 'https://${MESH_HOST}'
 | 
			
		||||
MESH_TOKEN_KEY = '${MESH_TOKEN}'
 | 
			
		||||
@@ -114,6 +96,7 @@ EOF
 | 
			
		||||
  "${VIRTUAL_ENV}"/bin/python manage.py load_chocos
 | 
			
		||||
  "${VIRTUAL_ENV}"/bin/python manage.py load_community_scripts
 | 
			
		||||
  "${VIRTUAL_ENV}"/bin/python manage.py reload_nats
 | 
			
		||||
  "${VIRTUAL_ENV}"/bin/python manage.py create_installer_user
 | 
			
		||||
 | 
			
		||||
  # create super user 
 | 
			
		||||
  echo "from accounts.models import User; User.objects.create_superuser('${TRMM_USER}', 'admin@example.com', '${TRMM_PASS}') if not User.objects.filter(username='${TRMM_USER}').exists() else 0;" | python manage.py shell
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,8 @@
 | 
			
		||||
asyncio-nats-client
 | 
			
		||||
celery
 | 
			
		||||
channels
 | 
			
		||||
channels_redis
 | 
			
		||||
django-ipware
 | 
			
		||||
Django
 | 
			
		||||
django-cors-headers
 | 
			
		||||
django-rest-knox
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -47,3 +47,5 @@ docs/.vuepress/dist
 | 
			
		||||
nats-rmm.conf
 | 
			
		||||
.mypy_cache
 | 
			
		||||
docs/site/
 | 
			
		||||
reset_db.sh
 | 
			
		||||
run_go_cmd.py
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@ Tactical RMM is a remote monitoring & management tool for Windows computers, bui
 | 
			
		||||
It uses an [agent](https://github.com/wh1te909/rmmagent) written in golang and integrates with [MeshCentral](https://github.com/Ylianst/MeshCentral)
 | 
			
		||||
 | 
			
		||||
# [LIVE DEMO](https://rmm.tacticalrmm.io/)
 | 
			
		||||
Demo database resets every hour. Alot of features are disabled for obvious reasons due to the nature of this app.
 | 
			
		||||
Demo database resets every hour. A lot of features are disabled for obvious reasons due to the nature of this app.
 | 
			
		||||
 | 
			
		||||
### [Discord Chat](https://discord.gg/upGTkWp)
 | 
			
		||||
 | 
			
		||||
@@ -35,4 +35,4 @@ Demo database resets every hour. Alot of features are disabled for obvious reaso
 | 
			
		||||
 | 
			
		||||
## Installation / Backup / Restore / Usage
 | 
			
		||||
 | 
			
		||||
### Refer to the [documentation](https://wh1te909.github.io/tacticalrmm/)
 | 
			
		||||
### Refer to the [documentation](https://wh1te909.github.io/tacticalrmm/)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,8 @@
 | 
			
		||||
from django.contrib import admin
 | 
			
		||||
from rest_framework.authtoken.admin import TokenAdmin
 | 
			
		||||
 | 
			
		||||
from .models import User
 | 
			
		||||
from .models import User, Role
 | 
			
		||||
 | 
			
		||||
admin.site.register(User)
 | 
			
		||||
TokenAdmin.raw_id_fields = ("user",)
 | 
			
		||||
admin.site.register(Role)
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,19 @@
 | 
			
		||||
import uuid
 | 
			
		||||
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
from accounts.models import User
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
    help = "Creates the installer user"
 | 
			
		||||
 | 
			
		||||
    def handle(self, *args, **kwargs):
 | 
			
		||||
        if User.objects.filter(is_installer_user=True).exists():
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        User.objects.create_user(  # type: ignore
 | 
			
		||||
            username=uuid.uuid4().hex,
 | 
			
		||||
            is_installer_user=True,
 | 
			
		||||
            password=User.objects.make_random_password(60),  # type: ignore
 | 
			
		||||
            block_dashboard_login=True,
 | 
			
		||||
        )
 | 
			
		||||
@@ -0,0 +1,25 @@
 | 
			
		||||
# Generated by Django 3.2.1 on 2021-05-07 15:26
 | 
			
		||||
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('core', '0022_urlaction'),
 | 
			
		||||
        ('accounts', '0015_user_loading_bar_color'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='url_action',
 | 
			
		||||
            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='user', to='core.urlaction'),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='agent_dblclick_action',
 | 
			
		||||
            field=models.CharField(choices=[('editagent', 'Edit Agent'), ('takecontrol', 'Take Control'), ('remotebg', 'Remote Background'), ('urlaction', 'URL Action')], default='editagent', max_length=50),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										173
									
								
								api/tacticalrmm/accounts/migrations/0017_auto_20210508_1716.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										173
									
								
								api/tacticalrmm/accounts/migrations/0017_auto_20210508_1716.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,173 @@
 | 
			
		||||
# Generated by Django 3.2.1 on 2021-05-08 17:16
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('accounts', '0016_auto_20210507_1526'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_code_sign',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_do_server_maint',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_edit_agent',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_edit_core_settings',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_install_agents',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_manage_accounts',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_manage_alerts',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_manage_automation_policies',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_manage_autotasks',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_manage_checks',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_manage_clients',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_manage_deployments',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_manage_notes',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_manage_pendingactions',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_manage_procs',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_manage_scripts',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_manage_sites',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_manage_software',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_manage_winsvcs',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_manage_winupdates',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_reboot_agents',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_run_autotasks',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_run_bulk',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_run_checks',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_run_scripts',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_send_cmd',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_uninstall_agents',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_update_agents',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_use_mesh',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_view_auditlogs',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_view_debuglogs',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_view_eventlogs',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										181
									
								
								api/tacticalrmm/accounts/migrations/0018_auto_20210511_0233.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										181
									
								
								api/tacticalrmm/accounts/migrations/0018_auto_20210511_0233.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,181 @@
 | 
			
		||||
# Generated by Django 3.2.1 on 2021-05-11 02:33
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('accounts', '0017_auto_20210508_1716'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='Role',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
			
		||||
                ('name', models.CharField(max_length=255, unique=True)),
 | 
			
		||||
                ('is_superuser', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_use_mesh', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_uninstall_agents', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_update_agents', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_edit_agent', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_manage_procs', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_view_eventlogs', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_send_cmd', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_reboot_agents', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_install_agents', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_run_scripts', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_run_bulk', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_manage_notes', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_edit_core_settings', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_do_server_maint', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_code_sign', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_manage_checks', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_run_checks', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_manage_clients', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_manage_sites', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_manage_deployments', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_manage_automation_policies', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_manage_autotasks', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_run_autotasks', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_view_auditlogs', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_manage_pendingactions', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_view_debuglogs', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_manage_scripts', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_manage_alerts', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_manage_winsvcs', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_manage_software', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_manage_winupdates', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_manage_accounts', models.BooleanField(default=False)),
 | 
			
		||||
            ],
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_code_sign',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_do_server_maint',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_edit_agent',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_edit_core_settings',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_install_agents',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_manage_accounts',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_manage_alerts',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_manage_automation_policies',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_manage_autotasks',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_manage_checks',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_manage_clients',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_manage_deployments',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_manage_notes',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_manage_pendingactions',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_manage_procs',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_manage_scripts',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_manage_sites',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_manage_software',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_manage_winsvcs',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_manage_winupdates',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_reboot_agents',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_run_autotasks',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_run_bulk',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_run_checks',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_run_scripts',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_send_cmd',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_uninstall_agents',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_update_agents',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_use_mesh',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_view_auditlogs',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_view_debuglogs',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_view_eventlogs',
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										25
									
								
								api/tacticalrmm/accounts/migrations/0019_user_role.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								api/tacticalrmm/accounts/migrations/0019_user_role.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,25 @@
 | 
			
		||||
# Generated by Django 3.2.1 on 2021-05-11 02:33
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("accounts", "0018_auto_20210511_0233"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name="user",
 | 
			
		||||
            name="role",
 | 
			
		||||
            field=models.ForeignKey(
 | 
			
		||||
                blank=True,
 | 
			
		||||
                null=True,
 | 
			
		||||
                on_delete=django.db.models.deletion.SET_NULL,
 | 
			
		||||
                related_name="roles",
 | 
			
		||||
                to="accounts.role",
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -0,0 +1,18 @@
 | 
			
		||||
# Generated by Django 3.2.1 on 2021-05-11 17:37
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('accounts', '0019_user_role'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='role',
 | 
			
		||||
            name='can_manage_roles',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -0,0 +1,18 @@
 | 
			
		||||
# Generated by Django 3.2.4 on 2021-06-17 04:29
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('accounts', '0020_role_can_manage_roles'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='role',
 | 
			
		||||
            name='can_view_core_settings',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -0,0 +1,18 @@
 | 
			
		||||
# Generated by Django 3.2.4 on 2021-06-28 05:01
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('accounts', '0021_role_can_view_core_settings'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='clear_search_when_switching',
 | 
			
		||||
            field=models.BooleanField(default=True),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -0,0 +1,18 @@
 | 
			
		||||
# Generated by Django 3.2.4 on 2021-06-30 03:22
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('accounts', '0022_user_clear_search_when_switching'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='is_installer_user',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -0,0 +1,18 @@
 | 
			
		||||
# Generated by Django 3.2.1 on 2021-07-20 20:26
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('accounts', '0023_user_is_installer_user'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='last_login_ip',
 | 
			
		||||
            field=models.GenericIPAddressField(blank=True, default=None, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -0,0 +1,33 @@
 | 
			
		||||
# Generated by Django 3.2.1 on 2021-07-21 04:24
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('accounts', '0024_user_last_login_ip'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='role',
 | 
			
		||||
            name='created_by',
 | 
			
		||||
            field=models.CharField(blank=True, max_length=100, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='role',
 | 
			
		||||
            name='created_time',
 | 
			
		||||
            field=models.DateTimeField(auto_now_add=True, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='role',
 | 
			
		||||
            name='modified_by',
 | 
			
		||||
            field=models.CharField(blank=True, max_length=100, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='role',
 | 
			
		||||
            name='modified_time',
 | 
			
		||||
            field=models.DateTimeField(auto_now=True, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -0,0 +1,34 @@
 | 
			
		||||
# Generated by Django 3.2.6 on 2021-09-01 12:47
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('accounts', '0025_auto_20210721_0424'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='APIKey',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
			
		||||
                ('created_by', models.CharField(blank=True, max_length=100, null=True)),
 | 
			
		||||
                ('created_time', models.DateTimeField(auto_now_add=True, null=True)),
 | 
			
		||||
                ('modified_by', models.CharField(blank=True, max_length=100, null=True)),
 | 
			
		||||
                ('modified_time', models.DateTimeField(auto_now=True, null=True)),
 | 
			
		||||
                ('name', models.CharField(max_length=25, unique=True)),
 | 
			
		||||
                ('key', models.CharField(blank=True, max_length=48, unique=True)),
 | 
			
		||||
                ('expiration', models.DateTimeField(blank=True, default=None, null=True)),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                'abstract': False,
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='role',
 | 
			
		||||
            name='can_manage_api_keys',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -0,0 +1,25 @@
 | 
			
		||||
# Generated by Django 3.2.6 on 2021-09-03 00:54
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('accounts', '0026_auto_20210901_1247'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='apikey',
 | 
			
		||||
            name='user',
 | 
			
		||||
            field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='api_key', to='accounts.user'),
 | 
			
		||||
            preserve_default=False,
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='block_dashboard_login',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										150
									
								
								api/tacticalrmm/accounts/migrations/0028_auto_20211010_0249.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										150
									
								
								api/tacticalrmm/accounts/migrations/0028_auto_20211010_0249.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,150 @@
 | 
			
		||||
# Generated by Django 3.2.6 on 2021-10-10 02:49
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('clients', '0018_auto_20211010_0249'),
 | 
			
		||||
        ('accounts', '0027_auto_20210903_0054'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='role',
 | 
			
		||||
            name='can_list_accounts',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='role',
 | 
			
		||||
            name='can_list_agent_history',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='role',
 | 
			
		||||
            name='can_list_agents',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='role',
 | 
			
		||||
            name='can_list_alerts',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='role',
 | 
			
		||||
            name='can_list_api_keys',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='role',
 | 
			
		||||
            name='can_list_automation_policies',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='role',
 | 
			
		||||
            name='can_list_autotasks',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='role',
 | 
			
		||||
            name='can_list_checks',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='role',
 | 
			
		||||
            name='can_list_clients',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='role',
 | 
			
		||||
            name='can_list_deployments',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='role',
 | 
			
		||||
            name='can_list_notes',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='role',
 | 
			
		||||
            name='can_list_pendingactions',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='role',
 | 
			
		||||
            name='can_list_roles',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='role',
 | 
			
		||||
            name='can_list_scripts',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='role',
 | 
			
		||||
            name='can_list_sites',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='role',
 | 
			
		||||
            name='can_list_software',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='role',
 | 
			
		||||
            name='can_ping_agents',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='role',
 | 
			
		||||
            name='can_recover_agents',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='role',
 | 
			
		||||
            name='can_view_clients',
 | 
			
		||||
            field=models.ManyToManyField(blank=True, related_name='role_clients', to='clients.Client'),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='role',
 | 
			
		||||
            name='can_view_sites',
 | 
			
		||||
            field=models.ManyToManyField(blank=True, related_name='role_sites', to='clients.Site'),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='apikey',
 | 
			
		||||
            name='created_by',
 | 
			
		||||
            field=models.CharField(blank=True, max_length=255, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='apikey',
 | 
			
		||||
            name='modified_by',
 | 
			
		||||
            field=models.CharField(blank=True, max_length=255, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='role',
 | 
			
		||||
            name='created_by',
 | 
			
		||||
            field=models.CharField(blank=True, max_length=255, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='role',
 | 
			
		||||
            name='modified_by',
 | 
			
		||||
            field=models.CharField(blank=True, max_length=255, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='created_by',
 | 
			
		||||
            field=models.CharField(blank=True, max_length=255, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='modified_by',
 | 
			
		||||
            field=models.CharField(blank=True, max_length=255, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='role',
 | 
			
		||||
            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='users', to='accounts.role'),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -0,0 +1,28 @@
 | 
			
		||||
# Generated by Django 3.2.6 on 2021-10-22 22:45
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('accounts', '0028_auto_20211010_0249'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='role',
 | 
			
		||||
            name='can_list_alerttemplates',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='role',
 | 
			
		||||
            name='can_manage_alerttemplates',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='role',
 | 
			
		||||
            name='can_run_urlactions',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -0,0 +1,23 @@
 | 
			
		||||
# Generated by Django 3.2.6 on 2021-11-04 02:21
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('accounts', '0029_auto_20211022_2245'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='role',
 | 
			
		||||
            name='can_manage_customfields',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='role',
 | 
			
		||||
            name='can_view_customfields',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
from django.contrib.auth.models import AbstractUser
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.db.models.fields import CharField, DateTimeField
 | 
			
		||||
 | 
			
		||||
from logs.models import BaseAuditModel
 | 
			
		||||
 | 
			
		||||
@@ -7,6 +8,7 @@ AGENT_DBLCLICK_CHOICES = [
 | 
			
		||||
    ("editagent", "Edit Agent"),
 | 
			
		||||
    ("takecontrol", "Take Control"),
 | 
			
		||||
    ("remotebg", "Remote Background"),
 | 
			
		||||
    ("urlaction", "URL Action"),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
AGENT_TBL_TAB_CHOICES = [
 | 
			
		||||
@@ -23,12 +25,20 @@ CLIENT_TREE_SORT_CHOICES = [
 | 
			
		||||
 | 
			
		||||
class User(AbstractUser, BaseAuditModel):
 | 
			
		||||
    is_active = models.BooleanField(default=True)
 | 
			
		||||
    block_dashboard_login = models.BooleanField(default=False)
 | 
			
		||||
    totp_key = models.CharField(max_length=50, null=True, blank=True)
 | 
			
		||||
    dark_mode = models.BooleanField(default=True)
 | 
			
		||||
    show_community_scripts = models.BooleanField(default=True)
 | 
			
		||||
    agent_dblclick_action = models.CharField(
 | 
			
		||||
        max_length=50, choices=AGENT_DBLCLICK_CHOICES, default="editagent"
 | 
			
		||||
    )
 | 
			
		||||
    url_action = models.ForeignKey(
 | 
			
		||||
        "core.URLAction",
 | 
			
		||||
        related_name="user",
 | 
			
		||||
        null=True,
 | 
			
		||||
        blank=True,
 | 
			
		||||
        on_delete=models.SET_NULL,
 | 
			
		||||
    )
 | 
			
		||||
    default_agent_tbl_tab = models.CharField(
 | 
			
		||||
        max_length=50, choices=AGENT_TBL_TAB_CHOICES, default="server"
 | 
			
		||||
    )
 | 
			
		||||
@@ -38,6 +48,9 @@ class User(AbstractUser, BaseAuditModel):
 | 
			
		||||
    )
 | 
			
		||||
    client_tree_splitter = models.PositiveIntegerField(default=11)
 | 
			
		||||
    loading_bar_color = models.CharField(max_length=255, default="red")
 | 
			
		||||
    clear_search_when_switching = models.BooleanField(default=True)
 | 
			
		||||
    is_installer_user = models.BooleanField(default=False)
 | 
			
		||||
    last_login_ip = models.GenericIPAddressField(default=None, blank=True, null=True)
 | 
			
		||||
 | 
			
		||||
    agent = models.OneToOneField(
 | 
			
		||||
        "agents.Agent",
 | 
			
		||||
@@ -47,9 +60,141 @@ class User(AbstractUser, BaseAuditModel):
 | 
			
		||||
        on_delete=models.CASCADE,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    role = models.ForeignKey(
 | 
			
		||||
        "accounts.Role",
 | 
			
		||||
        null=True,
 | 
			
		||||
        blank=True,
 | 
			
		||||
        related_name="users",
 | 
			
		||||
        on_delete=models.SET_NULL,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def serialize(user):
 | 
			
		||||
        # serializes the task and returns json
 | 
			
		||||
        from .serializers import UserSerializer
 | 
			
		||||
 | 
			
		||||
        return UserSerializer(user).data
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Role(BaseAuditModel):
 | 
			
		||||
    name = models.CharField(max_length=255, unique=True)
 | 
			
		||||
    is_superuser = models.BooleanField(default=False)
 | 
			
		||||
 | 
			
		||||
    # agents
 | 
			
		||||
    can_list_agents = models.BooleanField(default=False)
 | 
			
		||||
    can_ping_agents = models.BooleanField(default=False)
 | 
			
		||||
    can_use_mesh = models.BooleanField(default=False)
 | 
			
		||||
    can_uninstall_agents = models.BooleanField(default=False)
 | 
			
		||||
    can_update_agents = models.BooleanField(default=False)
 | 
			
		||||
    can_edit_agent = models.BooleanField(default=False)
 | 
			
		||||
    can_manage_procs = models.BooleanField(default=False)
 | 
			
		||||
    can_view_eventlogs = models.BooleanField(default=False)
 | 
			
		||||
    can_send_cmd = models.BooleanField(default=False)
 | 
			
		||||
    can_reboot_agents = models.BooleanField(default=False)
 | 
			
		||||
    can_install_agents = models.BooleanField(default=False)
 | 
			
		||||
    can_run_scripts = models.BooleanField(default=False)
 | 
			
		||||
    can_run_bulk = models.BooleanField(default=False)
 | 
			
		||||
    can_recover_agents = models.BooleanField(default=False)
 | 
			
		||||
    can_list_agent_history = models.BooleanField(default=False)
 | 
			
		||||
 | 
			
		||||
    # core
 | 
			
		||||
    can_list_notes = models.BooleanField(default=False)
 | 
			
		||||
    can_manage_notes = models.BooleanField(default=False)
 | 
			
		||||
    can_view_core_settings = models.BooleanField(default=False)
 | 
			
		||||
    can_edit_core_settings = models.BooleanField(default=False)
 | 
			
		||||
    can_do_server_maint = models.BooleanField(default=False)
 | 
			
		||||
    can_code_sign = models.BooleanField(default=False)
 | 
			
		||||
    can_run_urlactions = models.BooleanField(default=False)
 | 
			
		||||
    can_view_customfields = models.BooleanField(default=False)
 | 
			
		||||
    can_manage_customfields = models.BooleanField(default=False)
 | 
			
		||||
 | 
			
		||||
    # checks
 | 
			
		||||
    can_list_checks = models.BooleanField(default=False)
 | 
			
		||||
    can_manage_checks = models.BooleanField(default=False)
 | 
			
		||||
    can_run_checks = models.BooleanField(default=False)
 | 
			
		||||
 | 
			
		||||
    # clients
 | 
			
		||||
    can_list_clients = models.BooleanField(default=False)
 | 
			
		||||
    can_manage_clients = models.BooleanField(default=False)
 | 
			
		||||
    can_list_sites = models.BooleanField(default=False)
 | 
			
		||||
    can_manage_sites = models.BooleanField(default=False)
 | 
			
		||||
    can_list_deployments = models.BooleanField(default=False)
 | 
			
		||||
    can_manage_deployments = models.BooleanField(default=False)
 | 
			
		||||
    can_view_clients = models.ManyToManyField(
 | 
			
		||||
        "clients.Client", related_name="role_clients", blank=True
 | 
			
		||||
    )
 | 
			
		||||
    can_view_sites = models.ManyToManyField(
 | 
			
		||||
        "clients.Site", related_name="role_sites", blank=True
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    # automation
 | 
			
		||||
    can_list_automation_policies = models.BooleanField(default=False)
 | 
			
		||||
    can_manage_automation_policies = models.BooleanField(default=False)
 | 
			
		||||
 | 
			
		||||
    # automated tasks
 | 
			
		||||
    can_list_autotasks = models.BooleanField(default=False)
 | 
			
		||||
    can_manage_autotasks = models.BooleanField(default=False)
 | 
			
		||||
    can_run_autotasks = models.BooleanField(default=False)
 | 
			
		||||
 | 
			
		||||
    # logs
 | 
			
		||||
    can_view_auditlogs = models.BooleanField(default=False)
 | 
			
		||||
    can_list_pendingactions = models.BooleanField(default=False)
 | 
			
		||||
    can_manage_pendingactions = models.BooleanField(default=False)
 | 
			
		||||
    can_view_debuglogs = models.BooleanField(default=False)
 | 
			
		||||
 | 
			
		||||
    # scripts
 | 
			
		||||
    can_list_scripts = models.BooleanField(default=False)
 | 
			
		||||
    can_manage_scripts = models.BooleanField(default=False)
 | 
			
		||||
 | 
			
		||||
    # alerts
 | 
			
		||||
    can_list_alerts = models.BooleanField(default=False)
 | 
			
		||||
    can_manage_alerts = models.BooleanField(default=False)
 | 
			
		||||
    can_list_alerttemplates = models.BooleanField(default=False)
 | 
			
		||||
    can_manage_alerttemplates = models.BooleanField(default=False)
 | 
			
		||||
 | 
			
		||||
    # win services
 | 
			
		||||
    can_manage_winsvcs = models.BooleanField(default=False)
 | 
			
		||||
 | 
			
		||||
    # software
 | 
			
		||||
    can_list_software = models.BooleanField(default=False)
 | 
			
		||||
    can_manage_software = models.BooleanField(default=False)
 | 
			
		||||
 | 
			
		||||
    # windows updates
 | 
			
		||||
    can_manage_winupdates = models.BooleanField(default=False)
 | 
			
		||||
 | 
			
		||||
    # accounts
 | 
			
		||||
    can_list_accounts = models.BooleanField(default=False)
 | 
			
		||||
    can_manage_accounts = models.BooleanField(default=False)
 | 
			
		||||
    can_list_roles = models.BooleanField(default=False)
 | 
			
		||||
    can_manage_roles = models.BooleanField(default=False)
 | 
			
		||||
 | 
			
		||||
    # authentication
 | 
			
		||||
    can_list_api_keys = models.BooleanField(default=False)
 | 
			
		||||
    can_manage_api_keys = models.BooleanField(default=False)
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return self.name
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def serialize(role):
 | 
			
		||||
        # serializes the agent and returns json
 | 
			
		||||
        from .serializers import RoleAuditSerializer
 | 
			
		||||
 | 
			
		||||
        return RoleAuditSerializer(role).data
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class APIKey(BaseAuditModel):
 | 
			
		||||
    name = CharField(unique=True, max_length=25)
 | 
			
		||||
    key = CharField(unique=True, blank=True, max_length=48)
 | 
			
		||||
    expiration = DateTimeField(blank=True, null=True, default=None)
 | 
			
		||||
    user = models.ForeignKey(
 | 
			
		||||
        "accounts.User",
 | 
			
		||||
        related_name="api_key",
 | 
			
		||||
        on_delete=models.CASCADE,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def serialize(apikey):
 | 
			
		||||
        from .serializers import APIKeyAuditSerializer
 | 
			
		||||
 | 
			
		||||
        return APIKeyAuditSerializer(apikey).data
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										43
									
								
								api/tacticalrmm/accounts/permissions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								api/tacticalrmm/accounts/permissions.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,43 @@
 | 
			
		||||
from rest_framework import permissions
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.permissions import _has_perm
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AccountsPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        if r.method == "GET":
 | 
			
		||||
            return _has_perm(r, "can_list_accounts")
 | 
			
		||||
        else:
 | 
			
		||||
 | 
			
		||||
            # allow users to reset their own password/2fa see issue #686
 | 
			
		||||
            base_path = "/accounts/users/"
 | 
			
		||||
            paths = ["reset/", "reset_totp/"]
 | 
			
		||||
 | 
			
		||||
            if r.path in [base_path + i for i in paths]:
 | 
			
		||||
                from accounts.models import User
 | 
			
		||||
 | 
			
		||||
                try:
 | 
			
		||||
                    user = User.objects.get(pk=r.data["id"])
 | 
			
		||||
                except User.DoesNotExist:
 | 
			
		||||
                    pass
 | 
			
		||||
                else:
 | 
			
		||||
                    if user == r.user:
 | 
			
		||||
                        return True
 | 
			
		||||
 | 
			
		||||
            return _has_perm(r, "can_manage_accounts")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RolesPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        if r.method == "GET":
 | 
			
		||||
            return _has_perm(r, "can_list_roles")
 | 
			
		||||
        else:
 | 
			
		||||
            return _has_perm(r, "can_manage_roles")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class APIKeyPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        if r.method == "GET":
 | 
			
		||||
            return _has_perm(r, "can_list_api_keys")
 | 
			
		||||
 | 
			
		||||
        return _has_perm(r, "can_manage_api_keys")
 | 
			
		||||
@@ -1,7 +1,11 @@
 | 
			
		||||
import pyotp
 | 
			
		||||
from rest_framework.serializers import ModelSerializer, SerializerMethodField
 | 
			
		||||
from rest_framework.serializers import (
 | 
			
		||||
    ModelSerializer,
 | 
			
		||||
    SerializerMethodField,
 | 
			
		||||
    ReadOnlyField,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
from .models import User
 | 
			
		||||
from .models import APIKey, User, Role
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UserUISerializer(ModelSerializer):
 | 
			
		||||
@@ -11,17 +15,20 @@ class UserUISerializer(ModelSerializer):
 | 
			
		||||
            "dark_mode",
 | 
			
		||||
            "show_community_scripts",
 | 
			
		||||
            "agent_dblclick_action",
 | 
			
		||||
            "url_action",
 | 
			
		||||
            "default_agent_tbl_tab",
 | 
			
		||||
            "client_tree_sort",
 | 
			
		||||
            "client_tree_splitter",
 | 
			
		||||
            "loading_bar_color",
 | 
			
		||||
            "clear_search_when_switching",
 | 
			
		||||
            "block_dashboard_login",
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UserSerializer(ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = User
 | 
			
		||||
        fields = (
 | 
			
		||||
        fields = [
 | 
			
		||||
            "id",
 | 
			
		||||
            "username",
 | 
			
		||||
            "first_name",
 | 
			
		||||
@@ -29,7 +36,10 @@ class UserSerializer(ModelSerializer):
 | 
			
		||||
            "email",
 | 
			
		||||
            "is_active",
 | 
			
		||||
            "last_login",
 | 
			
		||||
        )
 | 
			
		||||
            "last_login_ip",
 | 
			
		||||
            "role",
 | 
			
		||||
            "block_dashboard_login",
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TOTPSetupSerializer(ModelSerializer):
 | 
			
		||||
@@ -48,3 +58,41 @@ class TOTPSetupSerializer(ModelSerializer):
 | 
			
		||||
        return pyotp.totp.TOTP(obj.totp_key).provisioning_uri(
 | 
			
		||||
            obj.username, issuer_name="Tactical RMM"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RoleSerializer(ModelSerializer):
 | 
			
		||||
    user_count = SerializerMethodField()
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Role
 | 
			
		||||
        fields = "__all__"
 | 
			
		||||
 | 
			
		||||
    def get_user_count(self, obj):
 | 
			
		||||
        return obj.users.count()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RoleAuditSerializer(ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Role
 | 
			
		||||
        fields = "__all__"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class APIKeySerializer(ModelSerializer):
 | 
			
		||||
 | 
			
		||||
    username = ReadOnlyField(source="user.username")
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = APIKey
 | 
			
		||||
        fields = "__all__"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class APIKeyAuditSerializer(ModelSerializer):
 | 
			
		||||
    username = ReadOnlyField(source="user.username")
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = APIKey
 | 
			
		||||
        fields = [
 | 
			
		||||
            "name",
 | 
			
		||||
            "username",
 | 
			
		||||
            "expiration",
 | 
			
		||||
        ]
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,12 @@
 | 
			
		||||
from unittest.mock import patch
 | 
			
		||||
 | 
			
		||||
from django.test import override_settings
 | 
			
		||||
 | 
			
		||||
from accounts.models import User
 | 
			
		||||
from model_bakery import baker, seq
 | 
			
		||||
from accounts.models import User, APIKey
 | 
			
		||||
from tacticalrmm.test import TacticalTestCase
 | 
			
		||||
 | 
			
		||||
from accounts.serializers import APIKeySerializer
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestAccounts(TacticalTestCase):
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
@@ -25,12 +27,12 @@ class TestAccounts(TacticalTestCase):
 | 
			
		||||
        data = {"username": "bob", "password": "a3asdsa2314"}
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 400)
 | 
			
		||||
        self.assertEqual(r.data, "bad credentials")
 | 
			
		||||
        self.assertEqual(r.data, "Bad credentials")
 | 
			
		||||
 | 
			
		||||
        data = {"username": "billy", "password": "hunter2"}
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 400)
 | 
			
		||||
        self.assertEqual(r.data, "bad credentials")
 | 
			
		||||
        self.assertEqual(r.data, "Bad credentials")
 | 
			
		||||
 | 
			
		||||
        self.bob.totp_key = "AB5RI6YPFTZAS52G"
 | 
			
		||||
        self.bob.save()
 | 
			
		||||
@@ -39,6 +41,12 @@ class TestAccounts(TacticalTestCase):
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(r.data, "ok")
 | 
			
		||||
 | 
			
		||||
        # test user set to block dashboard logins
 | 
			
		||||
        self.bob.block_dashboard_login = True
 | 
			
		||||
        self.bob.save()
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 400)
 | 
			
		||||
 | 
			
		||||
    @patch("pyotp.TOTP.verify")
 | 
			
		||||
    def test_login_view(self, mock_verify):
 | 
			
		||||
        url = "/login/"
 | 
			
		||||
@@ -53,7 +61,7 @@ class TestAccounts(TacticalTestCase):
 | 
			
		||||
        mock_verify.return_value = False
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 400)
 | 
			
		||||
        self.assertEqual(r.data, "bad credentials")
 | 
			
		||||
        self.assertEqual(r.data, "Bad credentials")
 | 
			
		||||
 | 
			
		||||
        mock_verify.return_value = True
 | 
			
		||||
        data = {"username": "bob", "password": "asd234234asd", "twofactor": "123456"}
 | 
			
		||||
@@ -280,6 +288,7 @@ class TestUserAction(TacticalTestCase):
 | 
			
		||||
            "client_tree_sort": "alpha",
 | 
			
		||||
            "client_tree_splitter": 14,
 | 
			
		||||
            "loading_bar_color": "green",
 | 
			
		||||
            "clear_search_when_switching": False,
 | 
			
		||||
        }
 | 
			
		||||
        r = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
@@ -287,6 +296,68 @@ class TestUserAction(TacticalTestCase):
 | 
			
		||||
        self.check_not_authenticated("patch", url)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestAPIKeyViews(TacticalTestCase):
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        self.setup_coresettings()
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
 | 
			
		||||
    def test_get_api_keys(self):
 | 
			
		||||
        url = "/accounts/apikeys/"
 | 
			
		||||
        apikeys = baker.make("accounts.APIKey", key=seq("APIKEY"), _quantity=3)
 | 
			
		||||
 | 
			
		||||
        serializer = APIKeySerializer(apikeys, many=True)
 | 
			
		||||
        resp = self.client.get(url, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertEqual(serializer.data, resp.data)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
    def test_add_api_keys(self):
 | 
			
		||||
        url = "/accounts/apikeys/"
 | 
			
		||||
 | 
			
		||||
        user = baker.make("accounts.User")
 | 
			
		||||
        data = {"name": "Name", "user": user.id, "expiration": None}
 | 
			
		||||
 | 
			
		||||
        resp = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertTrue(APIKey.objects.filter(name="Name").exists())
 | 
			
		||||
        self.assertTrue(APIKey.objects.get(name="Name").key)
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("post", url)
 | 
			
		||||
 | 
			
		||||
    def test_modify_api_key(self):
 | 
			
		||||
        # test a call where api key doesn't exist
 | 
			
		||||
        resp = self.client.put("/accounts/apikeys/500/", format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 404)
 | 
			
		||||
 | 
			
		||||
        apikey = baker.make("accounts.APIKey", name="Test")
 | 
			
		||||
        url = f"/accounts/apikeys/{apikey.pk}/"  # type: ignore
 | 
			
		||||
 | 
			
		||||
        data = {"name": "New Name"}  # type: ignore
 | 
			
		||||
 | 
			
		||||
        resp = self.client.put(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        apikey = APIKey.objects.get(pk=apikey.pk)  # type: ignore
 | 
			
		||||
        self.assertEquals(apikey.name, "New Name")
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("put", url)
 | 
			
		||||
 | 
			
		||||
    def test_delete_api_key(self):
 | 
			
		||||
        # test a call where api key doesn't exist
 | 
			
		||||
        resp = self.client.delete("/accounts/apikeys/500/", format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 404)
 | 
			
		||||
 | 
			
		||||
        # test delete api key
 | 
			
		||||
        apikey = baker.make("accounts.APIKey")
 | 
			
		||||
        url = f"/accounts/apikeys/{apikey.pk}/"  # type: ignore
 | 
			
		||||
        resp = self.client.delete(url, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        self.assertFalse(APIKey.objects.filter(pk=apikey.pk).exists())  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("delete", url)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestTOTPSetup(TacticalTestCase):
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
@@ -312,3 +383,29 @@ class TestTOTPSetup(TacticalTestCase):
 | 
			
		||||
        r = self.client.post(url)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(r.data, "totp token already set")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestAPIAuthentication(TacticalTestCase):
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        # create User and associate to API Key
 | 
			
		||||
        self.user = User.objects.create(username="api_user", is_superuser=True)
 | 
			
		||||
        self.api_key = APIKey.objects.create(
 | 
			
		||||
            name="Test Token", key="123456", user=self.user
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.client_setup()
 | 
			
		||||
 | 
			
		||||
    def test_api_auth(self):
 | 
			
		||||
        url = "/clients/"
 | 
			
		||||
        # auth should fail if no header set
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
        # invalid api key in header should return code 400
 | 
			
		||||
        self.client.credentials(HTTP_X_API_KEY="000000")
 | 
			
		||||
        r = self.client.get(url, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 401)
 | 
			
		||||
 | 
			
		||||
        # valid api key in header should return code 200
 | 
			
		||||
        self.client.credentials(HTTP_X_API_KEY="123456")
 | 
			
		||||
        r = self.client.get(url, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 
 | 
			
		||||
@@ -9,4 +9,8 @@ urlpatterns = [
 | 
			
		||||
    path("users/reset_totp/", views.UserActions.as_view()),
 | 
			
		||||
    path("users/setup_totp/", views.TOTPSetup.as_view()),
 | 
			
		||||
    path("users/ui/", views.UserUI.as_view()),
 | 
			
		||||
    path("roles/", views.GetAddRoles.as_view()),
 | 
			
		||||
    path("roles/<int:pk>/", views.GetUpdateDeleteRole.as_view()),
 | 
			
		||||
    path("apikeys/", views.GetAddAPIKeys.as_view()),
 | 
			
		||||
    path("apikeys/<int:pk>/", views.GetUpdateDeleteAPIKey.as_view()),
 | 
			
		||||
]
 | 
			
		||||
 
 | 
			
		||||
@@ -3,18 +3,25 @@ from django.conf import settings
 | 
			
		||||
from django.contrib.auth import login
 | 
			
		||||
from django.db import IntegrityError
 | 
			
		||||
from django.shortcuts import get_object_or_404
 | 
			
		||||
from ipware import get_client_ip
 | 
			
		||||
from knox.views import LoginView as KnoxLoginView
 | 
			
		||||
from logs.models import AuditLog
 | 
			
		||||
from rest_framework import status
 | 
			
		||||
from rest_framework.authtoken.serializers import AuthTokenSerializer
 | 
			
		||||
from rest_framework.permissions import AllowAny
 | 
			
		||||
from rest_framework.permissions import AllowAny, IsAuthenticated
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
from rest_framework.views import APIView
 | 
			
		||||
 | 
			
		||||
from logs.models import AuditLog
 | 
			
		||||
from tacticalrmm.utils import notify_error
 | 
			
		||||
 | 
			
		||||
from .models import User
 | 
			
		||||
from .serializers import TOTPSetupSerializer, UserSerializer, UserUISerializer
 | 
			
		||||
from .models import APIKey, Role, User
 | 
			
		||||
from .permissions import APIKeyPerms, AccountsPerms, RolesPerms
 | 
			
		||||
from .serializers import (
 | 
			
		||||
    APIKeySerializer,
 | 
			
		||||
    RoleSerializer,
 | 
			
		||||
    TOTPSetupSerializer,
 | 
			
		||||
    UserSerializer,
 | 
			
		||||
    UserUISerializer,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _is_root_user(request, user) -> bool:
 | 
			
		||||
@@ -34,11 +41,16 @@ class CheckCreds(KnoxLoginView):
 | 
			
		||||
        # check credentials
 | 
			
		||||
        serializer = AuthTokenSerializer(data=request.data)
 | 
			
		||||
        if not serializer.is_valid():
 | 
			
		||||
            AuditLog.audit_user_failed_login(request.data["username"])
 | 
			
		||||
            return Response("bad credentials", status=status.HTTP_400_BAD_REQUEST)
 | 
			
		||||
            AuditLog.audit_user_failed_login(
 | 
			
		||||
                request.data["username"], debug_info={"ip": request._client_ip}
 | 
			
		||||
            )
 | 
			
		||||
            return notify_error("Bad credentials")
 | 
			
		||||
 | 
			
		||||
        user = serializer.validated_data["user"]
 | 
			
		||||
 | 
			
		||||
        if user.block_dashboard_login:
 | 
			
		||||
            return notify_error("Bad credentials")
 | 
			
		||||
 | 
			
		||||
        # if totp token not set modify response to notify frontend
 | 
			
		||||
        if not user.totp_key:
 | 
			
		||||
            login(request, user)
 | 
			
		||||
@@ -60,6 +72,9 @@ class LoginView(KnoxLoginView):
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        user = serializer.validated_data["user"]
 | 
			
		||||
 | 
			
		||||
        if user.block_dashboard_login:
 | 
			
		||||
            return notify_error("Bad credentials")
 | 
			
		||||
 | 
			
		||||
        token = request.data["twofactor"]
 | 
			
		||||
        totp = pyotp.TOTP(user.totp_key)
 | 
			
		||||
 | 
			
		||||
@@ -70,16 +85,35 @@ class LoginView(KnoxLoginView):
 | 
			
		||||
 | 
			
		||||
        if valid:
 | 
			
		||||
            login(request, user)
 | 
			
		||||
            AuditLog.audit_user_login_successful(request.data["username"])
 | 
			
		||||
 | 
			
		||||
            # save ip information
 | 
			
		||||
            client_ip, is_routable = get_client_ip(request)
 | 
			
		||||
            user.last_login_ip = client_ip
 | 
			
		||||
            user.save()
 | 
			
		||||
 | 
			
		||||
            AuditLog.audit_user_login_successful(
 | 
			
		||||
                request.data["username"], debug_info={"ip": request._client_ip}
 | 
			
		||||
            )
 | 
			
		||||
            return super(LoginView, self).post(request, format=None)
 | 
			
		||||
        else:
 | 
			
		||||
            AuditLog.audit_user_failed_twofactor(request.data["username"])
 | 
			
		||||
            return Response("bad credentials", status=status.HTTP_400_BAD_REQUEST)
 | 
			
		||||
            AuditLog.audit_user_failed_twofactor(
 | 
			
		||||
                request.data["username"], debug_info={"ip": request._client_ip}
 | 
			
		||||
            )
 | 
			
		||||
            return notify_error("Bad credentials")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetAddUsers(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, AccountsPerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request):
 | 
			
		||||
        users = User.objects.filter(agent=None)
 | 
			
		||||
        search = request.GET.get("search", None)
 | 
			
		||||
 | 
			
		||||
        if search:
 | 
			
		||||
            users = User.objects.filter(agent=None, is_installer_user=False).filter(
 | 
			
		||||
                username__icontains=search
 | 
			
		||||
            )
 | 
			
		||||
        else:
 | 
			
		||||
            users = User.objects.filter(agent=None, is_installer_user=False)
 | 
			
		||||
 | 
			
		||||
        return Response(UserSerializer(users, many=True).data)
 | 
			
		||||
 | 
			
		||||
@@ -96,15 +130,21 @@ class GetAddUsers(APIView):
 | 
			
		||||
                f"ERROR: User {request.data['username']} already exists!"
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        user.first_name = request.data["first_name"]
 | 
			
		||||
        user.last_name = request.data["last_name"]
 | 
			
		||||
        # Can be changed once permissions and groups are introduced
 | 
			
		||||
        user.is_superuser = True
 | 
			
		||||
        if "first_name" in request.data.keys():
 | 
			
		||||
            user.first_name = request.data["first_name"]
 | 
			
		||||
        if "last_name" in request.data.keys():
 | 
			
		||||
            user.last_name = request.data["last_name"]
 | 
			
		||||
        if "role" in request.data.keys() and isinstance(request.data["role"], int):
 | 
			
		||||
            role = get_object_or_404(Role, pk=request.data["role"])
 | 
			
		||||
            user.role = role
 | 
			
		||||
 | 
			
		||||
        user.save()
 | 
			
		||||
        return Response(user.username)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetUpdateDeleteUser(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, AccountsPerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request, pk):
 | 
			
		||||
        user = get_object_or_404(User, pk=pk)
 | 
			
		||||
 | 
			
		||||
@@ -133,7 +173,7 @@ class GetUpdateDeleteUser(APIView):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UserActions(APIView):
 | 
			
		||||
 | 
			
		||||
    permission_classes = [IsAuthenticated, AccountsPerms]
 | 
			
		||||
    # reset password
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
        user = get_object_or_404(User, pk=request.data["id"])
 | 
			
		||||
@@ -182,3 +222,76 @@ class UserUI(APIView):
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        serializer.save()
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetAddRoles(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, RolesPerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request):
 | 
			
		||||
        roles = Role.objects.all()
 | 
			
		||||
        return Response(RoleSerializer(roles, many=True).data)
 | 
			
		||||
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
        serializer = RoleSerializer(data=request.data)
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        serializer.save()
 | 
			
		||||
        return Response("Role was added")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetUpdateDeleteRole(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, RolesPerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request, pk):
 | 
			
		||||
        role = get_object_or_404(Role, pk=pk)
 | 
			
		||||
        return Response(RoleSerializer(role).data)
 | 
			
		||||
 | 
			
		||||
    def put(self, request, pk):
 | 
			
		||||
        role = get_object_or_404(Role, pk=pk)
 | 
			
		||||
        serializer = RoleSerializer(instance=role, data=request.data)
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        serializer.save()
 | 
			
		||||
        return Response("Role was edited")
 | 
			
		||||
 | 
			
		||||
    def delete(self, request, pk):
 | 
			
		||||
        role = get_object_or_404(Role, pk=pk)
 | 
			
		||||
        role.delete()
 | 
			
		||||
        return Response("Role was removed")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetAddAPIKeys(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, APIKeyPerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request):
 | 
			
		||||
        apikeys = APIKey.objects.all()
 | 
			
		||||
        return Response(APIKeySerializer(apikeys, many=True).data)
 | 
			
		||||
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
        # generate a random API Key
 | 
			
		||||
        from django.utils.crypto import get_random_string
 | 
			
		||||
 | 
			
		||||
        request.data["key"] = get_random_string(length=32).upper()
 | 
			
		||||
        serializer = APIKeySerializer(data=request.data)
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        obj = serializer.save()
 | 
			
		||||
        return Response("The API Key was added")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetUpdateDeleteAPIKey(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, APIKeyPerms]
 | 
			
		||||
 | 
			
		||||
    def put(self, request, pk):
 | 
			
		||||
        apikey = get_object_or_404(APIKey, pk=pk)
 | 
			
		||||
 | 
			
		||||
        # remove API key is present in request data
 | 
			
		||||
        if "key" in request.data.keys():
 | 
			
		||||
            request.data.pop("key")
 | 
			
		||||
 | 
			
		||||
        serializer = APIKeySerializer(instance=apikey, data=request.data, partial=True)
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        serializer.save()
 | 
			
		||||
        return Response("The API Key was edited")
 | 
			
		||||
 | 
			
		||||
    def delete(self, request, pk):
 | 
			
		||||
        apikey = get_object_or_404(APIKey, pk=pk)
 | 
			
		||||
        apikey.delete()
 | 
			
		||||
        return Response("The API Key was deleted")
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,9 @@
 | 
			
		||||
from django.contrib import admin
 | 
			
		||||
 | 
			
		||||
from .models import Agent, AgentCustomField, Note, RecoveryAction
 | 
			
		||||
from .models import Agent, AgentCustomField, Note, RecoveryAction, AgentHistory
 | 
			
		||||
 | 
			
		||||
admin.site.register(Agent)
 | 
			
		||||
admin.site.register(RecoveryAction)
 | 
			
		||||
admin.site.register(Note)
 | 
			
		||||
admin.site.register(AgentCustomField)
 | 
			
		||||
admin.site.register(AgentHistory)
 | 
			
		||||
 
 | 
			
		||||
@@ -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(
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										23
									
								
								api/tacticalrmm/agents/migrations/0037_auto_20210627_0014.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								api/tacticalrmm/agents/migrations/0037_auto_20210627_0014.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
# Generated by Django 3.2.4 on 2021-06-27 00:14
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('agents', '0036_agent_block_policy_inheritance'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='agent',
 | 
			
		||||
            name='has_patches_pending',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='agent',
 | 
			
		||||
            name='pending_actions_count',
 | 
			
		||||
            field=models.PositiveIntegerField(default=0),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										27
									
								
								api/tacticalrmm/agents/migrations/0038_agenthistory.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								api/tacticalrmm/agents/migrations/0038_agenthistory.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,27 @@
 | 
			
		||||
# Generated by Django 3.2.1 on 2021-07-06 02:01
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('agents', '0037_auto_20210627_0014'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='AgentHistory',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
			
		||||
                ('time', models.DateTimeField(auto_now_add=True)),
 | 
			
		||||
                ('type', models.CharField(choices=[('task_run', 'Task Run'), ('script_run', 'Script Run'), ('cmd_run', 'CMD Run')], default='cmd_run', max_length=50)),
 | 
			
		||||
                ('command', models.TextField(blank=True, null=True)),
 | 
			
		||||
                ('status', models.CharField(choices=[('success', 'Success'), ('failure', 'Failure')], default='success', max_length=50)),
 | 
			
		||||
                ('username', models.CharField(default='system', max_length=50)),
 | 
			
		||||
                ('results', models.TextField(blank=True, null=True)),
 | 
			
		||||
                ('agent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='history', to='agents.agent')),
 | 
			
		||||
            ],
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										25
									
								
								api/tacticalrmm/agents/migrations/0039_auto_20210714_0738.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								api/tacticalrmm/agents/migrations/0039_auto_20210714_0738.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,25 @@
 | 
			
		||||
# Generated by Django 3.2.5 on 2021-07-14 07:38
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('scripts', '0008_script_guid'),
 | 
			
		||||
        ('agents', '0038_agenthistory'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='agenthistory',
 | 
			
		||||
            name='script',
 | 
			
		||||
            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='history', to='scripts.script'),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='agenthistory',
 | 
			
		||||
            name='script_results',
 | 
			
		||||
            field=models.JSONField(blank=True, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										28
									
								
								api/tacticalrmm/agents/migrations/0040_auto_20211010_0249.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								api/tacticalrmm/agents/migrations/0040_auto_20211010_0249.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,28 @@
 | 
			
		||||
# Generated by Django 3.2.6 on 2021-10-10 02:49
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('agents', '0039_auto_20210714_0738'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='agent',
 | 
			
		||||
            name='agent_id',
 | 
			
		||||
            field=models.CharField(max_length=200, unique=True),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='agent',
 | 
			
		||||
            name='created_by',
 | 
			
		||||
            field=models.CharField(blank=True, max_length=255, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='agent',
 | 
			
		||||
            name='modified_by',
 | 
			
		||||
            field=models.CharField(blank=True, max_length=255, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -0,0 +1,18 @@
 | 
			
		||||
# Generated by Django 3.2.6 on 2021-10-18 03:04
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('agents', '0040_auto_20211010_0249'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='agenthistory',
 | 
			
		||||
            name='username',
 | 
			
		||||
            field=models.CharField(default='system', max_length=255),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -16,17 +16,18 @@ from django.conf import settings
 | 
			
		||||
from django.contrib.postgres.fields import ArrayField
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from loguru import logger
 | 
			
		||||
from nats.aio.client import Client as NATS
 | 
			
		||||
from nats.aio.errors import ErrTimeout
 | 
			
		||||
from packaging import version as pyver
 | 
			
		||||
 | 
			
		||||
from core.models import TZ_CHOICES, CoreSettings
 | 
			
		||||
from logs.models import BaseAuditModel
 | 
			
		||||
 | 
			
		||||
logger.configure(**settings.LOG_CONFIG)
 | 
			
		||||
from logs.models import BaseAuditModel, DebugLog
 | 
			
		||||
from tacticalrmm.models import PermissionQuerySet
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Agent(BaseAuditModel):
 | 
			
		||||
    objects = PermissionQuerySet.as_manager()
 | 
			
		||||
 | 
			
		||||
    version = models.CharField(default="0.1.0", max_length=255)
 | 
			
		||||
    salt_ver = models.CharField(default="1.0.3", max_length=255)
 | 
			
		||||
    operating_system = models.CharField(null=True, blank=True, max_length=255)
 | 
			
		||||
@@ -35,7 +36,7 @@ class Agent(BaseAuditModel):
 | 
			
		||||
    hostname = models.CharField(max_length=255)
 | 
			
		||||
    salt_id = models.CharField(null=True, blank=True, max_length=255)
 | 
			
		||||
    local_ip = models.TextField(null=True, blank=True)  # deprecated
 | 
			
		||||
    agent_id = models.CharField(max_length=200)
 | 
			
		||||
    agent_id = models.CharField(max_length=200, unique=True)
 | 
			
		||||
    last_seen = models.DateTimeField(null=True, blank=True)
 | 
			
		||||
    services = models.JSONField(null=True, blank=True)
 | 
			
		||||
    public_ip = models.CharField(null=True, max_length=255)
 | 
			
		||||
@@ -64,6 +65,8 @@ class Agent(BaseAuditModel):
 | 
			
		||||
    )
 | 
			
		||||
    maintenance_mode = models.BooleanField(default=False)
 | 
			
		||||
    block_policy_inheritance = models.BooleanField(default=False)
 | 
			
		||||
    pending_actions_count = models.PositiveIntegerField(default=0)
 | 
			
		||||
    has_patches_pending = models.BooleanField(default=False)
 | 
			
		||||
    alert_template = models.ForeignKey(
 | 
			
		||||
        "alerts.AlertTemplate",
 | 
			
		||||
        related_name="agents",
 | 
			
		||||
@@ -87,22 +90,28 @@ class Agent(BaseAuditModel):
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def save(self, *args, **kwargs):
 | 
			
		||||
        from automation.tasks import generate_agent_checks_task
 | 
			
		||||
 | 
			
		||||
        # get old agent if exists
 | 
			
		||||
        old_agent = type(self).objects.get(pk=self.pk) if self.pk else None
 | 
			
		||||
        super(BaseAuditModel, self).save(*args, **kwargs)
 | 
			
		||||
        old_agent = Agent.objects.get(pk=self.pk) if self.pk else None
 | 
			
		||||
        super(Agent, self).save(old_model=old_agent, *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
        # check if new agent has been created
 | 
			
		||||
        # or check if policy have changed on agent
 | 
			
		||||
        # or if site has changed on agent and if so generate-policies
 | 
			
		||||
        # or if agent was changed from server or workstation
 | 
			
		||||
        if (
 | 
			
		||||
            not old_agent
 | 
			
		||||
            or (old_agent and old_agent.policy != self.policy)
 | 
			
		||||
            or (old_agent.site != self.site)
 | 
			
		||||
            or (old_agent.monitoring_type != self.monitoring_type)
 | 
			
		||||
            or (old_agent.block_policy_inheritance != self.block_policy_inheritance)
 | 
			
		||||
        ):
 | 
			
		||||
            self.generate_checks_from_policies()
 | 
			
		||||
            self.generate_tasks_from_policies()
 | 
			
		||||
            generate_agent_checks_task.delay(agents=[self.pk], create_tasks=True)
 | 
			
		||||
 | 
			
		||||
        # calculate alert template for new agents
 | 
			
		||||
        if not old_agent:
 | 
			
		||||
            self.set_alert_template()
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return self.hostname
 | 
			
		||||
@@ -119,7 +128,7 @@ class Agent(BaseAuditModel):
 | 
			
		||||
        else:
 | 
			
		||||
            from core.models import CoreSettings
 | 
			
		||||
 | 
			
		||||
            return CoreSettings.objects.first().default_time_zone
 | 
			
		||||
            return CoreSettings.objects.first().default_time_zone  # type: ignore
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def arch(self):
 | 
			
		||||
@@ -161,10 +170,6 @@ class Agent(BaseAuditModel):
 | 
			
		||||
        else:
 | 
			
		||||
            return "offline"
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def has_patches_pending(self):
 | 
			
		||||
        return self.winupdates.filter(action="approve").filter(installed=False).exists()  # type: ignore
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def checks(self):
 | 
			
		||||
        total, passing, failing, warning, info = 0, 0, 0, 0, 0
 | 
			
		||||
@@ -263,6 +268,11 @@ class Agent(BaseAuditModel):
 | 
			
		||||
                make = [x["Manufacturer"] for x in mobo if "Manufacturer" in x][0]
 | 
			
		||||
                model = [x["Product"] for x in mobo if "Product" in x][0]
 | 
			
		||||
 | 
			
		||||
            if make.lower() == "lenovo":
 | 
			
		||||
                sysfam = [x["SystemFamily"] for x in comp_sys if "SystemFamily" in x][0]
 | 
			
		||||
                if "to be filled" not in sysfam.lower():
 | 
			
		||||
                    model = sysfam
 | 
			
		||||
 | 
			
		||||
            return f"{make} {model}"
 | 
			
		||||
        except:
 | 
			
		||||
            pass
 | 
			
		||||
@@ -320,6 +330,7 @@ class Agent(BaseAuditModel):
 | 
			
		||||
        full: bool = False,
 | 
			
		||||
        wait: bool = False,
 | 
			
		||||
        run_on_any: bool = False,
 | 
			
		||||
        history_pk: int = 0,
 | 
			
		||||
    ) -> Any:
 | 
			
		||||
 | 
			
		||||
        from scripts.models import Script
 | 
			
		||||
@@ -338,6 +349,9 @@ class Agent(BaseAuditModel):
 | 
			
		||||
            },
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if history_pk != 0 and pyver.parse(self.version) >= pyver.parse("1.6.0"):
 | 
			
		||||
            data["id"] = history_pk
 | 
			
		||||
 | 
			
		||||
        running_agent = self
 | 
			
		||||
        if run_on_any:
 | 
			
		||||
            nats_ping = {"func": "ping"}
 | 
			
		||||
@@ -440,8 +454,8 @@ class Agent(BaseAuditModel):
 | 
			
		||||
 | 
			
		||||
            # if patch policy still doesn't exist check default policy
 | 
			
		||||
            elif (
 | 
			
		||||
                core_settings.server_policy
 | 
			
		||||
                and core_settings.server_policy.winupdatepolicy.exists()
 | 
			
		||||
                core_settings.server_policy  # type: ignore
 | 
			
		||||
                and core_settings.server_policy.winupdatepolicy.exists()  # type: ignore
 | 
			
		||||
            ):
 | 
			
		||||
                # make sure agent site and client are not blocking inheritance
 | 
			
		||||
                if (
 | 
			
		||||
@@ -449,7 +463,7 @@ class Agent(BaseAuditModel):
 | 
			
		||||
                    and not site.block_policy_inheritance
 | 
			
		||||
                    and not site.client.block_policy_inheritance
 | 
			
		||||
                ):
 | 
			
		||||
                    patch_policy = core_settings.server_policy.winupdatepolicy.get()
 | 
			
		||||
                    patch_policy = core_settings.server_policy.winupdatepolicy.get()  # type: ignore
 | 
			
		||||
 | 
			
		||||
        elif self.monitoring_type == "workstation":
 | 
			
		||||
            # check agent policy first which should override client or site policy
 | 
			
		||||
@@ -478,8 +492,8 @@ class Agent(BaseAuditModel):
 | 
			
		||||
 | 
			
		||||
            # if patch policy still doesn't exist check default policy
 | 
			
		||||
            elif (
 | 
			
		||||
                core_settings.workstation_policy
 | 
			
		||||
                and core_settings.workstation_policy.winupdatepolicy.exists()
 | 
			
		||||
                core_settings.workstation_policy  # type: ignore
 | 
			
		||||
                and core_settings.workstation_policy.winupdatepolicy.exists()  # type: ignore
 | 
			
		||||
            ):
 | 
			
		||||
                # make sure agent site and client are not blocking inheritance
 | 
			
		||||
                if (
 | 
			
		||||
@@ -488,7 +502,7 @@ class Agent(BaseAuditModel):
 | 
			
		||||
                    and not site.client.block_policy_inheritance
 | 
			
		||||
                ):
 | 
			
		||||
                    patch_policy = (
 | 
			
		||||
                        core_settings.workstation_policy.winupdatepolicy.get()
 | 
			
		||||
                        core_settings.workstation_policy.winupdatepolicy.get()  # type: ignore
 | 
			
		||||
                    )
 | 
			
		||||
 | 
			
		||||
        # if policy still doesn't exist return the agent patch policy
 | 
			
		||||
@@ -603,35 +617,35 @@ class Agent(BaseAuditModel):
 | 
			
		||||
 | 
			
		||||
        # check if alert template is applied globally and return
 | 
			
		||||
        if (
 | 
			
		||||
            core.alert_template
 | 
			
		||||
            and core.alert_template.is_active
 | 
			
		||||
            core.alert_template  # type: ignore
 | 
			
		||||
            and core.alert_template.is_active  # type: ignore
 | 
			
		||||
            and not self.block_policy_inheritance
 | 
			
		||||
            and not site.block_policy_inheritance
 | 
			
		||||
            and not client.block_policy_inheritance
 | 
			
		||||
        ):
 | 
			
		||||
            templates.append(core.alert_template)
 | 
			
		||||
            templates.append(core.alert_template)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # if agent is a workstation, check if policy with alert template is assigned to the site, client, or core
 | 
			
		||||
        if (
 | 
			
		||||
            self.monitoring_type == "server"
 | 
			
		||||
            and core.server_policy
 | 
			
		||||
            and core.server_policy.alert_template
 | 
			
		||||
            and core.server_policy.alert_template.is_active
 | 
			
		||||
            and core.server_policy  # type: ignore
 | 
			
		||||
            and core.server_policy.alert_template  # type: ignore
 | 
			
		||||
            and core.server_policy.alert_template.is_active  # type: ignore
 | 
			
		||||
            and not self.block_policy_inheritance
 | 
			
		||||
            and not site.block_policy_inheritance
 | 
			
		||||
            and not client.block_policy_inheritance
 | 
			
		||||
        ):
 | 
			
		||||
            templates.append(core.server_policy.alert_template)
 | 
			
		||||
            templates.append(core.server_policy.alert_template)  # type: ignore
 | 
			
		||||
        if (
 | 
			
		||||
            self.monitoring_type == "workstation"
 | 
			
		||||
            and core.workstation_policy
 | 
			
		||||
            and core.workstation_policy.alert_template
 | 
			
		||||
            and core.workstation_policy.alert_template.is_active
 | 
			
		||||
            and core.workstation_policy  # type: ignore
 | 
			
		||||
            and core.workstation_policy.alert_template  # type: ignore
 | 
			
		||||
            and core.workstation_policy.alert_template.is_active  # type: ignore
 | 
			
		||||
            and not self.block_policy_inheritance
 | 
			
		||||
            and not site.block_policy_inheritance
 | 
			
		||||
            and not client.block_policy_inheritance
 | 
			
		||||
        ):
 | 
			
		||||
            templates.append(core.workstation_policy.alert_template)
 | 
			
		||||
            templates.append(core.workstation_policy.alert_template)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # go through the templates and return the first one that isn't excluded
 | 
			
		||||
        for template in templates:
 | 
			
		||||
@@ -692,7 +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)
 | 
			
		||||
 | 
			
		||||
@@ -734,7 +748,7 @@ class Agent(BaseAuditModel):
 | 
			
		||||
                try:
 | 
			
		||||
                    ret = msgpack.loads(msg.data)  # type: ignore
 | 
			
		||||
                except Exception as e:
 | 
			
		||||
                    logger.error(e)
 | 
			
		||||
                    DebugLog.error(agent=self, log_type="agent_issues", message=e)
 | 
			
		||||
                    ret = str(e)
 | 
			
		||||
 | 
			
		||||
            await nc.close()
 | 
			
		||||
@@ -747,12 +761,9 @@ class Agent(BaseAuditModel):
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def serialize(agent):
 | 
			
		||||
        # serializes the agent and returns json
 | 
			
		||||
        from .serializers import AgentEditSerializer
 | 
			
		||||
        from .serializers import AgentAuditSerializer
 | 
			
		||||
 | 
			
		||||
        ret = AgentEditSerializer(agent).data
 | 
			
		||||
        del ret["all_timezones"]
 | 
			
		||||
        del ret["client"]
 | 
			
		||||
        return ret
 | 
			
		||||
        return AgentAuditSerializer(agent).data
 | 
			
		||||
 | 
			
		||||
    def delete_superseded_updates(self):
 | 
			
		||||
        try:
 | 
			
		||||
@@ -767,7 +778,7 @@ class Agent(BaseAuditModel):
 | 
			
		||||
                # skip if no version info is available therefore nothing to parse
 | 
			
		||||
                try:
 | 
			
		||||
                    vers = [
 | 
			
		||||
                        re.search(r"\(Version(.*?)\)", i).group(1).strip()
 | 
			
		||||
                        re.search(r"\(Version(.*?)\)", i).group(1).strip()  # type: ignore
 | 
			
		||||
                        for i in titles
 | 
			
		||||
                    ]
 | 
			
		||||
                    sorted_vers = sorted(vers, key=LooseVersion)
 | 
			
		||||
@@ -802,7 +813,7 @@ class Agent(BaseAuditModel):
 | 
			
		||||
        from core.models import CoreSettings
 | 
			
		||||
 | 
			
		||||
        CORE = CoreSettings.objects.first()
 | 
			
		||||
        CORE.send_mail(
 | 
			
		||||
        CORE.send_mail(  # type: ignore
 | 
			
		||||
            f"{self.client.name}, {self.site.name}, {self.hostname} - data overdue",
 | 
			
		||||
            (
 | 
			
		||||
                f"Data has not been received from client {self.client.name}, "
 | 
			
		||||
@@ -817,7 +828,7 @@ class Agent(BaseAuditModel):
 | 
			
		||||
        from core.models import CoreSettings
 | 
			
		||||
 | 
			
		||||
        CORE = CoreSettings.objects.first()
 | 
			
		||||
        CORE.send_mail(
 | 
			
		||||
        CORE.send_mail(  # type: ignore
 | 
			
		||||
            f"{self.client.name}, {self.site.name}, {self.hostname} - data received",
 | 
			
		||||
            (
 | 
			
		||||
                f"Data has been received from client {self.client.name}, "
 | 
			
		||||
@@ -832,7 +843,7 @@ class Agent(BaseAuditModel):
 | 
			
		||||
        from core.models import CoreSettings
 | 
			
		||||
 | 
			
		||||
        CORE = CoreSettings.objects.first()
 | 
			
		||||
        CORE.send_sms(
 | 
			
		||||
        CORE.send_sms(  # type: ignore
 | 
			
		||||
            f"{self.client.name}, {self.site.name}, {self.hostname} - data overdue",
 | 
			
		||||
            alert_template=self.alert_template,
 | 
			
		||||
        )
 | 
			
		||||
@@ -841,7 +852,7 @@ class Agent(BaseAuditModel):
 | 
			
		||||
        from core.models import CoreSettings
 | 
			
		||||
 | 
			
		||||
        CORE = CoreSettings.objects.first()
 | 
			
		||||
        CORE.send_sms(
 | 
			
		||||
        CORE.send_sms(  # type: ignore
 | 
			
		||||
            f"{self.client.name}, {self.site.name}, {self.hostname} - data received",
 | 
			
		||||
            alert_template=self.alert_template,
 | 
			
		||||
        )
 | 
			
		||||
@@ -857,6 +868,8 @@ RECOVERY_CHOICES = [
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RecoveryAction(models.Model):
 | 
			
		||||
    objects = PermissionQuerySet.as_manager()
 | 
			
		||||
 | 
			
		||||
    agent = models.ForeignKey(
 | 
			
		||||
        Agent,
 | 
			
		||||
        related_name="recoveryactions",
 | 
			
		||||
@@ -871,6 +884,8 @@ class RecoveryAction(models.Model):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Note(models.Model):
 | 
			
		||||
    objects = PermissionQuerySet.as_manager()
 | 
			
		||||
 | 
			
		||||
    agent = models.ForeignKey(
 | 
			
		||||
        Agent,
 | 
			
		||||
        related_name="notes",
 | 
			
		||||
@@ -891,6 +906,8 @@ class Note(models.Model):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AgentCustomField(models.Model):
 | 
			
		||||
    objects = PermissionQuerySet.as_manager()
 | 
			
		||||
 | 
			
		||||
    agent = models.ForeignKey(
 | 
			
		||||
        Agent,
 | 
			
		||||
        related_name="custom_fields",
 | 
			
		||||
@@ -923,3 +940,59 @@ class AgentCustomField(models.Model):
 | 
			
		||||
            return self.bool_value
 | 
			
		||||
        else:
 | 
			
		||||
            return self.string_value
 | 
			
		||||
 | 
			
		||||
    def save_to_field(self, value):
 | 
			
		||||
        if self.field.type in [
 | 
			
		||||
            "text",
 | 
			
		||||
            "number",
 | 
			
		||||
            "single",
 | 
			
		||||
            "datetime",
 | 
			
		||||
        ]:
 | 
			
		||||
            self.string_value = value
 | 
			
		||||
            self.save()
 | 
			
		||||
        elif self.field.type == "multiple":
 | 
			
		||||
            self.multiple_value = value.split(",")
 | 
			
		||||
            self.save()
 | 
			
		||||
        elif self.field.type == "checkbox":
 | 
			
		||||
            self.bool_value = bool(value)
 | 
			
		||||
            self.save()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
AGENT_HISTORY_TYPES = (
 | 
			
		||||
    ("task_run", "Task Run"),
 | 
			
		||||
    ("script_run", "Script Run"),
 | 
			
		||||
    ("cmd_run", "CMD Run"),
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
AGENT_HISTORY_STATUS = (("success", "Success"), ("failure", "Failure"))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AgentHistory(models.Model):
 | 
			
		||||
    objects = PermissionQuerySet.as_manager()
 | 
			
		||||
 | 
			
		||||
    agent = models.ForeignKey(
 | 
			
		||||
        Agent,
 | 
			
		||||
        related_name="history",
 | 
			
		||||
        on_delete=models.CASCADE,
 | 
			
		||||
    )
 | 
			
		||||
    time = models.DateTimeField(auto_now_add=True)
 | 
			
		||||
    type = models.CharField(
 | 
			
		||||
        max_length=50, choices=AGENT_HISTORY_TYPES, default="cmd_run"
 | 
			
		||||
    )
 | 
			
		||||
    command = models.TextField(null=True, blank=True)
 | 
			
		||||
    status = models.CharField(
 | 
			
		||||
        max_length=50, choices=AGENT_HISTORY_STATUS, default="success"
 | 
			
		||||
    )
 | 
			
		||||
    username = models.CharField(max_length=255, default="system")
 | 
			
		||||
    results = models.TextField(null=True, blank=True)
 | 
			
		||||
    script = models.ForeignKey(
 | 
			
		||||
        "scripts.Script",
 | 
			
		||||
        null=True,
 | 
			
		||||
        blank=True,
 | 
			
		||||
        related_name="history",
 | 
			
		||||
        on_delete=models.SET_NULL,
 | 
			
		||||
    )
 | 
			
		||||
    script_results = models.JSONField(null=True, blank=True)
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return f"{self.agent.hostname} - {self.type}"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										123
									
								
								api/tacticalrmm/agents/permissions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								api/tacticalrmm/agents/permissions.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,123 @@
 | 
			
		||||
from rest_framework import permissions
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.permissions import _has_perm, _has_perm_on_agent
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AgentPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        if r.method == "GET":
 | 
			
		||||
            if "agent_id" in view.kwargs.keys():
 | 
			
		||||
                return _has_perm(r, "can_list_agents") and _has_perm_on_agent(
 | 
			
		||||
                    r.user, view.kwargs["agent_id"]
 | 
			
		||||
                )
 | 
			
		||||
            else:
 | 
			
		||||
                return _has_perm(r, "can_list_agents")
 | 
			
		||||
        elif r.method == "DELETE":
 | 
			
		||||
            return _has_perm(r, "can_uninstall_agents") and _has_perm_on_agent(
 | 
			
		||||
                r.user, view.kwargs["agent_id"]
 | 
			
		||||
            )
 | 
			
		||||
        else:
 | 
			
		||||
            if r.path == "/agents/maintenance/bulk/":
 | 
			
		||||
                return _has_perm(r, "can_edit_agent")
 | 
			
		||||
            else:
 | 
			
		||||
                return _has_perm(r, "can_edit_agent") and _has_perm_on_agent(
 | 
			
		||||
                    r.user, view.kwargs["agent_id"]
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RecoverAgentPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        return _has_perm(r, "can_recover_agents") and _has_perm_on_agent(
 | 
			
		||||
            r.user, view.kwargs["agent_id"]
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class MeshPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        return _has_perm(r, "can_use_mesh") and _has_perm_on_agent(
 | 
			
		||||
            r.user, view.kwargs["agent_id"]
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UpdateAgentPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        return _has_perm(r, "can_update_agents")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PingAgentPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        return _has_perm(r, "can_ping_agents") and _has_perm_on_agent(
 | 
			
		||||
            r.user, view.kwargs["agent_id"]
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ManageProcPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        return _has_perm(r, "can_manage_procs") and _has_perm_on_agent(
 | 
			
		||||
            r.user, view.kwargs["agent_id"]
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class EvtLogPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        return _has_perm(r, "can_view_eventlogs") and _has_perm_on_agent(
 | 
			
		||||
            r.user, view.kwargs["agent_id"]
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SendCMDPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        return _has_perm(r, "can_send_cmd") and _has_perm_on_agent(
 | 
			
		||||
            r.user, view.kwargs["agent_id"]
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RebootAgentPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        return _has_perm(r, "can_reboot_agents") and _has_perm_on_agent(
 | 
			
		||||
            r.user, view.kwargs["agent_id"]
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class InstallAgentPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        return _has_perm(r, "can_install_agents")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RunScriptPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        return _has_perm(r, "can_run_scripts") and _has_perm_on_agent(
 | 
			
		||||
            r.user, view.kwargs["agent_id"]
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AgentNotesPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
 | 
			
		||||
        # permissions for GET /agents/notes/ endpoint
 | 
			
		||||
        if r.method == "GET":
 | 
			
		||||
 | 
			
		||||
            # permissions for /agents/<agent_id>/notes endpoint
 | 
			
		||||
            if "agent_id" in view.kwargs.keys():
 | 
			
		||||
                return _has_perm(r, "can_list_notes") and _has_perm_on_agent(
 | 
			
		||||
                    r.user, view.kwargs["agent_id"]
 | 
			
		||||
                )
 | 
			
		||||
            else:
 | 
			
		||||
                return _has_perm(r, "can_list_notes")
 | 
			
		||||
        else:
 | 
			
		||||
            return _has_perm(r, "can_manage_notes")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RunBulkPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        return _has_perm(r, "can_run_bulk")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AgentHistoryPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        if "agent_id" in view.kwargs.keys():
 | 
			
		||||
            return _has_perm(r, "can_list_agent_history") and _has_perm_on_agent(
 | 
			
		||||
                r.user, view.kwargs["agent_id"]
 | 
			
		||||
            )
 | 
			
		||||
        else:
 | 
			
		||||
            return _has_perm(r, "can_list_agent_history")
 | 
			
		||||
@@ -1,15 +1,30 @@
 | 
			
		||||
import pytz
 | 
			
		||||
from rest_framework import serializers
 | 
			
		||||
 | 
			
		||||
from clients.serializers import ClientSerializer
 | 
			
		||||
from winupdate.serializers import WinUpdatePolicySerializer
 | 
			
		||||
 | 
			
		||||
from .models import Agent, AgentCustomField, Note
 | 
			
		||||
from .models import Agent, AgentCustomField, Note, AgentHistory
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AgentCustomFieldSerializer(serializers.ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = AgentCustomField
 | 
			
		||||
        fields = (
 | 
			
		||||
            "id",
 | 
			
		||||
            "field",
 | 
			
		||||
            "agent",
 | 
			
		||||
            "value",
 | 
			
		||||
            "string_value",
 | 
			
		||||
            "bool_value",
 | 
			
		||||
            "multiple_value",
 | 
			
		||||
        )
 | 
			
		||||
        extra_kwargs = {
 | 
			
		||||
            "string_value": {"write_only": True},
 | 
			
		||||
            "bool_value": {"write_only": True},
 | 
			
		||||
            "multiple_value": {"write_only": True},
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AgentSerializer(serializers.ModelSerializer):
 | 
			
		||||
    # for vue
 | 
			
		||||
    patches_pending = serializers.ReadOnlyField(source="has_patches_pending")
 | 
			
		||||
    winupdatepolicy = WinUpdatePolicySerializer(many=True, read_only=True)
 | 
			
		||||
    status = serializers.ReadOnlyField()
 | 
			
		||||
    cpu_model = serializers.ReadOnlyField()
 | 
			
		||||
@@ -20,33 +35,19 @@ 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)
 | 
			
		||||
 | 
			
		||||
    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 = ["last_seen", "id"]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AgentTableSerializer(serializers.ModelSerializer):
 | 
			
		||||
    patches_pending = serializers.ReadOnlyField(source="has_patches_pending")
 | 
			
		||||
    pending_actions = serializers.SerializerMethodField()
 | 
			
		||||
    status = serializers.ReadOnlyField()
 | 
			
		||||
    checks = serializers.ReadOnlyField()
 | 
			
		||||
    last_seen = serializers.SerializerMethodField()
 | 
			
		||||
@@ -69,9 +70,6 @@ class AgentTableSerializer(serializers.ModelSerializer):
 | 
			
		||||
                "always_alert": obj.alert_template.agent_always_alert,
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
    def get_pending_actions(self, obj):
 | 
			
		||||
        return obj.pendingactions.filter(status="pending").count()
 | 
			
		||||
 | 
			
		||||
    def get_last_seen(self, obj) -> str:
 | 
			
		||||
        if obj.time_zone is not None:
 | 
			
		||||
            agent_tz = pytz.timezone(obj.time_zone)
 | 
			
		||||
@@ -94,17 +92,16 @@ class AgentTableSerializer(serializers.ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Agent
 | 
			
		||||
        fields = [
 | 
			
		||||
            "id",
 | 
			
		||||
            "agent_id",
 | 
			
		||||
            "alert_template",
 | 
			
		||||
            "hostname",
 | 
			
		||||
            "agent_id",
 | 
			
		||||
            "site_name",
 | 
			
		||||
            "client_name",
 | 
			
		||||
            "monitoring_type",
 | 
			
		||||
            "description",
 | 
			
		||||
            "needs_reboot",
 | 
			
		||||
            "patches_pending",
 | 
			
		||||
            "pending_actions",
 | 
			
		||||
            "has_patches_pending",
 | 
			
		||||
            "pending_actions_count",
 | 
			
		||||
            "status",
 | 
			
		||||
            "overdue_text_alert",
 | 
			
		||||
            "overdue_email_alert",
 | 
			
		||||
@@ -121,63 +118,7 @@ class AgentTableSerializer(serializers.ModelSerializer):
 | 
			
		||||
        depth = 2
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AgentCustomFieldSerializer(serializers.ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = AgentCustomField
 | 
			
		||||
        fields = (
 | 
			
		||||
            "id",
 | 
			
		||||
            "field",
 | 
			
		||||
            "agent",
 | 
			
		||||
            "value",
 | 
			
		||||
            "string_value",
 | 
			
		||||
            "bool_value",
 | 
			
		||||
            "multiple_value",
 | 
			
		||||
        )
 | 
			
		||||
        extra_kwargs = {
 | 
			
		||||
            "string_value": {"write_only": True},
 | 
			
		||||
            "bool_value": {"write_only": True},
 | 
			
		||||
            "multiple_value": {"write_only": True},
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AgentEditSerializer(serializers.ModelSerializer):
 | 
			
		||||
    winupdatepolicy = WinUpdatePolicySerializer(many=True, read_only=True)
 | 
			
		||||
    all_timezones = serializers.SerializerMethodField()
 | 
			
		||||
    client = ClientSerializer(read_only=True)
 | 
			
		||||
    custom_fields = AgentCustomFieldSerializer(many=True, read_only=True)
 | 
			
		||||
 | 
			
		||||
    def get_all_timezones(self, obj):
 | 
			
		||||
        return pytz.all_timezones
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Agent
 | 
			
		||||
        fields = [
 | 
			
		||||
            "id",
 | 
			
		||||
            "hostname",
 | 
			
		||||
            "client",
 | 
			
		||||
            "site",
 | 
			
		||||
            "monitoring_type",
 | 
			
		||||
            "description",
 | 
			
		||||
            "time_zone",
 | 
			
		||||
            "timezone",
 | 
			
		||||
            "check_interval",
 | 
			
		||||
            "overdue_time",
 | 
			
		||||
            "offline_time",
 | 
			
		||||
            "overdue_text_alert",
 | 
			
		||||
            "overdue_email_alert",
 | 
			
		||||
            "all_timezones",
 | 
			
		||||
            "winupdatepolicy",
 | 
			
		||||
            "policy",
 | 
			
		||||
            "custom_fields",
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class WinAgentSerializer(serializers.ModelSerializer):
 | 
			
		||||
    # for the windows agent
 | 
			
		||||
    patches_pending = serializers.ReadOnlyField(source="has_patches_pending")
 | 
			
		||||
    winupdatepolicy = WinUpdatePolicySerializer(many=True, read_only=True)
 | 
			
		||||
    status = serializers.ReadOnlyField()
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Agent
 | 
			
		||||
        fields = "__all__"
 | 
			
		||||
@@ -190,24 +131,38 @@ class AgentHostnameSerializer(serializers.ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Agent
 | 
			
		||||
        fields = (
 | 
			
		||||
            "id",
 | 
			
		||||
            "hostname",
 | 
			
		||||
            "pk",
 | 
			
		||||
            "agent_id",
 | 
			
		||||
            "client",
 | 
			
		||||
            "site",
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NoteSerializer(serializers.ModelSerializer):
 | 
			
		||||
class AgentNoteSerializer(serializers.ModelSerializer):
 | 
			
		||||
    username = serializers.ReadOnlyField(source="user.username")
 | 
			
		||||
    agent_id = serializers.ReadOnlyField(source="agent.agent_id")
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Note
 | 
			
		||||
        fields = "__all__"
 | 
			
		||||
        fields = ("pk", "entry_time", "agent", "user", "note", "username", "agent_id")
 | 
			
		||||
        extra_kwargs = {"agent": {"write_only": True}, "user": {"write_only": True}}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NotesSerializer(serializers.ModelSerializer):
 | 
			
		||||
    notes = NoteSerializer(many=True, read_only=True)
 | 
			
		||||
class AgentHistorySerializer(serializers.ModelSerializer):
 | 
			
		||||
    time = serializers.SerializerMethodField(read_only=True)
 | 
			
		||||
    script_name = serializers.ReadOnlyField(source="script.name")
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = AgentHistory
 | 
			
		||||
        fields = "__all__"
 | 
			
		||||
 | 
			
		||||
    def get_time(self, history):
 | 
			
		||||
        tz = self.context["default_tz"]
 | 
			
		||||
        return history.time.astimezone(tz).strftime("%m %d %Y %H:%M:%S")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AgentAuditSerializer(serializers.ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Agent
 | 
			
		||||
        fields = ["hostname", "pk", "notes"]
 | 
			
		||||
        exclude = ["disks", "services", "wmi_detail"]
 | 
			
		||||
 
 | 
			
		||||
@@ -1,49 +1,42 @@
 | 
			
		||||
import asyncio
 | 
			
		||||
import datetime as dt
 | 
			
		||||
import random
 | 
			
		||||
import urllib.parse
 | 
			
		||||
from time import sleep
 | 
			
		||||
from typing import Union
 | 
			
		||||
 | 
			
		||||
from alerts.models import Alert
 | 
			
		||||
from core.models import CoreSettings
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from loguru import logger
 | 
			
		||||
from logs.models import DebugLog, PendingAction
 | 
			
		||||
from packaging import version as pyver
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from core.models import CodeSignToken, CoreSettings
 | 
			
		||||
from logs.models import PendingAction
 | 
			
		||||
from scripts.models import Script
 | 
			
		||||
from tacticalrmm.celery import app
 | 
			
		||||
from tacticalrmm.utils import run_nats_api_cmd
 | 
			
		||||
 | 
			
		||||
logger.configure(**settings.LOG_CONFIG)
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from agents.utils import get_winagent_url
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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"
 | 
			
		||||
 | 
			
		||||
    # skip if we can't determine the arch
 | 
			
		||||
    if agent.arch is None:
 | 
			
		||||
        logger.warning(
 | 
			
		||||
            f"Unable to determine arch on {agent.hostname}. Skipping agent update."
 | 
			
		||||
        DebugLog.warning(
 | 
			
		||||
            agent=agent,
 | 
			
		||||
            log_type="agent_issues",
 | 
			
		||||
            message=f"Unable to determine arch on {agent.hostname}({agent.agent_id}). Skipping agent update.",
 | 
			
		||||
        )
 | 
			
		||||
        return "noarch"
 | 
			
		||||
 | 
			
		||||
    version = settings.LATEST_AGENT_VER
 | 
			
		||||
    inno = agent.win_inno_exe
 | 
			
		||||
 | 
			
		||||
    if codesigntoken is not None and pyver.parse(version) >= pyver.parse("1.5.0"):
 | 
			
		||||
        base_url = get_exegen_url() + "/api/v1/winagents/?"
 | 
			
		||||
        params = {"version": version, "arch": agent.arch, "token": codesigntoken}
 | 
			
		||||
        url = base_url + urllib.parse.urlencode(params)
 | 
			
		||||
    else:
 | 
			
		||||
        url = agent.winagent_dl
 | 
			
		||||
    url = get_winagent_url(agent.arch)
 | 
			
		||||
 | 
			
		||||
    if not force:
 | 
			
		||||
        if agent.pendingactions.filter(
 | 
			
		||||
@@ -76,31 +69,21 @@ def agent_update(pk: int, codesigntoken: str = None, force: bool = False) -> str
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
def force_code_sign(pks: list[int]) -> None:
 | 
			
		||||
    try:
 | 
			
		||||
        token = CodeSignToken.objects.first().token
 | 
			
		||||
    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
 | 
			
		||||
    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 + 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)
 | 
			
		||||
 | 
			
		||||
@@ -108,25 +91,20 @@ def send_agent_update_task(pks: list[int]) -> None:
 | 
			
		||||
@app.task
 | 
			
		||||
def auto_self_agent_update_task() -> None:
 | 
			
		||||
    core = CoreSettings.objects.first()
 | 
			
		||||
    if not core.agent_auto_update:
 | 
			
		||||
    if not core.agent_auto_update:  # type:ignore
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        codesigntoken = CodeSignToken.objects.first().token
 | 
			
		||||
    except:
 | 
			
		||||
        codesigntoken = None
 | 
			
		||||
 | 
			
		||||
    q = Agent.objects.only("pk", "version")
 | 
			
		||||
    pks: list[int] = [
 | 
			
		||||
        i.pk
 | 
			
		||||
    q = Agent.objects.only("agent_id", "version")
 | 
			
		||||
    agent_ids: list[str] = [
 | 
			
		||||
        i.agent_id
 | 
			
		||||
        for i in q
 | 
			
		||||
        if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER)
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    chunks = (pks[i : i + 30] for i in range(0, len(pks), 30))
 | 
			
		||||
    chunks = (agent_ids[i : i + 30] for i in range(0, len(agent_ids), 30))
 | 
			
		||||
    for chunk in chunks:
 | 
			
		||||
        for pk in chunk:
 | 
			
		||||
            agent_update(pk, codesigntoken)
 | 
			
		||||
        for agent_id in chunk:
 | 
			
		||||
            agent_update(agent_id)
 | 
			
		||||
            sleep(0.05)
 | 
			
		||||
        sleep(4)
 | 
			
		||||
 | 
			
		||||
@@ -211,6 +189,7 @@ def agent_outages_task() -> None:
 | 
			
		||||
 | 
			
		||||
    agents = Agent.objects.only(
 | 
			
		||||
        "pk",
 | 
			
		||||
        "agent_id",
 | 
			
		||||
        "last_seen",
 | 
			
		||||
        "offline_time",
 | 
			
		||||
        "overdue_time",
 | 
			
		||||
@@ -231,14 +210,24 @@ def run_script_email_results_task(
 | 
			
		||||
    nats_timeout: int,
 | 
			
		||||
    emails: list[str],
 | 
			
		||||
    args: list[str] = [],
 | 
			
		||||
    history_pk: int = 0,
 | 
			
		||||
):
 | 
			
		||||
    agent = Agent.objects.get(pk=agentpk)
 | 
			
		||||
    script = Script.objects.get(pk=scriptpk)
 | 
			
		||||
    r = agent.run_script(
 | 
			
		||||
        scriptpk=script.pk, args=args, full=True, timeout=nats_timeout, wait=True
 | 
			
		||||
        scriptpk=script.pk,
 | 
			
		||||
        args=args,
 | 
			
		||||
        full=True,
 | 
			
		||||
        timeout=nats_timeout,
 | 
			
		||||
        wait=True,
 | 
			
		||||
        history_pk=history_pk,
 | 
			
		||||
    )
 | 
			
		||||
    if r == "timeout":
 | 
			
		||||
        logger.error(f"{agent.hostname} timed out running script.")
 | 
			
		||||
        DebugLog.error(
 | 
			
		||||
            agent=agent,
 | 
			
		||||
            log_type="scripting",
 | 
			
		||||
            message=f"{agent.hostname}({agent.pk}) timed out running script.",
 | 
			
		||||
        )
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    CORE = CoreSettings.objects.first()
 | 
			
		||||
@@ -254,37 +243,60 @@ def run_script_email_results_task(
 | 
			
		||||
 | 
			
		||||
    msg = EmailMessage()
 | 
			
		||||
    msg["Subject"] = subject
 | 
			
		||||
    msg["From"] = CORE.smtp_from_email
 | 
			
		||||
    msg["From"] = CORE.smtp_from_email  # type:ignore
 | 
			
		||||
 | 
			
		||||
    if emails:
 | 
			
		||||
        msg["To"] = ", ".join(emails)
 | 
			
		||||
    else:
 | 
			
		||||
        msg["To"] = ", ".join(CORE.email_alert_recipients)
 | 
			
		||||
        msg["To"] = ", ".join(CORE.email_alert_recipients)  # type:ignore
 | 
			
		||||
 | 
			
		||||
    msg.set_content(body)
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        with smtplib.SMTP(CORE.smtp_host, CORE.smtp_port, timeout=20) as server:
 | 
			
		||||
            if CORE.smtp_requires_auth:
 | 
			
		||||
        with smtplib.SMTP(
 | 
			
		||||
            CORE.smtp_host, CORE.smtp_port, timeout=20  # type:ignore
 | 
			
		||||
        ) as server:  # type:ignore
 | 
			
		||||
            if CORE.smtp_requires_auth:  # type:ignore
 | 
			
		||||
                server.ehlo()
 | 
			
		||||
                server.starttls()
 | 
			
		||||
                server.login(CORE.smtp_host_user, CORE.smtp_host_password)
 | 
			
		||||
                server.login(
 | 
			
		||||
                    CORE.smtp_host_user, CORE.smtp_host_password  # type:ignore
 | 
			
		||||
                )  # type:ignore
 | 
			
		||||
                server.send_message(msg)
 | 
			
		||||
                server.quit()
 | 
			
		||||
            else:
 | 
			
		||||
                server.send_message(msg)
 | 
			
		||||
                server.quit()
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        logger.error(e)
 | 
			
		||||
        DebugLog.error(message=e)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
def monitor_agents_task() -> None:
 | 
			
		||||
    agents = Agent.objects.only(
 | 
			
		||||
        "pk", "agent_id", "last_seen", "overdue_time", "offline_time"
 | 
			
		||||
def clear_faults_task(older_than_days: int) -> None:
 | 
			
		||||
    # https://github.com/wh1te909/tacticalrmm/issues/484
 | 
			
		||||
    agents = Agent.objects.exclude(last_seen__isnull=True).filter(
 | 
			
		||||
        last_seen__lt=djangotime.now() - djangotime.timedelta(days=older_than_days)
 | 
			
		||||
    )
 | 
			
		||||
    ids = [i.agent_id for i in agents if i.status != "online"]
 | 
			
		||||
    run_nats_api_cmd("monitor", ids)
 | 
			
		||||
    for agent in agents:
 | 
			
		||||
        if agent.agentchecks.exists():
 | 
			
		||||
            for check in agent.agentchecks.all():
 | 
			
		||||
                # reset check status
 | 
			
		||||
                check.status = "passing"
 | 
			
		||||
                check.save(update_fields=["status"])
 | 
			
		||||
                if check.alert.filter(resolved=False).exists():
 | 
			
		||||
                    check.alert.get(resolved=False).resolve()
 | 
			
		||||
 | 
			
		||||
        # reset overdue alerts
 | 
			
		||||
        agent.overdue_email_alert = False
 | 
			
		||||
        agent.overdue_text_alert = False
 | 
			
		||||
        agent.overdue_dashboard_alert = False
 | 
			
		||||
        agent.save(
 | 
			
		||||
            update_fields=[
 | 
			
		||||
                "overdue_email_alert",
 | 
			
		||||
                "overdue_text_alert",
 | 
			
		||||
                "overdue_dashboard_alert",
 | 
			
		||||
            ]
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
@@ -293,4 +305,67 @@ def get_wmi_task() -> None:
 | 
			
		||||
        "pk", "agent_id", "last_seen", "overdue_time", "offline_time"
 | 
			
		||||
    )
 | 
			
		||||
    ids = [i.agent_id for i in agents if i.status == "online"]
 | 
			
		||||
    run_nats_api_cmd("wmi", ids)
 | 
			
		||||
    run_nats_api_cmd("wmi", ids, timeout=45)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
def agent_checkin_task() -> None:
 | 
			
		||||
    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
 | 
			
		||||
 | 
			
		||||
    AgentHistory.objects.filter(
 | 
			
		||||
        time__lt=djangotime.now() - djangotime.timedelta(days=older_than_days)
 | 
			
		||||
    ).delete()
 | 
			
		||||
 | 
			
		||||
    return "ok"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
def handle_agents_task() -> None:
 | 
			
		||||
    q = Agent.objects.prefetch_related("pendingactions", "autotasks").only(
 | 
			
		||||
        "pk", "agent_id", "version", "last_seen", "overdue_time", "offline_time"
 | 
			
		||||
    )
 | 
			
		||||
    agents = [
 | 
			
		||||
        i
 | 
			
		||||
        for i in q
 | 
			
		||||
        if pyver.parse(i.version) >= pyver.parse("1.6.0") and i.status == "online"
 | 
			
		||||
    ]
 | 
			
		||||
    for agent in agents:
 | 
			
		||||
        # change agent update pending status to completed if agent has just updated
 | 
			
		||||
        if (
 | 
			
		||||
            pyver.parse(agent.version) == pyver.parse(settings.LATEST_AGENT_VER)
 | 
			
		||||
            and agent.pendingactions.filter(
 | 
			
		||||
                action_type="agentupdate", status="pending"
 | 
			
		||||
            ).exists()
 | 
			
		||||
        ):
 | 
			
		||||
            agent.pendingactions.filter(
 | 
			
		||||
                action_type="agentupdate", status="pending"
 | 
			
		||||
            ).update(status="completed")
 | 
			
		||||
 | 
			
		||||
        # sync scheduled tasks
 | 
			
		||||
        if agent.autotasks.exclude(sync_status="synced").exists():  # type: ignore
 | 
			
		||||
            tasks = agent.autotasks.exclude(sync_status="synced")  # type: ignore
 | 
			
		||||
 | 
			
		||||
            for task in tasks:
 | 
			
		||||
                if task.sync_status == "pendingdeletion":
 | 
			
		||||
                    task.delete_task_on_agent()
 | 
			
		||||
                elif task.sync_status == "initial":
 | 
			
		||||
                    task.modify_task_on_agent()
 | 
			
		||||
                elif task.sync_status == "notsynced":
 | 
			
		||||
                    task.create_task_on_agent()
 | 
			
		||||
 | 
			
		||||
        # handles any alerting actions
 | 
			
		||||
        if Alert.objects.filter(agent=agent, resolved=False).exists():
 | 
			
		||||
            try:
 | 
			
		||||
                Alert.handle_alert_resolve(agent)
 | 
			
		||||
            except:
 | 
			
		||||
                continue
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -1,32 +1,44 @@
 | 
			
		||||
from django.urls import path
 | 
			
		||||
 | 
			
		||||
from . import views
 | 
			
		||||
from checks.views import GetAddChecks
 | 
			
		||||
from autotasks.views import GetAddAutoTasks
 | 
			
		||||
from logs.views import PendingActions
 | 
			
		||||
 | 
			
		||||
urlpatterns = [
 | 
			
		||||
    path("listagents/", views.AgentsTableList.as_view()),
 | 
			
		||||
    path("listagentsnodetail/", views.list_agents_no_detail),
 | 
			
		||||
    path("<int:pk>/agenteditdetails/", views.agent_edit_details),
 | 
			
		||||
    path("overdueaction/", views.overdue_action),
 | 
			
		||||
    path("sendrawcmd/", views.send_raw_cmd),
 | 
			
		||||
    path("<pk>/agentdetail/", views.agent_detail),
 | 
			
		||||
    path("<int:pk>/meshcentral/", views.meshcentral),
 | 
			
		||||
    # agent views
 | 
			
		||||
    path("", views.GetAgents.as_view()),
 | 
			
		||||
    path("<agent:agent_id>/", views.GetUpdateDeleteAgent.as_view()),
 | 
			
		||||
    path("<agent:agent_id>/cmd/", views.send_raw_cmd),
 | 
			
		||||
    path("<agent:agent_id>/runscript/", views.run_script),
 | 
			
		||||
    path("<agent:agent_id>/wmi/", views.WMI.as_view()),
 | 
			
		||||
    path("<agent:agent_id>/recover/", views.recover),
 | 
			
		||||
    path("<agent:agent_id>/reboot/", views.Reboot.as_view()),
 | 
			
		||||
    path("<agent:agent_id>/ping/", views.ping),
 | 
			
		||||
    # alias for checks get view
 | 
			
		||||
    path("<agent:agent_id>/checks/", GetAddChecks.as_view()),
 | 
			
		||||
    # alias for autotasks get view
 | 
			
		||||
    path("<agent:agent_id>/tasks/", GetAddAutoTasks.as_view()),
 | 
			
		||||
    # alias for pending actions get view
 | 
			
		||||
    path("<agent:agent_id>/pendingactions/", PendingActions.as_view()),
 | 
			
		||||
    # agent remote background
 | 
			
		||||
    path("<agent:agent_id>/meshcentral/", views.AgentMeshCentral.as_view()),
 | 
			
		||||
    path("<agent:agent_id>/meshcentral/recover/", views.AgentMeshCentral.as_view()),
 | 
			
		||||
    path("<agent:agent_id>/processes/", views.AgentProcesses.as_view()),
 | 
			
		||||
    path("<agent:agent_id>/processes/<int:pid>/", views.AgentProcesses.as_view()),
 | 
			
		||||
    path("<agent:agent_id>/eventlog/<str:logtype>/<int:days>/", views.get_event_log),
 | 
			
		||||
    # agent history
 | 
			
		||||
    path("history/", views.AgentHistoryView.as_view()),
 | 
			
		||||
    path("<agent:agent_id>/history/", views.AgentHistoryView.as_view()),
 | 
			
		||||
    # agent notes
 | 
			
		||||
    path("notes/", views.GetAddNotes.as_view()),
 | 
			
		||||
    path("notes/<int:pk>/", views.GetEditDeleteNote.as_view()),
 | 
			
		||||
    path("<agent:agent_id>/notes/", views.GetAddNotes.as_view()),
 | 
			
		||||
    # bulk actions
 | 
			
		||||
    path("maintenance/bulk/", views.agent_maintenance),
 | 
			
		||||
    path("actions/bulk/", views.bulk),
 | 
			
		||||
    path("versions/", views.get_agent_versions),
 | 
			
		||||
    path("update/", views.update_agents),
 | 
			
		||||
    path("installer/", views.install_agent),
 | 
			
		||||
    path("<str:arch>/getmeshexe/", views.get_mesh_exe),
 | 
			
		||||
    path("uninstall/", views.uninstall),
 | 
			
		||||
    path("editagent/", views.edit_agent),
 | 
			
		||||
    path("<pk>/geteventlog/<logtype>/<days>/", views.get_event_log),
 | 
			
		||||
    path("getagentversions/", views.get_agent_versions),
 | 
			
		||||
    path("updateagents/", views.update_agents),
 | 
			
		||||
    path("<pk>/getprocs/", views.get_processes),
 | 
			
		||||
    path("<pk>/<pid>/killproc/", views.kill_proc),
 | 
			
		||||
    path("reboot/", views.Reboot.as_view()),
 | 
			
		||||
    path("installagent/", views.install_agent),
 | 
			
		||||
    path("<int:pk>/ping/", views.ping),
 | 
			
		||||
    path("recover/", views.recover),
 | 
			
		||||
    path("runscript/", views.run_script),
 | 
			
		||||
    path("<int:pk>/recovermesh/", views.recover_mesh),
 | 
			
		||||
    path("<int:pk>/notes/", views.GetAddNotes.as_view()),
 | 
			
		||||
    path("<int:pk>/note/", views.GetEditDeleteNote.as_view()),
 | 
			
		||||
    path("bulk/", views.bulk),
 | 
			
		||||
    path("maintenance/", views.agent_maintenance),
 | 
			
		||||
    path("<int:pk>/wmi/", views.WMI.as_view()),
 | 
			
		||||
]
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,9 @@
 | 
			
		||||
import random
 | 
			
		||||
import urllib.parse
 | 
			
		||||
 | 
			
		||||
import requests
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from core.models import CodeSignToken
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_exegen_url() -> str:
 | 
			
		||||
@@ -20,18 +21,20 @@ def get_exegen_url() -> str:
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_winagent_url(arch: str) -> str:
 | 
			
		||||
    from core.models import CodeSignToken
 | 
			
		||||
 | 
			
		||||
    dl_url = settings.DL_32 if arch == "32" else settings.DL_64
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        codetoken = CodeSignToken.objects.first().token
 | 
			
		||||
        base_url = get_exegen_url() + "/api/v1/winagents/?"
 | 
			
		||||
        params = {
 | 
			
		||||
            "version": settings.LATEST_AGENT_VER,
 | 
			
		||||
            "arch": arch,
 | 
			
		||||
            "token": codetoken,
 | 
			
		||||
        }
 | 
			
		||||
        dl_url = base_url + urllib.parse.urlencode(params)
 | 
			
		||||
        t: CodeSignToken = CodeSignToken.objects.first()  # type: ignore
 | 
			
		||||
        if t.is_valid:
 | 
			
		||||
            base_url = get_exegen_url() + "/api/v1/winagents/?"
 | 
			
		||||
            params = {
 | 
			
		||||
                "version": settings.LATEST_AGENT_VER,
 | 
			
		||||
                "arch": arch,
 | 
			
		||||
                "token": t.token,
 | 
			
		||||
            }
 | 
			
		||||
            dl_url = base_url + urllib.parse.urlencode(params)
 | 
			
		||||
    except:
 | 
			
		||||
        dl_url = settings.DL_64 if arch == "64" else settings.DL_32
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    return dl_url
 | 
			
		||||
 
 | 
			
		||||
@@ -8,40 +8,260 @@ import time
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.http import HttpResponse
 | 
			
		||||
from django.shortcuts import get_object_or_404
 | 
			
		||||
from loguru import logger
 | 
			
		||||
from django.db.models import Q
 | 
			
		||||
from packaging import version as pyver
 | 
			
		||||
from rest_framework import status
 | 
			
		||||
from rest_framework.decorators import api_view
 | 
			
		||||
from rest_framework.decorators import api_view, permission_classes
 | 
			
		||||
from rest_framework.permissions import IsAuthenticated
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
from rest_framework.views import APIView
 | 
			
		||||
from rest_framework.exceptions import PermissionDenied
 | 
			
		||||
 | 
			
		||||
from core.models import CoreSettings
 | 
			
		||||
from logs.models import AuditLog, PendingAction
 | 
			
		||||
from logs.models import AuditLog, DebugLog, PendingAction
 | 
			
		||||
from scripts.models import Script
 | 
			
		||||
from scripts.tasks import handle_bulk_command_task, handle_bulk_script_task
 | 
			
		||||
from tacticalrmm.utils import get_default_timezone, notify_error, reload_nats
 | 
			
		||||
from winupdate.serializers import WinUpdatePolicySerializer
 | 
			
		||||
from winupdate.tasks import bulk_check_for_updates_task, bulk_install_updates_task
 | 
			
		||||
from tacticalrmm.permissions import (
 | 
			
		||||
    _has_perm_on_agent,
 | 
			
		||||
    _has_perm_on_client,
 | 
			
		||||
    _has_perm_on_site,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
from .models import Agent, AgentCustomField, Note, RecoveryAction
 | 
			
		||||
from .models import Agent, AgentCustomField, Note, RecoveryAction, AgentHistory
 | 
			
		||||
from .permissions import (
 | 
			
		||||
    AgentHistoryPerms,
 | 
			
		||||
    AgentPerms,
 | 
			
		||||
    EvtLogPerms,
 | 
			
		||||
    InstallAgentPerms,
 | 
			
		||||
    RecoverAgentPerms,
 | 
			
		||||
    AgentNotesPerms,
 | 
			
		||||
    ManageProcPerms,
 | 
			
		||||
    MeshPerms,
 | 
			
		||||
    RebootAgentPerms,
 | 
			
		||||
    RunBulkPerms,
 | 
			
		||||
    RunScriptPerms,
 | 
			
		||||
    SendCMDPerms,
 | 
			
		||||
    PingAgentPerms,
 | 
			
		||||
    UpdateAgentPerms,
 | 
			
		||||
)
 | 
			
		||||
from .serializers import (
 | 
			
		||||
    AgentCustomFieldSerializer,
 | 
			
		||||
    AgentEditSerializer,
 | 
			
		||||
    AgentHistorySerializer,
 | 
			
		||||
    AgentHostnameSerializer,
 | 
			
		||||
    AgentOverdueActionSerializer,
 | 
			
		||||
    AgentSerializer,
 | 
			
		||||
    AgentTableSerializer,
 | 
			
		||||
    NoteSerializer,
 | 
			
		||||
    NotesSerializer,
 | 
			
		||||
    AgentNoteSerializer,
 | 
			
		||||
)
 | 
			
		||||
from .tasks import run_script_email_results_task, send_agent_update_task
 | 
			
		||||
 | 
			
		||||
logger.configure(**settings.LOG_CONFIG)
 | 
			
		||||
 | 
			
		||||
class GetAgents(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, AgentPerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request):
 | 
			
		||||
        if "site" in request.query_params.keys():
 | 
			
		||||
            filter = Q(site_id=request.query_params["site"])
 | 
			
		||||
        elif "client" in request.query_params.keys():
 | 
			
		||||
            filter = Q(site__client_id=request.query_params["client"])
 | 
			
		||||
        else:
 | 
			
		||||
            filter = Q()
 | 
			
		||||
 | 
			
		||||
        # by default detail=true
 | 
			
		||||
        if (
 | 
			
		||||
            "detail" not in request.query_params.keys()
 | 
			
		||||
            or "detail" in request.query_params.keys()
 | 
			
		||||
            and request.query_params["detail"] == "true"
 | 
			
		||||
        ):
 | 
			
		||||
            agents = (
 | 
			
		||||
                Agent.objects.filter_by_role(request.user)
 | 
			
		||||
                .select_related("site", "policy", "alert_template")
 | 
			
		||||
                .prefetch_related("agentchecks")
 | 
			
		||||
                .filter(filter)
 | 
			
		||||
                .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(agents, many=True, context=ctx)
 | 
			
		||||
 | 
			
		||||
        # if detail=false
 | 
			
		||||
        else:
 | 
			
		||||
            agents = (
 | 
			
		||||
                Agent.objects.filter_by_role(request.user)
 | 
			
		||||
                .select_related("site")
 | 
			
		||||
                .filter(filter)
 | 
			
		||||
                .only("agent_id", "hostname", "site")
 | 
			
		||||
            )
 | 
			
		||||
            serializer = AgentHostnameSerializer(agents, many=True)
 | 
			
		||||
 | 
			
		||||
        return Response(serializer.data)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view()
 | 
			
		||||
class GetUpdateDeleteAgent(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, AgentPerms]
 | 
			
		||||
 | 
			
		||||
    # get agent details
 | 
			
		||||
    def get(self, request, agent_id):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=agent_id)
 | 
			
		||||
        return Response(
 | 
			
		||||
            AgentSerializer(agent, context={"default_tz": get_default_timezone()}).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],
 | 
			
		||||
@@ -51,20 +271,26 @@ def get_agent_versions(request):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view(["POST"])
 | 
			
		||||
@permission_classes([IsAuthenticated, UpdateAgentPerms])
 | 
			
		||||
def update_agents(request):
 | 
			
		||||
    q = Agent.objects.filter(pk__in=request.data["pks"]).only("pk", "version")
 | 
			
		||||
    pks: list[int] = [
 | 
			
		||||
        i.pk
 | 
			
		||||
    q = (
 | 
			
		||||
        Agent.objects.filter_by_role(request.user)
 | 
			
		||||
        .filter(agent_id__in=request.data["agent_ids"])
 | 
			
		||||
        .only("agent_id", "version")
 | 
			
		||||
    )
 | 
			
		||||
    agent_ids: list[str] = [
 | 
			
		||||
        i.agent_id
 | 
			
		||||
        for i in q
 | 
			
		||||
        if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER)
 | 
			
		||||
    ]
 | 
			
		||||
    send_agent_update_task.delay(pks=pks)
 | 
			
		||||
    send_agent_update_task.delay(agent_ids=agent_ids)
 | 
			
		||||
    return Response("ok")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view()
 | 
			
		||||
def ping(request, pk):
 | 
			
		||||
    agent = get_object_or_404(Agent, pk=pk)
 | 
			
		||||
@api_view(["GET"])
 | 
			
		||||
@permission_classes([IsAuthenticated, PingAgentPerms])
 | 
			
		||||
def ping(request, agent_id):
 | 
			
		||||
    agent = get_object_or_404(Agent, agent_id=agent_id)
 | 
			
		||||
    status = "offline"
 | 
			
		||||
    attempts = 0
 | 
			
		||||
    while 1:
 | 
			
		||||
@@ -82,123 +308,12 @@ def ping(request, pk):
 | 
			
		||||
    return Response({"name": agent.hostname, "status": status})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view(["DELETE"])
 | 
			
		||||
def uninstall(request):
 | 
			
		||||
    agent = get_object_or_404(Agent, pk=request.data["pk"])
 | 
			
		||||
    asyncio.run(agent.nats_cmd({"func": "uninstall"}, wait=False))
 | 
			
		||||
 | 
			
		||||
    name = agent.hostname
 | 
			
		||||
    agent.delete()
 | 
			
		||||
    reload_nats()
 | 
			
		||||
    return Response(f"{name} will now be uninstalled.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view(["PATCH", "PUT"])
 | 
			
		||||
def edit_agent(request):
 | 
			
		||||
    agent = get_object_or_404(Agent, pk=request.data["id"])
 | 
			
		||||
 | 
			
		||||
    a_serializer = AgentSerializer(instance=agent, data=request.data, partial=True)
 | 
			
		||||
    a_serializer.is_valid(raise_exception=True)
 | 
			
		||||
    a_serializer.save()
 | 
			
		||||
 | 
			
		||||
    if "winupdatepolicy" in request.data.keys():
 | 
			
		||||
        policy = agent.winupdatepolicy.get()  # type: ignore
 | 
			
		||||
        p_serializer = WinUpdatePolicySerializer(
 | 
			
		||||
            instance=policy, data=request.data["winupdatepolicy"][0]
 | 
			
		||||
        )
 | 
			
		||||
        p_serializer.is_valid(raise_exception=True)
 | 
			
		||||
        p_serializer.save()
 | 
			
		||||
 | 
			
		||||
    if "custom_fields" in request.data.keys():
 | 
			
		||||
 | 
			
		||||
        for field in request.data["custom_fields"]:
 | 
			
		||||
 | 
			
		||||
            custom_field = field
 | 
			
		||||
            custom_field["agent"] = agent.id  # type: ignore
 | 
			
		||||
 | 
			
		||||
            if AgentCustomField.objects.filter(
 | 
			
		||||
                field=field["field"], agent=agent.id  # type: ignore
 | 
			
		||||
            ):
 | 
			
		||||
                value = AgentCustomField.objects.get(
 | 
			
		||||
                    field=field["field"], agent=agent.id  # type: ignore
 | 
			
		||||
                )
 | 
			
		||||
                serializer = AgentCustomFieldSerializer(
 | 
			
		||||
                    instance=value, data=custom_field
 | 
			
		||||
                )
 | 
			
		||||
                serializer.is_valid(raise_exception=True)
 | 
			
		||||
                serializer.save()
 | 
			
		||||
            else:
 | 
			
		||||
                serializer = AgentCustomFieldSerializer(data=custom_field)
 | 
			
		||||
                serializer.is_valid(raise_exception=True)
 | 
			
		||||
                serializer.save()
 | 
			
		||||
 | 
			
		||||
    return Response("ok")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view()
 | 
			
		||||
def meshcentral(request, pk):
 | 
			
		||||
    agent = get_object_or_404(Agent, pk=pk)
 | 
			
		||||
    core = CoreSettings.objects.first()
 | 
			
		||||
 | 
			
		||||
    token = agent.get_login_token(
 | 
			
		||||
        key=core.mesh_token, user=f"user//{core.mesh_username}"
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    if token == "err":
 | 
			
		||||
        return notify_error("Invalid mesh token")
 | 
			
		||||
 | 
			
		||||
    control = f"{core.mesh_site}/?login={token}&gotonode={agent.mesh_node_id}&viewmode=11&hide=31"
 | 
			
		||||
    terminal = f"{core.mesh_site}/?login={token}&gotonode={agent.mesh_node_id}&viewmode=12&hide=31"
 | 
			
		||||
    file = f"{core.mesh_site}/?login={token}&gotonode={agent.mesh_node_id}&viewmode=13&hide=31"
 | 
			
		||||
 | 
			
		||||
    AuditLog.audit_mesh_session(username=request.user.username, hostname=agent.hostname)
 | 
			
		||||
 | 
			
		||||
    ret = {
 | 
			
		||||
        "hostname": agent.hostname,
 | 
			
		||||
        "control": control,
 | 
			
		||||
        "terminal": terminal,
 | 
			
		||||
        "file": file,
 | 
			
		||||
        "status": agent.status,
 | 
			
		||||
        "client": agent.client.name,
 | 
			
		||||
        "site": agent.site.name,
 | 
			
		||||
    }
 | 
			
		||||
    return Response(ret)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view()
 | 
			
		||||
def agent_detail(request, pk):
 | 
			
		||||
    agent = get_object_or_404(Agent, pk=pk)
 | 
			
		||||
    return Response(AgentSerializer(agent).data)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view()
 | 
			
		||||
def get_processes(request, pk):
 | 
			
		||||
    agent = get_object_or_404(Agent, pk=pk)
 | 
			
		||||
    r = asyncio.run(agent.nats_cmd(data={"func": "procs"}, timeout=5))
 | 
			
		||||
    if r == "timeout":
 | 
			
		||||
        return notify_error("Unable to contact the agent")
 | 
			
		||||
    return Response(r)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view()
 | 
			
		||||
def kill_proc(request, pk, pid):
 | 
			
		||||
    agent = get_object_or_404(Agent, pk=pk)
 | 
			
		||||
    r = asyncio.run(
 | 
			
		||||
        agent.nats_cmd({"func": "killproc", "procpid": int(pid)}, timeout=15)
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    if r == "timeout":
 | 
			
		||||
        return notify_error("Unable to contact the agent")
 | 
			
		||||
    elif r != "ok":
 | 
			
		||||
        return notify_error(r)
 | 
			
		||||
 | 
			
		||||
    return Response("ok")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view()
 | 
			
		||||
def get_event_log(request, pk, logtype, days):
 | 
			
		||||
    agent = get_object_or_404(Agent, pk=pk)
 | 
			
		||||
@api_view(["GET"])
 | 
			
		||||
@permission_classes([IsAuthenticated, EvtLogPerms])
 | 
			
		||||
def get_event_log(request, agent_id, logtype, days):
 | 
			
		||||
    agent = get_object_or_404(Agent, agent_id=agent_id)
 | 
			
		||||
    timeout = 180 if logtype == "Security" else 30
 | 
			
		||||
 | 
			
		||||
    data = {
 | 
			
		||||
        "func": "eventlog",
 | 
			
		||||
        "timeout": timeout,
 | 
			
		||||
@@ -208,15 +323,16 @@ def get_event_log(request, pk, logtype, days):
 | 
			
		||||
        },
 | 
			
		||||
    }
 | 
			
		||||
    r = asyncio.run(agent.nats_cmd(data, timeout=timeout + 2))
 | 
			
		||||
    if r == "timeout":
 | 
			
		||||
    if r == "timeout" or r == "natsdown":
 | 
			
		||||
        return notify_error("Unable to contact the agent")
 | 
			
		||||
 | 
			
		||||
    return Response(r)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view(["POST"])
 | 
			
		||||
def send_raw_cmd(request):
 | 
			
		||||
    agent = get_object_or_404(Agent, pk=request.data["pk"])
 | 
			
		||||
@permission_classes([IsAuthenticated, SendCMDPerms])
 | 
			
		||||
def send_raw_cmd(request, agent_id):
 | 
			
		||||
    agent = get_object_or_404(Agent, agent_id=agent_id)
 | 
			
		||||
    timeout = int(request.data["timeout"])
 | 
			
		||||
    data = {
 | 
			
		||||
        "func": "rawcmd",
 | 
			
		||||
@@ -226,6 +342,16 @@ def send_raw_cmd(request):
 | 
			
		||||
            "shell": request.data["shell"],
 | 
			
		||||
        },
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if pyver.parse(agent.version) >= pyver.parse("1.6.0"):
 | 
			
		||||
        hist = AgentHistory.objects.create(
 | 
			
		||||
            agent=agent,
 | 
			
		||||
            type="cmd_run",
 | 
			
		||||
            command=request.data["cmd"],
 | 
			
		||||
            username=request.user.username[:50],
 | 
			
		||||
        )
 | 
			
		||||
        data["id"] = hist.pk
 | 
			
		||||
 | 
			
		||||
    r = asyncio.run(agent.nats_cmd(data, timeout=timeout + 2))
 | 
			
		||||
 | 
			
		||||
    if r == "timeout":
 | 
			
		||||
@@ -233,86 +359,20 @@ def send_raw_cmd(request):
 | 
			
		||||
 | 
			
		||||
    AuditLog.audit_raw_command(
 | 
			
		||||
        username=request.user.username,
 | 
			
		||||
        hostname=agent.hostname,
 | 
			
		||||
        agent=agent,
 | 
			
		||||
        cmd=request.data["cmd"],
 | 
			
		||||
        shell=request.data["shell"],
 | 
			
		||||
        debug_info={"ip": request._client_ip},
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    return Response(r)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AgentsTableList(APIView):
 | 
			
		||||
    def patch(self, request):
 | 
			
		||||
        if "sitePK" in request.data.keys():
 | 
			
		||||
            queryset = (
 | 
			
		||||
                Agent.objects.select_related("site", "policy", "alert_template")
 | 
			
		||||
                .prefetch_related("agentchecks")
 | 
			
		||||
                .filter(site_id=request.data["sitePK"])
 | 
			
		||||
            )
 | 
			
		||||
        elif "clientPK" in request.data.keys():
 | 
			
		||||
            queryset = (
 | 
			
		||||
                Agent.objects.select_related("site", "policy", "alert_template")
 | 
			
		||||
                .prefetch_related("agentchecks")
 | 
			
		||||
                .filter(site__client_id=request.data["clientPK"])
 | 
			
		||||
            )
 | 
			
		||||
        else:
 | 
			
		||||
            queryset = Agent.objects.select_related(
 | 
			
		||||
                "site", "policy", "alert_template"
 | 
			
		||||
            ).prefetch_related("agentchecks")
 | 
			
		||||
 | 
			
		||||
        queryset = queryset.only(
 | 
			
		||||
            "pk",
 | 
			
		||||
            "hostname",
 | 
			
		||||
            "agent_id",
 | 
			
		||||
            "site",
 | 
			
		||||
            "policy",
 | 
			
		||||
            "alert_template",
 | 
			
		||||
            "monitoring_type",
 | 
			
		||||
            "description",
 | 
			
		||||
            "needs_reboot",
 | 
			
		||||
            "overdue_text_alert",
 | 
			
		||||
            "overdue_email_alert",
 | 
			
		||||
            "overdue_time",
 | 
			
		||||
            "offline_time",
 | 
			
		||||
            "last_seen",
 | 
			
		||||
            "boot_time",
 | 
			
		||||
            "logged_in_username",
 | 
			
		||||
            "last_logged_in_user",
 | 
			
		||||
            "time_zone",
 | 
			
		||||
            "maintenance_mode",
 | 
			
		||||
        )
 | 
			
		||||
        ctx = {"default_tz": get_default_timezone()}
 | 
			
		||||
        serializer = AgentTableSerializer(queryset, many=True, context=ctx)
 | 
			
		||||
        return Response(serializer.data)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view()
 | 
			
		||||
def list_agents_no_detail(request):
 | 
			
		||||
    agents = Agent.objects.select_related("site").only("pk", "hostname", "site")
 | 
			
		||||
    return Response(AgentHostnameSerializer(agents, many=True).data)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view()
 | 
			
		||||
def agent_edit_details(request, pk):
 | 
			
		||||
    agent = get_object_or_404(Agent, pk=pk)
 | 
			
		||||
    return Response(AgentEditSerializer(agent).data)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view(["POST"])
 | 
			
		||||
def overdue_action(request):
 | 
			
		||||
    agent = get_object_or_404(Agent, pk=request.data["pk"])
 | 
			
		||||
    serializer = AgentOverdueActionSerializer(
 | 
			
		||||
        instance=agent, data=request.data, partial=True
 | 
			
		||||
    )
 | 
			
		||||
    serializer.is_valid(raise_exception=True)
 | 
			
		||||
    serializer.save()
 | 
			
		||||
    return Response(agent.hostname)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Reboot(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, RebootAgentPerms]
 | 
			
		||||
    # reboot now
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
        agent = get_object_or_404(Agent, pk=request.data["pk"])
 | 
			
		||||
    def post(self, request, agent_id):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=agent_id)
 | 
			
		||||
        r = asyncio.run(agent.nats_cmd({"func": "rebootnow"}, timeout=10))
 | 
			
		||||
        if r != "ok":
 | 
			
		||||
            return notify_error("Unable to contact the agent")
 | 
			
		||||
@@ -320,8 +380,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")
 | 
			
		||||
@@ -362,8 +422,10 @@ class Reboot(APIView):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view(["POST"])
 | 
			
		||||
@permission_classes([IsAuthenticated, InstallAgentPerms])
 | 
			
		||||
def install_agent(request):
 | 
			
		||||
    from knox.models import AuthToken
 | 
			
		||||
    from accounts.models import User
 | 
			
		||||
 | 
			
		||||
    from agents.utils import get_winagent_url
 | 
			
		||||
 | 
			
		||||
@@ -372,25 +434,34 @@ def install_agent(request):
 | 
			
		||||
    version = settings.LATEST_AGENT_VER
 | 
			
		||||
    arch = request.data["arch"]
 | 
			
		||||
 | 
			
		||||
    if not _has_perm_on_site(request.user, site_id):
 | 
			
		||||
        raise PermissionDenied()
 | 
			
		||||
 | 
			
		||||
    # response type is blob so we have to use
 | 
			
		||||
    # status codes and render error message on the frontend
 | 
			
		||||
    if arch == "64" and not os.path.exists(
 | 
			
		||||
        os.path.join(settings.EXE_DIR, "meshagent.exe")
 | 
			
		||||
    ):
 | 
			
		||||
        return Response(status=status.HTTP_406_NOT_ACCEPTABLE)
 | 
			
		||||
        return notify_error(
 | 
			
		||||
            "Missing 64 bit meshagent.exe. Upload it from Settings > Global Settings > MeshCentral"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    if arch == "32" and not os.path.exists(
 | 
			
		||||
        os.path.join(settings.EXE_DIR, "meshagent-x86.exe")
 | 
			
		||||
    ):
 | 
			
		||||
        return Response(status=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE)
 | 
			
		||||
        return notify_error(
 | 
			
		||||
            "Missing 32 bit meshagent.exe. Upload it from Settings > Global Settings > MeshCentral"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    inno = (
 | 
			
		||||
        f"winagent-v{version}.exe" if arch == "64" else f"winagent-v{version}-x86.exe"
 | 
			
		||||
    )
 | 
			
		||||
    download_url = get_winagent_url(arch)
 | 
			
		||||
 | 
			
		||||
    installer_user = User.objects.filter(is_installer_user=True).first()
 | 
			
		||||
 | 
			
		||||
    _, token = AuthToken.objects.create(
 | 
			
		||||
        user=request.user, expiry=dt.timedelta(hours=request.data["expires"])
 | 
			
		||||
        user=installer_user, expiry=dt.timedelta(hours=request.data["expires"])
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    if request.data["installMethod"] == "exe":
 | 
			
		||||
@@ -479,7 +550,7 @@ def install_agent(request):
 | 
			
		||||
            try:
 | 
			
		||||
                os.remove(ps1)
 | 
			
		||||
            except Exception as e:
 | 
			
		||||
                logger.error(str(e))
 | 
			
		||||
                DebugLog.error(message=str(e))
 | 
			
		||||
 | 
			
		||||
        with open(ps1, "w") as f:
 | 
			
		||||
            f.write(text)
 | 
			
		||||
@@ -497,8 +568,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
 | 
			
		||||
@@ -534,28 +606,44 @@ def recover(request):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view(["POST"])
 | 
			
		||||
def run_script(request):
 | 
			
		||||
    agent = get_object_or_404(Agent, pk=request.data["pk"])
 | 
			
		||||
    script = get_object_or_404(Script, pk=request.data["scriptPK"])
 | 
			
		||||
@permission_classes([IsAuthenticated, RunScriptPerms])
 | 
			
		||||
def run_script(request, agent_id):
 | 
			
		||||
    agent = get_object_or_404(Agent, agent_id=agent_id)
 | 
			
		||||
    script = get_object_or_404(Script, pk=request.data["script"])
 | 
			
		||||
    output = request.data["output"]
 | 
			
		||||
    args = request.data["args"]
 | 
			
		||||
    req_timeout = int(request.data["timeout"]) + 3
 | 
			
		||||
 | 
			
		||||
    AuditLog.audit_script_run(
 | 
			
		||||
        username=request.user.username,
 | 
			
		||||
        hostname=agent.hostname,
 | 
			
		||||
        agent=agent,
 | 
			
		||||
        script=script.name,
 | 
			
		||||
        debug_info={"ip": request._client_ip},
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    history_pk = 0
 | 
			
		||||
    if pyver.parse(agent.version) >= pyver.parse("1.6.0"):
 | 
			
		||||
        hist = AgentHistory.objects.create(
 | 
			
		||||
            agent=agent,
 | 
			
		||||
            type="script_run",
 | 
			
		||||
            script=script,
 | 
			
		||||
            username=request.user.username[:50],
 | 
			
		||||
        )
 | 
			
		||||
        history_pk = hist.pk
 | 
			
		||||
 | 
			
		||||
    if output == "wait":
 | 
			
		||||
        r = agent.run_script(
 | 
			
		||||
            scriptpk=script.pk, args=args, timeout=req_timeout, wait=True
 | 
			
		||||
            scriptpk=script.pk,
 | 
			
		||||
            args=args,
 | 
			
		||||
            timeout=req_timeout,
 | 
			
		||||
            wait=True,
 | 
			
		||||
            history_pk=history_pk,
 | 
			
		||||
        )
 | 
			
		||||
        return Response(r)
 | 
			
		||||
 | 
			
		||||
    elif output == "email":
 | 
			
		||||
        emails = (
 | 
			
		||||
            [] if request.data["emailmode"] == "default" else request.data["emails"]
 | 
			
		||||
            [] if request.data["emailMode"] == "default" else request.data["emails"]
 | 
			
		||||
        )
 | 
			
		||||
        run_script_email_results_task.delay(
 | 
			
		||||
            agentpk=agent.pk,
 | 
			
		||||
@@ -564,23 +652,55 @@ def run_script(request):
 | 
			
		||||
            emails=emails,
 | 
			
		||||
            args=args,
 | 
			
		||||
        )
 | 
			
		||||
    elif output == "collector":
 | 
			
		||||
        from core.models import CustomField
 | 
			
		||||
 | 
			
		||||
        r = agent.run_script(
 | 
			
		||||
            scriptpk=script.pk,
 | 
			
		||||
            args=args,
 | 
			
		||||
            timeout=req_timeout,
 | 
			
		||||
            wait=True,
 | 
			
		||||
            history_pk=history_pk,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        custom_field = CustomField.objects.get(pk=request.data["custom_field"])
 | 
			
		||||
 | 
			
		||||
        if custom_field.model == "agent":
 | 
			
		||||
            field = custom_field.get_or_create_field_value(agent)
 | 
			
		||||
        elif custom_field.model == "client":
 | 
			
		||||
            field = custom_field.get_or_create_field_value(agent.client)
 | 
			
		||||
        elif custom_field.model == "site":
 | 
			
		||||
            field = custom_field.get_or_create_field_value(agent.site)
 | 
			
		||||
        else:
 | 
			
		||||
            return notify_error("Custom Field was invalid")
 | 
			
		||||
 | 
			
		||||
        value = (
 | 
			
		||||
            r.strip()
 | 
			
		||||
            if request.data["save_all_output"]
 | 
			
		||||
            else r.strip().split("\n")[-1].strip()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        field.save_to_field(value)
 | 
			
		||||
        return Response(r)
 | 
			
		||||
    elif output == "note":
 | 
			
		||||
        r = agent.run_script(
 | 
			
		||||
            scriptpk=script.pk,
 | 
			
		||||
            args=args,
 | 
			
		||||
            timeout=req_timeout,
 | 
			
		||||
            wait=True,
 | 
			
		||||
            history_pk=history_pk,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        Note.objects.create(agent=agent, user=request.user, note=r)
 | 
			
		||||
        return Response(r)
 | 
			
		||||
    else:
 | 
			
		||||
        agent.run_script(scriptpk=script.pk, args=args, timeout=req_timeout)
 | 
			
		||||
        agent.run_script(
 | 
			
		||||
            scriptpk=script.pk, args=args, timeout=req_timeout, history_pk=history_pk
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    return Response(f"{script.name} will now be run on {agent.hostname}")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view()
 | 
			
		||||
def recover_mesh(request, pk):
 | 
			
		||||
    agent = get_object_or_404(Agent, pk=pk)
 | 
			
		||||
    data = {"func": "recover", "payload": {"mode": "mesh"}}
 | 
			
		||||
    r = asyncio.run(agent.nats_cmd(data, timeout=45))
 | 
			
		||||
    if r != "ok":
 | 
			
		||||
        return notify_error("Unable to contact the agent")
 | 
			
		||||
 | 
			
		||||
    return Response(f"Repaired mesh agent on {agent.hostname}")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view(["POST"])
 | 
			
		||||
def get_mesh_exe(request, arch):
 | 
			
		||||
    filename = "meshagent.exe" if arch == "64" else "meshagent-x86.exe"
 | 
			
		||||
@@ -603,49 +723,94 @@ def get_mesh_exe(request, arch):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetAddNotes(APIView):
 | 
			
		||||
    def get(self, request, pk):
 | 
			
		||||
        agent = get_object_or_404(Agent, pk=pk)
 | 
			
		||||
        return Response(NotesSerializer(agent).data)
 | 
			
		||||
    permission_classes = [IsAuthenticated, AgentNotesPerms]
 | 
			
		||||
 | 
			
		||||
    def post(self, request, pk):
 | 
			
		||||
        agent = get_object_or_404(Agent, pk=pk)
 | 
			
		||||
        serializer = NoteSerializer(data=request.data, partial=True)
 | 
			
		||||
    def get(self, request, agent_id=None):
 | 
			
		||||
        if agent_id:
 | 
			
		||||
            agent = get_object_or_404(Agent, agent_id=agent_id)
 | 
			
		||||
            notes = Note.objects.filter(agent=agent)
 | 
			
		||||
        else:
 | 
			
		||||
            notes = Note.objects.filter_by_role(request.user)
 | 
			
		||||
 | 
			
		||||
        return Response(AgentNoteSerializer(notes, many=True).data)
 | 
			
		||||
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
 | 
			
		||||
        if not _has_perm_on_agent(request.user, agent.agent_id):
 | 
			
		||||
            raise PermissionDenied()
 | 
			
		||||
 | 
			
		||||
        data = {
 | 
			
		||||
            "note": request.data["note"],
 | 
			
		||||
            "agent": agent.pk,
 | 
			
		||||
            "user": request.user.pk,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        serializer = AgentNoteSerializer(data=data)
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        serializer.save(agent=agent, user=request.user)
 | 
			
		||||
        serializer.save()
 | 
			
		||||
        return Response("Note added!")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetEditDeleteNote(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, AgentNotesPerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request, pk):
 | 
			
		||||
        note = get_object_or_404(Note, pk=pk)
 | 
			
		||||
        return Response(NoteSerializer(note).data)
 | 
			
		||||
 | 
			
		||||
    def patch(self, request, pk):
 | 
			
		||||
        if not _has_perm_on_agent(request.user, note.agent.agent_id):
 | 
			
		||||
            raise PermissionDenied()
 | 
			
		||||
 | 
			
		||||
        return Response(AgentNoteSerializer(note).data)
 | 
			
		||||
 | 
			
		||||
    def put(self, request, pk):
 | 
			
		||||
        note = get_object_or_404(Note, pk=pk)
 | 
			
		||||
        serializer = NoteSerializer(instance=note, data=request.data, partial=True)
 | 
			
		||||
 | 
			
		||||
        if not _has_perm_on_agent(request.user, note.agent.agent_id):
 | 
			
		||||
            raise PermissionDenied()
 | 
			
		||||
 | 
			
		||||
        serializer = AgentNoteSerializer(instance=note, data=request.data, partial=True)
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        serializer.save()
 | 
			
		||||
        return Response("Note edited!")
 | 
			
		||||
 | 
			
		||||
    def delete(self, request, pk):
 | 
			
		||||
        note = get_object_or_404(Note, pk=pk)
 | 
			
		||||
 | 
			
		||||
        if not _has_perm_on_agent(request.user, note.agent.agent_id):
 | 
			
		||||
            raise PermissionDenied()
 | 
			
		||||
 | 
			
		||||
        note.delete()
 | 
			
		||||
        return Response("Note was deleted!")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view(["POST"])
 | 
			
		||||
@permission_classes([IsAuthenticated, RunBulkPerms])
 | 
			
		||||
def bulk(request):
 | 
			
		||||
    if request.data["target"] == "agents" and not request.data["agentPKs"]:
 | 
			
		||||
    if request.data["target"] == "agents" and not request.data["agents"]:
 | 
			
		||||
        return notify_error("Must select at least 1 agent")
 | 
			
		||||
 | 
			
		||||
    if request.data["target"] == "client":
 | 
			
		||||
        q = Agent.objects.filter(site__client_id=request.data["client"])
 | 
			
		||||
        if not _has_perm_on_client(request.user, request.data["client"]):
 | 
			
		||||
            raise PermissionDenied()
 | 
			
		||||
        q = Agent.objects.filter_by_role(request.user).filter(
 | 
			
		||||
            site__client_id=request.data["client"]
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    elif request.data["target"] == "site":
 | 
			
		||||
        q = Agent.objects.filter(site_id=request.data["site"])
 | 
			
		||||
        if not _has_perm_on_site(request.user, request.data["site"]):
 | 
			
		||||
            raise PermissionDenied()
 | 
			
		||||
        q = Agent.objects.filter_by_role(request.user).filter(
 | 
			
		||||
            site_id=request.data["site"]
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    elif request.data["target"] == "agents":
 | 
			
		||||
        q = Agent.objects.filter(pk__in=request.data["agentPKs"])
 | 
			
		||||
        q = Agent.objects.filter_by_role(request.user).filter(
 | 
			
		||||
            agent_id__in=request.data["agents"]
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    elif request.data["target"] == "all":
 | 
			
		||||
        q = Agent.objects.only("pk", "monitoring_type")
 | 
			
		||||
        q = Agent.objects.filter_by_role(request.user).only("pk", "monitoring_type")
 | 
			
		||||
 | 
			
		||||
    else:
 | 
			
		||||
        return notify_error("Something went wrong")
 | 
			
		||||
 | 
			
		||||
@@ -656,60 +821,107 @@ def bulk(request):
 | 
			
		||||
 | 
			
		||||
    agents: list[int] = [agent.pk for agent in q]
 | 
			
		||||
 | 
			
		||||
    AuditLog.audit_bulk_action(request.user, request.data["mode"], request.data)
 | 
			
		||||
    if not agents:
 | 
			
		||||
        return notify_error("No agents where found meeting the selected criteria")
 | 
			
		||||
 | 
			
		||||
    AuditLog.audit_bulk_action(
 | 
			
		||||
        request.user,
 | 
			
		||||
        request.data["mode"],
 | 
			
		||||
        request.data,
 | 
			
		||||
        debug_info={"ip": request._client_ip},
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    if request.data["mode"] == "command":
 | 
			
		||||
        handle_bulk_command_task.delay(
 | 
			
		||||
            agents, request.data["cmd"], request.data["shell"], request.data["timeout"]
 | 
			
		||||
            agents,
 | 
			
		||||
            request.data["cmd"],
 | 
			
		||||
            request.data["shell"],
 | 
			
		||||
            request.data["timeout"],
 | 
			
		||||
            request.user.username[:50],
 | 
			
		||||
            run_on_offline=request.data["offlineAgents"],
 | 
			
		||||
        )
 | 
			
		||||
        return Response(f"Command will now be run on {len(agents)} agents")
 | 
			
		||||
 | 
			
		||||
    elif request.data["mode"] == "script":
 | 
			
		||||
        script = get_object_or_404(Script, pk=request.data["scriptPK"])
 | 
			
		||||
        script = get_object_or_404(Script, pk=request.data["script"])
 | 
			
		||||
        handle_bulk_script_task.delay(
 | 
			
		||||
            script.pk, agents, request.data["args"], request.data["timeout"]
 | 
			
		||||
            script.pk,
 | 
			
		||||
            agents,
 | 
			
		||||
            request.data["args"],
 | 
			
		||||
            request.data["timeout"],
 | 
			
		||||
            request.user.username[:50],
 | 
			
		||||
        )
 | 
			
		||||
        return Response(f"{script.name} will now be run on {len(agents)} agents")
 | 
			
		||||
 | 
			
		||||
    elif request.data["mode"] == "install":
 | 
			
		||||
        bulk_install_updates_task.delay(agents)
 | 
			
		||||
        return Response(
 | 
			
		||||
            f"Pending updates will now be installed on {len(agents)} agents"
 | 
			
		||||
        )
 | 
			
		||||
    elif request.data["mode"] == "scan":
 | 
			
		||||
        bulk_check_for_updates_task.delay(agents)
 | 
			
		||||
        return Response(f"Patch status scan will now run on {len(agents)} agents")
 | 
			
		||||
    elif request.data["mode"] == "patch":
 | 
			
		||||
 | 
			
		||||
        if request.data["patchMode"] == "install":
 | 
			
		||||
            bulk_install_updates_task.delay(agents)
 | 
			
		||||
            return Response(
 | 
			
		||||
                f"Pending updates will now be installed on {len(agents)} agents"
 | 
			
		||||
            )
 | 
			
		||||
        elif request.data["patchMode"] == "scan":
 | 
			
		||||
            bulk_check_for_updates_task.delay(agents)
 | 
			
		||||
            return Response(f"Patch status scan will now run on {len(agents)} agents")
 | 
			
		||||
 | 
			
		||||
    return notify_error("Something went wrong")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view(["POST"])
 | 
			
		||||
@permission_classes([IsAuthenticated, AgentPerms])
 | 
			
		||||
def agent_maintenance(request):
 | 
			
		||||
 | 
			
		||||
    if request.data["type"] == "Client":
 | 
			
		||||
        Agent.objects.filter(site__client_id=request.data["id"]).update(
 | 
			
		||||
            maintenance_mode=request.data["action"]
 | 
			
		||||
        if not _has_perm_on_client(request.user, request.data["id"]):
 | 
			
		||||
            raise PermissionDenied()
 | 
			
		||||
 | 
			
		||||
        count = (
 | 
			
		||||
            Agent.objects.filter_by_role(request.user)
 | 
			
		||||
            .filter(site__client_id=request.data["id"])
 | 
			
		||||
            .update(maintenance_mode=request.data["action"])
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    elif request.data["type"] == "Site":
 | 
			
		||||
        Agent.objects.filter(site_id=request.data["id"]).update(
 | 
			
		||||
            maintenance_mode=request.data["action"]
 | 
			
		||||
        )
 | 
			
		||||
        if not _has_perm_on_site(request.user, request.data["id"]):
 | 
			
		||||
            raise PermissionDenied()
 | 
			
		||||
 | 
			
		||||
    elif request.data["type"] == "Agent":
 | 
			
		||||
        agent = Agent.objects.get(pk=request.data["id"])
 | 
			
		||||
        agent.maintenance_mode = request.data["action"]
 | 
			
		||||
        agent.save(update_fields=["maintenance_mode"])
 | 
			
		||||
        count = (
 | 
			
		||||
            Agent.objects.filter_by_role(request.user)
 | 
			
		||||
            .filter(site_id=request.data["id"])
 | 
			
		||||
            .update(maintenance_mode=request.data["action"])
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    else:
 | 
			
		||||
        return notify_error("Invalid data")
 | 
			
		||||
 | 
			
		||||
    return Response("ok")
 | 
			
		||||
    if count:
 | 
			
		||||
        action = "disabled" if not request.data["action"] else "enabled"
 | 
			
		||||
        return Response(f"Maintenance mode has been {action} on {count} agents")
 | 
			
		||||
    else:
 | 
			
		||||
        return Response(
 | 
			
		||||
            f"No agents have been put in maintenance mode. You might not have permissions to the resources."
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class WMI(APIView):
 | 
			
		||||
    def get(self, request, pk):
 | 
			
		||||
        agent = get_object_or_404(Agent, pk=pk)
 | 
			
		||||
    permission_classes = [IsAuthenticated, AgentPerms]
 | 
			
		||||
 | 
			
		||||
    def post(self, request, agent_id):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=agent_id)
 | 
			
		||||
        r = asyncio.run(agent.nats_cmd({"func": "sysinfo"}, timeout=20))
 | 
			
		||||
        if r != "ok":
 | 
			
		||||
            return notify_error("Unable to contact the agent")
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
        return Response("Agent WMI data refreshed successfully")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AgentHistoryView(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, AgentHistoryPerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request, agent_id=None):
 | 
			
		||||
        if agent_id:
 | 
			
		||||
            agent = get_object_or_404(Agent, agent_id=agent_id)
 | 
			
		||||
            history = AgentHistory.objects.filter(agent=agent)
 | 
			
		||||
        else:
 | 
			
		||||
            history = AgentHistory.objects.filter_by_role(request.user)
 | 
			
		||||
        ctx = {"default_tz": get_default_timezone()}
 | 
			
		||||
        return Response(AgentHistorySerializer(history, many=True, context=ctx).data)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										33
									
								
								api/tacticalrmm/alerts/migrations/0007_auto_20210721_0423.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								api/tacticalrmm/alerts/migrations/0007_auto_20210721_0423.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,33 @@
 | 
			
		||||
# Generated by Django 3.2.1 on 2021-07-21 04:23
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('alerts', '0006_auto_20210217_1736'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='alerttemplate',
 | 
			
		||||
            name='created_by',
 | 
			
		||||
            field=models.CharField(blank=True, max_length=100, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='alerttemplate',
 | 
			
		||||
            name='created_time',
 | 
			
		||||
            field=models.DateTimeField(auto_now_add=True, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='alerttemplate',
 | 
			
		||||
            name='modified_by',
 | 
			
		||||
            field=models.CharField(blank=True, max_length=100, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='alerttemplate',
 | 
			
		||||
            name='modified_time',
 | 
			
		||||
            field=models.DateTimeField(auto_now=True, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										28
									
								
								api/tacticalrmm/alerts/migrations/0008_auto_20210721_1757.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								api/tacticalrmm/alerts/migrations/0008_auto_20210721_1757.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,28 @@
 | 
			
		||||
# Generated by Django 3.2.1 on 2021-07-21 17:57
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('alerts', '0007_auto_20210721_0423'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='alerttemplate',
 | 
			
		||||
            name='agent_script_actions',
 | 
			
		||||
            field=models.BooleanField(blank=True, default=None, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='alerttemplate',
 | 
			
		||||
            name='check_script_actions',
 | 
			
		||||
            field=models.BooleanField(blank=True, default=None, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='alerttemplate',
 | 
			
		||||
            name='task_script_actions',
 | 
			
		||||
            field=models.BooleanField(blank=True, default=None, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										28
									
								
								api/tacticalrmm/alerts/migrations/0009_auto_20210721_1810.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								api/tacticalrmm/alerts/migrations/0009_auto_20210721_1810.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,28 @@
 | 
			
		||||
# Generated by Django 3.2.1 on 2021-07-21 18:10
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('alerts', '0008_auto_20210721_1757'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='alerttemplate',
 | 
			
		||||
            name='agent_script_actions',
 | 
			
		||||
            field=models.BooleanField(blank=True, default=True, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='alerttemplate',
 | 
			
		||||
            name='check_script_actions',
 | 
			
		||||
            field=models.BooleanField(blank=True, default=True, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='alerttemplate',
 | 
			
		||||
            name='task_script_actions',
 | 
			
		||||
            field=models.BooleanField(blank=True, default=True, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										23
									
								
								api/tacticalrmm/alerts/migrations/0010_auto_20210917_1954.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								api/tacticalrmm/alerts/migrations/0010_auto_20210917_1954.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
# Generated by Django 3.2.6 on 2021-09-17 19:54
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("alerts", "0009_auto_20210721_1810"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="alerttemplate",
 | 
			
		||||
            name="created_by",
 | 
			
		||||
            field=models.CharField(blank=True, max_length=255, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="alerttemplate",
 | 
			
		||||
            name="modified_by",
 | 
			
		||||
            field=models.CharField(blank=True, max_length=255, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -3,19 +3,19 @@ from __future__ import annotations
 | 
			
		||||
import re
 | 
			
		||||
from typing import TYPE_CHECKING, Union
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.contrib.postgres.fields import ArrayField
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.db.models.fields import BooleanField, PositiveIntegerField
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from loguru import logger
 | 
			
		||||
 | 
			
		||||
from logs.models import BaseAuditModel, DebugLog
 | 
			
		||||
from tacticalrmm.models import PermissionQuerySet
 | 
			
		||||
 | 
			
		||||
if TYPE_CHECKING:
 | 
			
		||||
    from agents.models import Agent
 | 
			
		||||
    from autotasks.models import AutomatedTask
 | 
			
		||||
    from checks.models import Check
 | 
			
		||||
 | 
			
		||||
logger.configure(**settings.LOG_CONFIG)
 | 
			
		||||
 | 
			
		||||
SEVERITY_CHOICES = [
 | 
			
		||||
    ("info", "Informational"),
 | 
			
		||||
@@ -32,6 +32,8 @@ ALERT_TYPE_CHOICES = [
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Alert(models.Model):
 | 
			
		||||
    objects = PermissionQuerySet.as_manager()
 | 
			
		||||
 | 
			
		||||
    agent = models.ForeignKey(
 | 
			
		||||
        "agents.Agent",
 | 
			
		||||
        related_name="agent",
 | 
			
		||||
@@ -173,6 +175,7 @@ class Alert(models.Model):
 | 
			
		||||
                always_email = alert_template.agent_always_email
 | 
			
		||||
                always_text = alert_template.agent_always_text
 | 
			
		||||
                alert_interval = alert_template.agent_periodic_alert_days
 | 
			
		||||
                run_script_action = alert_template.agent_script_actions
 | 
			
		||||
 | 
			
		||||
            if instance.should_create_alert(alert_template):
 | 
			
		||||
                alert = cls.create_or_return_availability_alert(instance)
 | 
			
		||||
@@ -209,6 +212,7 @@ class Alert(models.Model):
 | 
			
		||||
                always_email = alert_template.check_always_email
 | 
			
		||||
                always_text = alert_template.check_always_text
 | 
			
		||||
                alert_interval = alert_template.check_periodic_alert_days
 | 
			
		||||
                run_script_action = alert_template.check_script_actions
 | 
			
		||||
 | 
			
		||||
            if instance.should_create_alert(alert_template):
 | 
			
		||||
                alert = cls.create_or_return_check_alert(instance)
 | 
			
		||||
@@ -242,6 +246,7 @@ class Alert(models.Model):
 | 
			
		||||
                always_email = alert_template.task_always_email
 | 
			
		||||
                always_text = alert_template.task_always_text
 | 
			
		||||
                alert_interval = alert_template.task_periodic_alert_days
 | 
			
		||||
                run_script_action = alert_template.task_script_actions
 | 
			
		||||
 | 
			
		||||
            if instance.should_create_alert(alert_template):
 | 
			
		||||
                alert = cls.create_or_return_task_alert(instance)
 | 
			
		||||
@@ -295,7 +300,7 @@ class Alert(models.Model):
 | 
			
		||||
                text_task.delay(pk=alert.pk, alert_interval=alert_interval)
 | 
			
		||||
 | 
			
		||||
        # check if any scripts should be run
 | 
			
		||||
        if alert_template and alert_template.action and not alert.action_run:
 | 
			
		||||
        if alert_template and alert_template.action and run_script_action and not alert.action_run:  # type: ignore
 | 
			
		||||
            r = agent.run_script(
 | 
			
		||||
                scriptpk=alert_template.action.pk,
 | 
			
		||||
                args=alert.parse_script_args(alert_template.action_args),
 | 
			
		||||
@@ -314,8 +319,10 @@ class Alert(models.Model):
 | 
			
		||||
                alert.action_run = djangotime.now()
 | 
			
		||||
                alert.save()
 | 
			
		||||
            else:
 | 
			
		||||
                logger.error(
 | 
			
		||||
                    f"Failure action: {alert_template.action.name} failed to run on any agent for {agent.hostname} failure alert"
 | 
			
		||||
                DebugLog.error(
 | 
			
		||||
                    agent=agent,
 | 
			
		||||
                    log_type="scripting",
 | 
			
		||||
                    message=f"Failure action: {alert_template.action.name} failed to run on any agent for {agent.hostname}({agent.pk}) failure alert",
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
@@ -345,6 +352,7 @@ class Alert(models.Model):
 | 
			
		||||
            if alert_template:
 | 
			
		||||
                email_on_resolved = alert_template.agent_email_on_resolved
 | 
			
		||||
                text_on_resolved = alert_template.agent_text_on_resolved
 | 
			
		||||
                run_script_action = alert_template.agent_script_actions
 | 
			
		||||
 | 
			
		||||
        elif isinstance(instance, Check):
 | 
			
		||||
            from checks.tasks import (
 | 
			
		||||
@@ -363,6 +371,7 @@ class Alert(models.Model):
 | 
			
		||||
            if alert_template:
 | 
			
		||||
                email_on_resolved = alert_template.check_email_on_resolved
 | 
			
		||||
                text_on_resolved = alert_template.check_text_on_resolved
 | 
			
		||||
                run_script_action = alert_template.check_script_actions
 | 
			
		||||
 | 
			
		||||
        elif isinstance(instance, AutomatedTask):
 | 
			
		||||
            from autotasks.tasks import (
 | 
			
		||||
@@ -381,6 +390,7 @@ class Alert(models.Model):
 | 
			
		||||
            if alert_template:
 | 
			
		||||
                email_on_resolved = alert_template.task_email_on_resolved
 | 
			
		||||
                text_on_resolved = alert_template.task_text_on_resolved
 | 
			
		||||
                run_script_action = alert_template.task_script_actions
 | 
			
		||||
 | 
			
		||||
        else:
 | 
			
		||||
            return
 | 
			
		||||
@@ -403,6 +413,7 @@ class Alert(models.Model):
 | 
			
		||||
        if (
 | 
			
		||||
            alert_template
 | 
			
		||||
            and alert_template.resolved_action
 | 
			
		||||
            and run_script_action  # type: ignore
 | 
			
		||||
            and not alert.resolved_action_run
 | 
			
		||||
        ):
 | 
			
		||||
            r = agent.run_script(
 | 
			
		||||
@@ -425,8 +436,10 @@ class Alert(models.Model):
 | 
			
		||||
                alert.resolved_action_run = djangotime.now()
 | 
			
		||||
                alert.save()
 | 
			
		||||
            else:
 | 
			
		||||
                logger.error(
 | 
			
		||||
                    f"Resolved action: {alert_template.action.name} failed to run on any agent for {agent.hostname} resolved alert"
 | 
			
		||||
                DebugLog.error(
 | 
			
		||||
                    agent=agent,
 | 
			
		||||
                    log_type="scripting",
 | 
			
		||||
                    message=f"Resolved action: {alert_template.action.name} failed to run on any agent for {agent.hostname}({agent.pk}) resolved alert",
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
    def parse_script_args(self, args: list[str]):
 | 
			
		||||
@@ -444,14 +457,14 @@ class Alert(models.Model):
 | 
			
		||||
                name = match.group(1)
 | 
			
		||||
 | 
			
		||||
                if hasattr(self, name):
 | 
			
		||||
                    value = getattr(self, name)
 | 
			
		||||
                    value = f"'{getattr(self, name)}'"
 | 
			
		||||
                else:
 | 
			
		||||
                    continue
 | 
			
		||||
 | 
			
		||||
                try:
 | 
			
		||||
                    temp_args.append(re.sub("\\{\\{.*\\}\\}", "'" + value + "'", arg))  # type: ignore
 | 
			
		||||
                    temp_args.append(re.sub("\\{\\{.*\\}\\}", value, arg))  # type: ignore
 | 
			
		||||
                except Exception as e:
 | 
			
		||||
                    logger.error(e)
 | 
			
		||||
                    DebugLog.error(log_type="scripting", message=e)
 | 
			
		||||
                    continue
 | 
			
		||||
 | 
			
		||||
            else:
 | 
			
		||||
@@ -460,7 +473,7 @@ class Alert(models.Model):
 | 
			
		||||
        return temp_args
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AlertTemplate(models.Model):
 | 
			
		||||
class AlertTemplate(BaseAuditModel):
 | 
			
		||||
    name = models.CharField(max_length=100)
 | 
			
		||||
    is_active = models.BooleanField(default=True)
 | 
			
		||||
 | 
			
		||||
@@ -517,6 +530,7 @@ class AlertTemplate(models.Model):
 | 
			
		||||
    agent_always_text = BooleanField(null=True, blank=True, default=None)
 | 
			
		||||
    agent_always_alert = BooleanField(null=True, blank=True, default=None)
 | 
			
		||||
    agent_periodic_alert_days = PositiveIntegerField(blank=True, null=True, default=0)
 | 
			
		||||
    agent_script_actions = BooleanField(null=True, blank=True, default=True)
 | 
			
		||||
 | 
			
		||||
    # check alert settings
 | 
			
		||||
    check_email_alert_severity = ArrayField(
 | 
			
		||||
@@ -540,6 +554,7 @@ class AlertTemplate(models.Model):
 | 
			
		||||
    check_always_text = BooleanField(null=True, blank=True, default=None)
 | 
			
		||||
    check_always_alert = BooleanField(null=True, blank=True, default=None)
 | 
			
		||||
    check_periodic_alert_days = PositiveIntegerField(blank=True, null=True, default=0)
 | 
			
		||||
    check_script_actions = BooleanField(null=True, blank=True, default=True)
 | 
			
		||||
 | 
			
		||||
    # task alert settings
 | 
			
		||||
    task_email_alert_severity = ArrayField(
 | 
			
		||||
@@ -563,6 +578,7 @@ class AlertTemplate(models.Model):
 | 
			
		||||
    task_always_text = BooleanField(null=True, blank=True, default=None)
 | 
			
		||||
    task_always_alert = BooleanField(null=True, blank=True, default=None)
 | 
			
		||||
    task_periodic_alert_days = PositiveIntegerField(blank=True, null=True, default=0)
 | 
			
		||||
    task_script_actions = BooleanField(null=True, blank=True, default=True)
 | 
			
		||||
 | 
			
		||||
    # exclusion settings
 | 
			
		||||
    exclude_workstations = BooleanField(null=True, blank=True, default=False)
 | 
			
		||||
@@ -581,6 +597,13 @@ class AlertTemplate(models.Model):
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return self.name
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def serialize(alert_template):
 | 
			
		||||
        # serializes the agent and returns json
 | 
			
		||||
        from .serializers import AlertTemplateAuditSerializer
 | 
			
		||||
 | 
			
		||||
        return AlertTemplateAuditSerializer(alert_template).data
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def has_agent_settings(self) -> bool:
 | 
			
		||||
        return (
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										55
									
								
								api/tacticalrmm/alerts/permissions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								api/tacticalrmm/alerts/permissions.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,55 @@
 | 
			
		||||
from django.shortcuts import get_object_or_404
 | 
			
		||||
from rest_framework import permissions
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.permissions import _has_perm, _has_perm_on_agent
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _has_perm_on_alert(user, id: int):
 | 
			
		||||
    from alerts.models import Alert
 | 
			
		||||
 | 
			
		||||
    role = user.role
 | 
			
		||||
    if user.is_superuser or (role and getattr(role, "is_superuser")):
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
    # make sure non-superusers with empty roles aren't permitted
 | 
			
		||||
    elif not role:
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
    alert = get_object_or_404(Alert, id=id)
 | 
			
		||||
 | 
			
		||||
    if alert.agent:
 | 
			
		||||
        agent_id = alert.agent.agent_id
 | 
			
		||||
    elif alert.assigned_check:
 | 
			
		||||
        agent_id = alert.assigned_check.agent.agent_id
 | 
			
		||||
    elif alert.assigned_task:
 | 
			
		||||
        agent_id = alert.assigned_task.agent.agent_id
 | 
			
		||||
    else:
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
    return _has_perm_on_agent(user, agent_id)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AlertPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        if r.method == "GET" or r.method == "PATCH":
 | 
			
		||||
            if "pk" in view.kwargs.keys():
 | 
			
		||||
                return _has_perm(r, "can_list_alerts") and _has_perm_on_alert(
 | 
			
		||||
                    r.user, view.kwargs["pk"]
 | 
			
		||||
                )
 | 
			
		||||
            else:
 | 
			
		||||
                return _has_perm(r, "can_list_alerts")
 | 
			
		||||
        else:
 | 
			
		||||
            if "pk" in view.kwargs.keys():
 | 
			
		||||
                return _has_perm(r, "can_manage_alerts") and _has_perm_on_alert(
 | 
			
		||||
                    r.user, view.kwargs["pk"]
 | 
			
		||||
                )
 | 
			
		||||
            else:
 | 
			
		||||
                return _has_perm(r, "can_manage_alerts")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AlertTemplatePerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        if r.method == "GET":
 | 
			
		||||
            return _has_perm(r, "can_list_alerttemplates")
 | 
			
		||||
        else:
 | 
			
		||||
            return _has_perm(r, "can_manage_alerttemplates")
 | 
			
		||||
@@ -2,7 +2,7 @@ from rest_framework.fields import SerializerMethodField
 | 
			
		||||
from rest_framework.serializers import ModelSerializer, ReadOnlyField
 | 
			
		||||
 | 
			
		||||
from automation.serializers import PolicySerializer
 | 
			
		||||
from clients.serializers import ClientSerializer, SiteSerializer
 | 
			
		||||
from clients.serializers import ClientMinimumSerializer, SiteMinimumSerializer
 | 
			
		||||
from tacticalrmm.utils import get_default_timezone
 | 
			
		||||
 | 
			
		||||
from .models import Alert, AlertTemplate
 | 
			
		||||
@@ -113,9 +113,15 @@ class AlertTemplateSerializer(ModelSerializer):
 | 
			
		||||
 | 
			
		||||
class AlertTemplateRelationSerializer(ModelSerializer):
 | 
			
		||||
    policies = PolicySerializer(read_only=True, many=True)
 | 
			
		||||
    clients = ClientSerializer(read_only=True, many=True)
 | 
			
		||||
    sites = SiteSerializer(read_only=True, many=True)
 | 
			
		||||
    clients = ClientMinimumSerializer(read_only=True, many=True)
 | 
			
		||||
    sites = SiteMinimumSerializer(read_only=True, many=True)
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = AlertTemplate
 | 
			
		||||
        fields = "__all__"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AlertTemplateAuditSerializer(ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = AlertTemplate
 | 
			
		||||
        fields = "__all__"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,10 @@
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
 | 
			
		||||
from alerts.models import Alert
 | 
			
		||||
from tacticalrmm.celery import app
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
def unsnooze_alerts() -> str:
 | 
			
		||||
    from .models import Alert
 | 
			
		||||
 | 
			
		||||
    Alert.objects.filter(snoozed=True, snooze_until__lte=djangotime.now()).update(
 | 
			
		||||
        snoozed=False, snooze_until=None
 | 
			
		||||
@@ -22,3 +21,14 @@ def cache_agents_alert_template():
 | 
			
		||||
        agent.set_alert_template()
 | 
			
		||||
 | 
			
		||||
    return "ok"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
def prune_resolved_alerts(older_than_days: int) -> str:
 | 
			
		||||
    from .models import Alert
 | 
			
		||||
 | 
			
		||||
    Alert.objects.filter(resolved=True).filter(
 | 
			
		||||
        alert_time__lt=djangotime.now() - djangotime.timedelta(days=older_than_days)
 | 
			
		||||
    ).delete()
 | 
			
		||||
 | 
			
		||||
    return "ok"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,14 @@
 | 
			
		||||
from datetime import datetime, timedelta
 | 
			
		||||
from unittest.mock import patch
 | 
			
		||||
from itertools import cycle
 | 
			
		||||
 | 
			
		||||
from core.models import CoreSettings
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from model_bakery import baker, seq
 | 
			
		||||
from tacticalrmm.test import TacticalTestCase
 | 
			
		||||
 | 
			
		||||
from alerts.tasks import cache_agents_alert_template
 | 
			
		||||
from autotasks.models import AutomatedTask
 | 
			
		||||
from core.models import CoreSettings
 | 
			
		||||
from tacticalrmm.test import TacticalTestCase
 | 
			
		||||
 | 
			
		||||
from .models import Alert, AlertTemplate
 | 
			
		||||
from .serializers import (
 | 
			
		||||
@@ -17,6 +17,8 @@ from .serializers import (
 | 
			
		||||
    AlertTemplateSerializer,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
base_url = "/alerts"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestAlertsViews(TacticalTestCase):
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
@@ -24,7 +26,7 @@ class TestAlertsViews(TacticalTestCase):
 | 
			
		||||
        self.setup_coresettings()
 | 
			
		||||
 | 
			
		||||
    def test_get_alerts(self):
 | 
			
		||||
        url = "/alerts/alerts/"
 | 
			
		||||
        url = "/alerts/"
 | 
			
		||||
 | 
			
		||||
        # create check, task, and agent to test each serializer function
 | 
			
		||||
        check = baker.make_recipe("checks.diskspace_check")
 | 
			
		||||
@@ -117,7 +119,7 @@ class TestAlertsViews(TacticalTestCase):
 | 
			
		||||
        self.check_not_authenticated("patch", url)
 | 
			
		||||
 | 
			
		||||
    def test_add_alert(self):
 | 
			
		||||
        url = "/alerts/alerts/"
 | 
			
		||||
        url = "/alerts/"
 | 
			
		||||
 | 
			
		||||
        agent = baker.make_recipe("agents.agent")
 | 
			
		||||
        data = {
 | 
			
		||||
@@ -134,11 +136,11 @@ class TestAlertsViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
    def test_get_alert(self):
 | 
			
		||||
        # returns 404 for invalid alert pk
 | 
			
		||||
        resp = self.client.get("/alerts/alerts/500/", format="json")
 | 
			
		||||
        resp = self.client.get("/alerts/500/", format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 404)
 | 
			
		||||
 | 
			
		||||
        alert = baker.make("alerts.Alert")
 | 
			
		||||
        url = f"/alerts/alerts/{alert.pk}/"  # type: ignore
 | 
			
		||||
        url = f"/alerts/{alert.pk}/"  # type: ignore
 | 
			
		||||
 | 
			
		||||
        resp = self.client.get(url, format="json")
 | 
			
		||||
        serializer = AlertSerializer(alert)
 | 
			
		||||
@@ -150,16 +152,15 @@ class TestAlertsViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
    def test_update_alert(self):
 | 
			
		||||
        # returns 404 for invalid alert pk
 | 
			
		||||
        resp = self.client.put("/alerts/alerts/500/", format="json")
 | 
			
		||||
        resp = self.client.put("/alerts/500/", format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 404)
 | 
			
		||||
 | 
			
		||||
        alert = baker.make("alerts.Alert", resolved=False, snoozed=False)
 | 
			
		||||
 | 
			
		||||
        url = f"/alerts/alerts/{alert.pk}/"  # type: ignore
 | 
			
		||||
        url = f"/alerts/{alert.pk}/"  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # test resolving alert
 | 
			
		||||
        data = {
 | 
			
		||||
            "id": alert.pk,  # type: ignore
 | 
			
		||||
            "type": "resolve",
 | 
			
		||||
        }
 | 
			
		||||
        resp = self.client.put(url, data, format="json")
 | 
			
		||||
@@ -168,26 +169,26 @@ class TestAlertsViews(TacticalTestCase):
 | 
			
		||||
        self.assertTrue(Alert.objects.get(pk=alert.pk).resolved_on)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # test snoozing alert
 | 
			
		||||
        data = {"id": alert.pk, "type": "snooze", "snooze_days": "30"}  # type: ignore
 | 
			
		||||
        data = {"type": "snooze", "snooze_days": "30"}  # type: ignore
 | 
			
		||||
        resp = self.client.put(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertTrue(Alert.objects.get(pk=alert.pk).snoozed)  # type: ignore
 | 
			
		||||
        self.assertTrue(Alert.objects.get(pk=alert.pk).snooze_until)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # test snoozing alert without snooze_days
 | 
			
		||||
        data = {"id": alert.pk, "type": "snooze"}  # type: ignore
 | 
			
		||||
        data = {"type": "snooze"}  # type: ignore
 | 
			
		||||
        resp = self.client.put(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 400)
 | 
			
		||||
 | 
			
		||||
        # test unsnoozing alert
 | 
			
		||||
        data = {"id": alert.pk, "type": "unsnooze"}  # type: ignore
 | 
			
		||||
        data = {"type": "unsnooze"}  # type: ignore
 | 
			
		||||
        resp = self.client.put(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertFalse(Alert.objects.get(pk=alert.pk).snoozed)  # type: ignore
 | 
			
		||||
        self.assertFalse(Alert.objects.get(pk=alert.pk).snooze_until)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # test invalid type
 | 
			
		||||
        data = {"id": alert.pk, "type": "invalid"}  # type: ignore
 | 
			
		||||
        data = {"type": "invalid"}  # type: ignore
 | 
			
		||||
        resp = self.client.put(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 400)
 | 
			
		||||
 | 
			
		||||
@@ -195,13 +196,13 @@ class TestAlertsViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
    def test_delete_alert(self):
 | 
			
		||||
        # returns 404 for invalid alert pk
 | 
			
		||||
        resp = self.client.put("/alerts/alerts/500/", format="json")
 | 
			
		||||
        resp = self.client.put("/alerts/500/", format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 404)
 | 
			
		||||
 | 
			
		||||
        alert = baker.make("alerts.Alert")
 | 
			
		||||
 | 
			
		||||
        # test delete alert
 | 
			
		||||
        url = f"/alerts/alerts/{alert.pk}/"  # type: ignore
 | 
			
		||||
        url = f"/alerts/{alert.pk}/"  # type: ignore
 | 
			
		||||
        resp = self.client.delete(url, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
@@ -243,7 +244,7 @@ class TestAlertsViews(TacticalTestCase):
 | 
			
		||||
        self.assertTrue(Alert.objects.filter(snoozed=False).exists())
 | 
			
		||||
 | 
			
		||||
    def test_get_alert_templates(self):
 | 
			
		||||
        url = "/alerts/alerttemplates/"
 | 
			
		||||
        url = "/alerts/templates/"
 | 
			
		||||
 | 
			
		||||
        alert_templates = baker.make("alerts.AlertTemplate", _quantity=3)
 | 
			
		||||
        resp = self.client.get(url, format="json")
 | 
			
		||||
@@ -255,7 +256,7 @@ class TestAlertsViews(TacticalTestCase):
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
    def test_add_alert_template(self):
 | 
			
		||||
        url = "/alerts/alerttemplates/"
 | 
			
		||||
        url = "/alerts/templates/"
 | 
			
		||||
 | 
			
		||||
        data = {
 | 
			
		||||
            "name": "Test Template",
 | 
			
		||||
@@ -268,11 +269,11 @@ class TestAlertsViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
    def test_get_alert_template(self):
 | 
			
		||||
        # returns 404 for invalid alert template pk
 | 
			
		||||
        resp = self.client.get("/alerts/alerttemplates/500/", format="json")
 | 
			
		||||
        resp = self.client.get("/alerts/templates/500/", format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 404)
 | 
			
		||||
 | 
			
		||||
        alert_template = baker.make("alerts.AlertTemplate")
 | 
			
		||||
        url = f"/alerts/alerttemplates/{alert_template.pk}/"  # type: ignore
 | 
			
		||||
        url = f"/alerts/templates/{alert_template.pk}/"  # type: ignore
 | 
			
		||||
 | 
			
		||||
        resp = self.client.get(url, format="json")
 | 
			
		||||
        serializer = AlertTemplateSerializer(alert_template)
 | 
			
		||||
@@ -284,16 +285,15 @@ class TestAlertsViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
    def test_update_alert_template(self):
 | 
			
		||||
        # returns 404 for invalid alert pk
 | 
			
		||||
        resp = self.client.put("/alerts/alerttemplates/500/", format="json")
 | 
			
		||||
        resp = self.client.put("/alerts/templates/500/", format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 404)
 | 
			
		||||
 | 
			
		||||
        alert_template = baker.make("alerts.AlertTemplate")
 | 
			
		||||
 | 
			
		||||
        url = f"/alerts/alerttemplates/{alert_template.pk}/"  # type: ignore
 | 
			
		||||
        url = f"/alerts/templates/{alert_template.pk}/"  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # test data
 | 
			
		||||
        data = {
 | 
			
		||||
            "id": alert_template.pk,  # type: ignore
 | 
			
		||||
            "agent_email_on_resolved": True,
 | 
			
		||||
            "agent_text_on_resolved": True,
 | 
			
		||||
            "agent_include_desktops": True,
 | 
			
		||||
@@ -309,13 +309,13 @@ class TestAlertsViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
    def test_delete_alert_template(self):
 | 
			
		||||
        # returns 404 for invalid alert pk
 | 
			
		||||
        resp = self.client.put("/alerts/alerttemplates/500/", format="json")
 | 
			
		||||
        resp = self.client.put("/alerts/templates/500/", format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 404)
 | 
			
		||||
 | 
			
		||||
        alert_template = baker.make("alerts.AlertTemplate")
 | 
			
		||||
 | 
			
		||||
        # test delete alert
 | 
			
		||||
        url = f"/alerts/alerttemplates/{alert_template.pk}/"  # type: ignore
 | 
			
		||||
        url = f"/alerts/templates/{alert_template.pk}/"  # type: ignore
 | 
			
		||||
        resp = self.client.delete(url, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
@@ -330,10 +330,10 @@ class TestAlertsViews(TacticalTestCase):
 | 
			
		||||
        baker.make("clients.Site", alert_template=alert_template, _quantity=3)
 | 
			
		||||
        baker.make("automation.Policy", alert_template=alert_template)
 | 
			
		||||
        core = CoreSettings.objects.first()
 | 
			
		||||
        core.alert_template = alert_template
 | 
			
		||||
        core.save()
 | 
			
		||||
        core.alert_template = alert_template  # type: ignore
 | 
			
		||||
        core.save()  # type: ignore
 | 
			
		||||
 | 
			
		||||
        url = f"/alerts/alerttemplates/{alert_template.pk}/related/"  # type: ignore
 | 
			
		||||
        url = f"/alerts/templates/{alert_template.pk}/related/"  # type: ignore
 | 
			
		||||
 | 
			
		||||
        resp = self.client.get(url, format="json")
 | 
			
		||||
        serializer = AlertTemplateRelationSerializer(alert_template)
 | 
			
		||||
@@ -403,16 +403,16 @@ class TestAlertTasks(TacticalTestCase):
 | 
			
		||||
        # assign first Alert Template as to a policy and apply it as default
 | 
			
		||||
        policy.alert_template = alert_templates[0]  # type: ignore
 | 
			
		||||
        policy.save()  # type: ignore
 | 
			
		||||
        core.workstation_policy = policy
 | 
			
		||||
        core.server_policy = policy
 | 
			
		||||
        core.save()
 | 
			
		||||
        core.workstation_policy = policy  # type: ignore
 | 
			
		||||
        core.server_policy = policy  # type: ignore
 | 
			
		||||
        core.save()  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.assertEquals(server.set_alert_template().pk, alert_templates[0].pk)  # type: ignore
 | 
			
		||||
        self.assertEquals(workstation.set_alert_template().pk, alert_templates[0].pk)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # assign second Alert Template to as default alert template
 | 
			
		||||
        core.alert_template = alert_templates[1]  # type: ignore
 | 
			
		||||
        core.save()
 | 
			
		||||
        core.save()  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.assertEquals(workstation.set_alert_template().pk, alert_templates[1].pk)  # type: ignore
 | 
			
		||||
        self.assertEquals(server.set_alert_template().pk, alert_templates[1].pk)  # type: ignore
 | 
			
		||||
@@ -514,6 +514,7 @@ class TestAlertTasks(TacticalTestCase):
 | 
			
		||||
            agent_recovery_email_task,
 | 
			
		||||
            agent_recovery_sms_task,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        from alerts.models import Alert
 | 
			
		||||
 | 
			
		||||
        agent_dashboard_alert = baker.make_recipe("agents.overdue_agent")
 | 
			
		||||
@@ -727,7 +728,6 @@ class TestAlertTasks(TacticalTestCase):
 | 
			
		||||
        send_email,
 | 
			
		||||
        sleep,
 | 
			
		||||
    ):
 | 
			
		||||
        from alerts.tasks import cache_agents_alert_template
 | 
			
		||||
        from checks.models import Check
 | 
			
		||||
        from checks.tasks import (
 | 
			
		||||
            handle_check_email_alert_task,
 | 
			
		||||
@@ -736,6 +736,8 @@ class TestAlertTasks(TacticalTestCase):
 | 
			
		||||
            handle_resolved_check_sms_alert_task,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        from alerts.tasks import cache_agents_alert_template
 | 
			
		||||
 | 
			
		||||
        # create test data
 | 
			
		||||
        agent = baker.make_recipe("agents.agent")
 | 
			
		||||
        agent_no_settings = baker.make_recipe("agents.agent")
 | 
			
		||||
@@ -1011,7 +1013,6 @@ class TestAlertTasks(TacticalTestCase):
 | 
			
		||||
        send_email,
 | 
			
		||||
        sleep,
 | 
			
		||||
    ):
 | 
			
		||||
        from alerts.tasks import cache_agents_alert_template
 | 
			
		||||
        from autotasks.models import AutomatedTask
 | 
			
		||||
        from autotasks.tasks import (
 | 
			
		||||
            handle_resolved_task_email_alert,
 | 
			
		||||
@@ -1020,6 +1021,8 @@ class TestAlertTasks(TacticalTestCase):
 | 
			
		||||
            handle_task_sms_alert,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        from alerts.tasks import cache_agents_alert_template
 | 
			
		||||
 | 
			
		||||
        # create test data
 | 
			
		||||
        agent = baker.make_recipe("agents.agent")
 | 
			
		||||
        agent_no_settings = baker.make_recipe("agents.agent")
 | 
			
		||||
@@ -1272,17 +1275,17 @@ class TestAlertTasks(TacticalTestCase):
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        core = CoreSettings.objects.first()
 | 
			
		||||
        core.smtp_host = "test.test.com"
 | 
			
		||||
        core.smtp_port = 587
 | 
			
		||||
        core.smtp_recipients = ["recipient@test.com"]
 | 
			
		||||
        core.twilio_account_sid = "test"
 | 
			
		||||
        core.twilio_auth_token = "1234123412341234"
 | 
			
		||||
        core.sms_alert_recipients = ["+1234567890"]
 | 
			
		||||
        core.smtp_host = "test.test.com"  # type: ignore
 | 
			
		||||
        core.smtp_port = 587  # type: ignore
 | 
			
		||||
        core.smtp_recipients = ["recipient@test.com"]  # type: ignore
 | 
			
		||||
        core.twilio_account_sid = "test"  # type: ignore
 | 
			
		||||
        core.twilio_auth_token = "1234123412341234"  # type: ignore
 | 
			
		||||
        core.sms_alert_recipients = ["+1234567890"]  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # test sending email with alert template settings
 | 
			
		||||
        core.send_mail("Test", "Test", alert_template=alert_template)
 | 
			
		||||
        core.send_mail("Test", "Test", alert_template=alert_template)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        core.send_sms("Test", alert_template=alert_template)
 | 
			
		||||
        core.send_sms("Test", alert_template=alert_template)  # type: ignore
 | 
			
		||||
 | 
			
		||||
    @patch("agents.models.Agent.nats_cmd")
 | 
			
		||||
    @patch("agents.tasks.agent_outage_sms_task.delay")
 | 
			
		||||
@@ -1315,6 +1318,7 @@ class TestAlertTasks(TacticalTestCase):
 | 
			
		||||
            "alerts.AlertTemplate",
 | 
			
		||||
            is_active=True,
 | 
			
		||||
            agent_always_alert=True,
 | 
			
		||||
            agent_script_actions=False,
 | 
			
		||||
            action=failure_action,
 | 
			
		||||
            action_timeout=30,
 | 
			
		||||
            resolved_action=resolved_action,
 | 
			
		||||
@@ -1328,6 +1332,14 @@ class TestAlertTasks(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        agent_outages_task()
 | 
			
		||||
 | 
			
		||||
        # should not have been called since agent_script_actions is set to False
 | 
			
		||||
        nats_cmd.assert_not_called()
 | 
			
		||||
 | 
			
		||||
        alert_template.agent_script_actions = True  # type: ignore
 | 
			
		||||
        alert_template.save()  # type: ignore
 | 
			
		||||
 | 
			
		||||
        agent_outages_task()
 | 
			
		||||
 | 
			
		||||
        # this is what data should be
 | 
			
		||||
        data = {
 | 
			
		||||
            "func": "runscriptfull",
 | 
			
		||||
@@ -1340,14 +1352,6 @@ class TestAlertTasks(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        nats_cmd.reset_mock()
 | 
			
		||||
 | 
			
		||||
        # Setup cmd mock
 | 
			
		||||
        success = {
 | 
			
		||||
            "retcode": 0,
 | 
			
		||||
            "stdout": "success!",
 | 
			
		||||
            "stderr": "",
 | 
			
		||||
            "execution_time": 5.0000,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        nats_cmd.side_effect = ["pong", success]
 | 
			
		||||
 | 
			
		||||
        # make sure script run results were stored
 | 
			
		||||
@@ -1387,3 +1391,199 @@ class TestAlertTasks(TacticalTestCase):
 | 
			
		||||
        self.assertEqual(alert.resolved_action_execution_time, "5.0000")
 | 
			
		||||
        self.assertEqual(alert.resolved_action_stdout, "success!")
 | 
			
		||||
        self.assertEqual(alert.resolved_action_stderr, "")
 | 
			
		||||
 | 
			
		||||
    def test_parse_script_args(self):
 | 
			
		||||
        alert = baker.make("alerts.Alert")
 | 
			
		||||
 | 
			
		||||
        args = ["-Parameter", "-Another {{alert.id}}"]
 | 
			
		||||
 | 
			
		||||
        # test default value
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            ["-Parameter", f"-Another '{alert.id}'"],  # type: ignore
 | 
			
		||||
            alert.parse_script_args(args=args),  # type: ignore
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_prune_resolved_alerts(self):
 | 
			
		||||
        from .tasks import prune_resolved_alerts
 | 
			
		||||
 | 
			
		||||
        # setup data
 | 
			
		||||
        resolved_alerts = baker.make(
 | 
			
		||||
            "alerts.Alert",
 | 
			
		||||
            resolved=True,
 | 
			
		||||
            _quantity=25,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        alerts = baker.make(
 | 
			
		||||
            "alerts.Alert",
 | 
			
		||||
            resolved=False,
 | 
			
		||||
            _quantity=25,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        days = 0
 | 
			
		||||
        for alert in resolved_alerts:  # type: ignore
 | 
			
		||||
            alert.alert_time = djangotime.now() - djangotime.timedelta(days=days)
 | 
			
		||||
            alert.save()
 | 
			
		||||
            days = days + 5
 | 
			
		||||
 | 
			
		||||
        days = 0
 | 
			
		||||
        for alert in alerts:  # type: ignore
 | 
			
		||||
            alert.alert_time = djangotime.now() - djangotime.timedelta(days=days)
 | 
			
		||||
            alert.save()
 | 
			
		||||
            days = days + 5
 | 
			
		||||
 | 
			
		||||
        # delete AgentHistory older than 30 days
 | 
			
		||||
        prune_resolved_alerts(30)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(Alert.objects.count(), 31)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestAlertPermissions(TacticalTestCase):
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        self.setup_coresettings()
 | 
			
		||||
        self.client_setup()
 | 
			
		||||
 | 
			
		||||
    def test_get_alerts_permissions(self):
 | 
			
		||||
        agent = baker.make_recipe("agents.agent")
 | 
			
		||||
        agent1 = baker.make_recipe("agents.agent")
 | 
			
		||||
        agent2 = baker.make_recipe("agents.agent")
 | 
			
		||||
        agents = [agent, agent1, agent2]
 | 
			
		||||
        checks = baker.make("checks.Check", agent=cycle(agents), _quantity=3)
 | 
			
		||||
        tasks = baker.make("autotasks.AutomatedTask", agent=cycle(agents), _quantity=3)
 | 
			
		||||
        baker.make(
 | 
			
		||||
            "alerts.Alert", alert_type="task", assigned_task=cycle(tasks), _quantity=3
 | 
			
		||||
        )
 | 
			
		||||
        baker.make(
 | 
			
		||||
            "alerts.Alert",
 | 
			
		||||
            alert_type="check",
 | 
			
		||||
            assigned_check=cycle(checks),
 | 
			
		||||
            _quantity=3,
 | 
			
		||||
        )
 | 
			
		||||
        baker.make(
 | 
			
		||||
            "alerts.Alert", alert_type="availability", agent=cycle(agents), _quantity=3
 | 
			
		||||
        )
 | 
			
		||||
        baker.make("alerts.Alert", alert_type="custom", _quantity=4)
 | 
			
		||||
 | 
			
		||||
        # test super user access
 | 
			
		||||
        r = self.check_authorized_superuser("patch", f"{base_url}/")
 | 
			
		||||
        self.assertEqual(len(r.data), 13)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        user = self.create_user_with_roles([])
 | 
			
		||||
        self.client.force_authenticate(user=user)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authorized("patch", f"{base_url}/")
 | 
			
		||||
 | 
			
		||||
        # add list software role to user
 | 
			
		||||
        user.role.can_list_alerts = True
 | 
			
		||||
        user.role.save()
 | 
			
		||||
 | 
			
		||||
        r = self.check_authorized("patch", f"{base_url}/")
 | 
			
		||||
        self.assertEqual(len(r.data), 13)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # test limiting to client
 | 
			
		||||
        user.role.can_view_clients.set([agent.client])
 | 
			
		||||
        r = self.check_authorized("patch", f"{base_url}/")
 | 
			
		||||
        self.assertEqual(len(r.data), 7)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # test limiting to site
 | 
			
		||||
        user.role.can_view_clients.clear()
 | 
			
		||||
        user.role.can_view_sites.set([agent1.site])
 | 
			
		||||
        r = self.client.patch(f"{base_url}/")
 | 
			
		||||
        self.assertEqual(len(r.data), 7)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # test limiting to site and client
 | 
			
		||||
        user.role.can_view_clients.set([agent2.client])
 | 
			
		||||
        r = self.client.patch(f"{base_url}/")
 | 
			
		||||
        self.assertEqual(len(r.data), 10)  # type: ignore
 | 
			
		||||
 | 
			
		||||
    @patch("alerts.models.Alert.delete", return_value=1)
 | 
			
		||||
    def test_edit_delete_get_alert_permissions(self, delete):
 | 
			
		||||
        agent = baker.make_recipe("agents.agent")
 | 
			
		||||
        agent1 = baker.make_recipe("agents.agent")
 | 
			
		||||
        agent2 = baker.make_recipe("agents.agent")
 | 
			
		||||
        agents = [agent, agent1, agent2]
 | 
			
		||||
        checks = baker.make("checks.Check", agent=cycle(agents), _quantity=3)
 | 
			
		||||
        tasks = baker.make("autotasks.AutomatedTask", agent=cycle(agents), _quantity=3)
 | 
			
		||||
        alert_tasks = baker.make(
 | 
			
		||||
            "alerts.Alert", alert_type="task", assigned_task=cycle(tasks), _quantity=3
 | 
			
		||||
        )
 | 
			
		||||
        alert_checks = baker.make(
 | 
			
		||||
            "alerts.Alert",
 | 
			
		||||
            alert_type="check",
 | 
			
		||||
            assigned_check=cycle(checks),
 | 
			
		||||
            _quantity=3,
 | 
			
		||||
        )
 | 
			
		||||
        alert_agents = baker.make(
 | 
			
		||||
            "alerts.Alert", alert_type="availability", agent=cycle(agents), _quantity=3
 | 
			
		||||
        )
 | 
			
		||||
        alert_custom = baker.make("alerts.Alert", alert_type="custom", _quantity=4)
 | 
			
		||||
 | 
			
		||||
        # alert task url
 | 
			
		||||
        task_url = f"{base_url}/{alert_tasks[0].id}/"  # for agent
 | 
			
		||||
        unauthorized_task_url = f"{base_url}/{alert_tasks[1].id}/"  # for agent1
 | 
			
		||||
        # alert check url
 | 
			
		||||
        check_url = f"{base_url}/{alert_checks[0].id}/"  # for agent
 | 
			
		||||
        unauthorized_check_url = f"{base_url}/{alert_checks[1].id}/"  # for agent1
 | 
			
		||||
        # alert agent url
 | 
			
		||||
        agent_url = f"{base_url}/{alert_agents[0].id}/"  # for agent
 | 
			
		||||
        unauthorized_agent_url = f"{base_url}/{alert_agents[1].id}/"  # for agent1
 | 
			
		||||
        # custom alert url
 | 
			
		||||
        custom_url = f"{base_url}/{alert_custom[0].id}/"  # no agent associated
 | 
			
		||||
 | 
			
		||||
        authorized_urls = [task_url, check_url, agent_url, custom_url]
 | 
			
		||||
        unauthorized_urls = [
 | 
			
		||||
            unauthorized_agent_url,
 | 
			
		||||
            unauthorized_check_url,
 | 
			
		||||
            unauthorized_task_url,
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        for method in ["get", "put", "delete"]:
 | 
			
		||||
 | 
			
		||||
            # test superuser access
 | 
			
		||||
            for url in authorized_urls:
 | 
			
		||||
                self.check_authorized_superuser(method, url)
 | 
			
		||||
 | 
			
		||||
            for url in unauthorized_urls:
 | 
			
		||||
                self.check_authorized_superuser(method, url)
 | 
			
		||||
 | 
			
		||||
            user = self.create_user_with_roles([])
 | 
			
		||||
            self.client.force_authenticate(user=user)  # type: ignore
 | 
			
		||||
 | 
			
		||||
            # test user without role
 | 
			
		||||
            for url in authorized_urls:
 | 
			
		||||
                self.check_not_authorized(method, url)
 | 
			
		||||
 | 
			
		||||
            for url in unauthorized_urls:
 | 
			
		||||
                self.check_not_authorized(method, url)
 | 
			
		||||
 | 
			
		||||
            # add user to role and test
 | 
			
		||||
            setattr(
 | 
			
		||||
                user.role,
 | 
			
		||||
                "can_list_alerts" if method == "get" else "can_manage_alerts",
 | 
			
		||||
                True,
 | 
			
		||||
            )
 | 
			
		||||
            user.role.save()
 | 
			
		||||
 | 
			
		||||
            # test user with role
 | 
			
		||||
            for url in authorized_urls:
 | 
			
		||||
                self.check_authorized(method, url)
 | 
			
		||||
 | 
			
		||||
            for url in unauthorized_urls:
 | 
			
		||||
                self.check_authorized(method, url)
 | 
			
		||||
 | 
			
		||||
            # limit user to client if agent check
 | 
			
		||||
            user.role.can_view_clients.set([agent.client])
 | 
			
		||||
 | 
			
		||||
            for url in authorized_urls:
 | 
			
		||||
                self.check_authorized(method, url)
 | 
			
		||||
 | 
			
		||||
            for url in unauthorized_urls:
 | 
			
		||||
                self.check_not_authorized(method, url)
 | 
			
		||||
 | 
			
		||||
            # limit user to client if agent check
 | 
			
		||||
            user.role.can_view_sites.set([agent1.site])
 | 
			
		||||
 | 
			
		||||
            for url in authorized_urls:
 | 
			
		||||
                self.check_authorized(method, url)
 | 
			
		||||
 | 
			
		||||
            for url in unauthorized_urls:
 | 
			
		||||
                self.check_authorized(method, url)
 | 
			
		||||
 
 | 
			
		||||
@@ -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()),
 | 
			
		||||
]
 | 
			
		||||
 
 | 
			
		||||
@@ -3,12 +3,14 @@ from datetime import datetime as dt
 | 
			
		||||
from django.db.models import Q
 | 
			
		||||
from django.shortcuts import get_object_or_404
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from rest_framework.permissions import IsAuthenticated
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
from rest_framework.views import APIView
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.utils import notify_error
 | 
			
		||||
 | 
			
		||||
from .models import Alert, AlertTemplate
 | 
			
		||||
from .permissions import AlertPerms, AlertTemplatePerms
 | 
			
		||||
from .serializers import (
 | 
			
		||||
    AlertSerializer,
 | 
			
		||||
    AlertTemplateRelationSerializer,
 | 
			
		||||
@@ -18,6 +20,8 @@ from .tasks import cache_agents_alert_template
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetAddAlerts(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, AlertPerms]
 | 
			
		||||
 | 
			
		||||
    def patch(self, request):
 | 
			
		||||
 | 
			
		||||
        # top 10 alerts for dashboard icon
 | 
			
		||||
@@ -88,7 +92,8 @@ class GetAddAlerts(APIView):
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
            alerts = (
 | 
			
		||||
                Alert.objects.filter(clientFilter)
 | 
			
		||||
                Alert.objects.filter_by_role(request.user)
 | 
			
		||||
                .filter(clientFilter)
 | 
			
		||||
                .filter(severityFilter)
 | 
			
		||||
                .filter(resolvedFilter)
 | 
			
		||||
                .filter(snoozedFilter)
 | 
			
		||||
@@ -97,7 +102,7 @@ class GetAddAlerts(APIView):
 | 
			
		||||
            return Response(AlertSerializer(alerts, many=True).data)
 | 
			
		||||
 | 
			
		||||
        else:
 | 
			
		||||
            alerts = Alert.objects.all()
 | 
			
		||||
            alerts = Alert.objects.filter_by_role(request.user)
 | 
			
		||||
            return Response(AlertSerializer(alerts, many=True).data)
 | 
			
		||||
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
@@ -109,9 +114,10 @@ class GetAddAlerts(APIView):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetUpdateDeleteAlert(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, AlertPerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request, pk):
 | 
			
		||||
        alert = get_object_or_404(Alert, pk=pk)
 | 
			
		||||
 | 
			
		||||
        return Response(AlertSerializer(alert).data)
 | 
			
		||||
 | 
			
		||||
    def put(self, request, pk):
 | 
			
		||||
@@ -163,6 +169,8 @@ class GetUpdateDeleteAlert(APIView):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BulkAlerts(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, AlertPerms]
 | 
			
		||||
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
        if request.data["bulk_action"] == "resolve":
 | 
			
		||||
            Alert.objects.filter(id__in=request.data["alerts"]).update(
 | 
			
		||||
@@ -185,9 +193,10 @@ class BulkAlerts(APIView):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetAddAlertTemplates(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, AlertTemplatePerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request):
 | 
			
		||||
        alert_templates = AlertTemplate.objects.all()
 | 
			
		||||
 | 
			
		||||
        return Response(AlertTemplateSerializer(alert_templates, many=True).data)
 | 
			
		||||
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
@@ -202,6 +211,8 @@ class GetAddAlertTemplates(APIView):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetUpdateDeleteAlertTemplate(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, AlertTemplatePerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request, pk):
 | 
			
		||||
        alert_template = get_object_or_404(AlertTemplate, pk=pk)
 | 
			
		||||
 | 
			
		||||
@@ -231,6 +242,8 @@ class GetUpdateDeleteAlertTemplate(APIView):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RelatedAlertTemplate(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, AlertTemplatePerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request, pk):
 | 
			
		||||
        alert_template = get_object_or_404(AlertTemplate, pk=pk)
 | 
			
		||||
        return Response(AlertTemplateRelationSerializer(alert_template).data)
 | 
			
		||||
 
 | 
			
		||||
@@ -5,8 +5,8 @@ from unittest.mock import patch
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from model_bakery import baker
 | 
			
		||||
from autotasks.models import AutomatedTask
 | 
			
		||||
 | 
			
		||||
from autotasks.models import AutomatedTask
 | 
			
		||||
from tacticalrmm.test import TacticalTestCase
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -213,7 +213,8 @@ class TestAPIv3(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        # setup data
 | 
			
		||||
        agent = baker.make_recipe("agents.agent")
 | 
			
		||||
        task = baker.make("autotasks.AutomatedTask", agent=agent)
 | 
			
		||||
        script = baker.make_recipe("scripts.script")
 | 
			
		||||
        task = baker.make("autotasks.AutomatedTask", agent=agent, script=script)
 | 
			
		||||
 | 
			
		||||
        url = f"/api/v3/{task.pk}/{agent.agent_id}/taskrunner/"  # type: ignore
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -20,4 +20,5 @@ urlpatterns = [
 | 
			
		||||
    path("superseded/", views.SupersededWinUpdate.as_view()),
 | 
			
		||||
    path("<int:pk>/chocoresult/", views.ChocoResult.as_view()),
 | 
			
		||||
    path("<str:agentid>/recovery/", views.AgentRecovery.as_view()),
 | 
			
		||||
    path("<int:pk>/<str:agentid>/histresult/", views.AgentHistoryResult.as_view()),
 | 
			
		||||
]
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,6 @@ from django.conf import settings
 | 
			
		||||
from django.http import HttpResponse
 | 
			
		||||
from django.shortcuts import get_object_or_404
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from loguru import logger
 | 
			
		||||
from packaging import version as pyver
 | 
			
		||||
from rest_framework.authentication import TokenAuthentication
 | 
			
		||||
from rest_framework.authtoken.models import Token
 | 
			
		||||
@@ -15,20 +14,18 @@ from rest_framework.response import Response
 | 
			
		||||
from rest_framework.views import APIView
 | 
			
		||||
 | 
			
		||||
from accounts.models import User
 | 
			
		||||
from agents.models import Agent, AgentCustomField
 | 
			
		||||
from agents.serializers import WinAgentSerializer
 | 
			
		||||
from agents.models import Agent, AgentHistory
 | 
			
		||||
from agents.serializers import WinAgentSerializer, AgentHistorySerializer
 | 
			
		||||
from autotasks.models import AutomatedTask
 | 
			
		||||
from autotasks.serializers import TaskGOGetSerializer, TaskRunnerPatchSerializer
 | 
			
		||||
from checks.models import Check
 | 
			
		||||
from checks.serializers import CheckRunnerGetSerializer
 | 
			
		||||
from checks.utils import bytes2human
 | 
			
		||||
from logs.models import PendingAction
 | 
			
		||||
from logs.models import PendingAction, DebugLog
 | 
			
		||||
from software.models import InstalledSoftware
 | 
			
		||||
from tacticalrmm.utils import SoftwareList, filter_software, notify_error, reload_nats
 | 
			
		||||
from winupdate.models import WinUpdate, WinUpdatePolicy
 | 
			
		||||
 | 
			
		||||
logger.configure(**settings.LOG_CONFIG)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CheckIn(APIView):
 | 
			
		||||
 | 
			
		||||
@@ -36,6 +33,10 @@ class CheckIn(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated]
 | 
			
		||||
 | 
			
		||||
    def patch(self, request):
 | 
			
		||||
        """
 | 
			
		||||
        !!! DEPRECATED AS OF AGENT 1.6.0 !!!
 | 
			
		||||
        Endpoint be removed in a future release
 | 
			
		||||
        """
 | 
			
		||||
        from alerts.models import Alert
 | 
			
		||||
 | 
			
		||||
        updated = False
 | 
			
		||||
@@ -182,7 +183,11 @@ class WinUpdates(APIView):
 | 
			
		||||
 | 
			
		||||
        if reboot:
 | 
			
		||||
            asyncio.run(agent.nats_cmd({"func": "rebootnow"}, wait=False))
 | 
			
		||||
            logger.info(f"{agent.hostname} is rebooting after updates were installed.")
 | 
			
		||||
            DebugLog.info(
 | 
			
		||||
                agent=agent,
 | 
			
		||||
                log_type="windows_updates",
 | 
			
		||||
                message=f"{agent.hostname} is rebooting after updates were installed.",
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        agent.delete_superseded_updates()
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
@@ -304,10 +309,11 @@ class CheckRunner(APIView):
 | 
			
		||||
                    < djangotime.now()
 | 
			
		||||
                    - djangotime.timedelta(seconds=check.run_interval)
 | 
			
		||||
                )
 | 
			
		||||
                # if check interval isn't set, make sure the agent's check interval has passed before running
 | 
			
		||||
            )
 | 
			
		||||
            # if check interval isn't set, make sure the agent's check interval has passed before running
 | 
			
		||||
            or (
 | 
			
		||||
                check.last_run
 | 
			
		||||
                not check.run_interval
 | 
			
		||||
                and check.last_run
 | 
			
		||||
                < djangotime.now() - djangotime.timedelta(seconds=agent.check_interval)
 | 
			
		||||
            )
 | 
			
		||||
        ]
 | 
			
		||||
@@ -320,11 +326,16 @@ class CheckRunner(APIView):
 | 
			
		||||
 | 
			
		||||
    def patch(self, request):
 | 
			
		||||
        check = get_object_or_404(Check, pk=request.data["id"])
 | 
			
		||||
        if pyver.parse(check.agent.version) < pyver.parse("1.5.7"):
 | 
			
		||||
            return notify_error("unsupported")
 | 
			
		||||
 | 
			
		||||
        check.last_run = djangotime.now()
 | 
			
		||||
        check.save(update_fields=["last_run"])
 | 
			
		||||
        status = check.handle_checkv2(request.data)
 | 
			
		||||
        status = check.handle_check(request.data)
 | 
			
		||||
        if status == "failing" and check.assignedtask.exists():  # type: ignore
 | 
			
		||||
            check.handle_assigned_task()
 | 
			
		||||
 | 
			
		||||
        return Response(status)
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CheckRunnerInterval(APIView):
 | 
			
		||||
@@ -344,13 +355,12 @@ class TaskRunner(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated]
 | 
			
		||||
 | 
			
		||||
    def get(self, request, pk, agentid):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=agentid)
 | 
			
		||||
        _ = get_object_or_404(Agent, agent_id=agentid)
 | 
			
		||||
        task = get_object_or_404(AutomatedTask, pk=pk)
 | 
			
		||||
        return Response(TaskGOGetSerializer(task).data)
 | 
			
		||||
 | 
			
		||||
    def patch(self, request, pk, agentid):
 | 
			
		||||
        from alerts.models import Alert
 | 
			
		||||
        from logs.models import AuditLog
 | 
			
		||||
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=agentid)
 | 
			
		||||
        task = get_object_or_404(AutomatedTask, pk=pk)
 | 
			
		||||
@@ -365,29 +375,7 @@ class TaskRunner(APIView):
 | 
			
		||||
        if task.custom_field:
 | 
			
		||||
            if not task.stderr:
 | 
			
		||||
 | 
			
		||||
                if AgentCustomField.objects.filter(
 | 
			
		||||
                    field=task.custom_field, agent=task.agent
 | 
			
		||||
                ).exists():
 | 
			
		||||
                    agent_field = AgentCustomField.objects.get(
 | 
			
		||||
                        field=task.custom_field, agent=task.agent
 | 
			
		||||
                    )
 | 
			
		||||
                else:
 | 
			
		||||
                    agent_field = AgentCustomField.objects.create(
 | 
			
		||||
                        field=task.custom_field, agent=task.agent
 | 
			
		||||
                    )
 | 
			
		||||
 | 
			
		||||
                # get last line of stdout
 | 
			
		||||
                value = new_task.stdout.split("\n")[-1].strip()
 | 
			
		||||
 | 
			
		||||
                if task.custom_field.type in ["text", "number", "single", "datetime"]:
 | 
			
		||||
                    agent_field.string_value = value
 | 
			
		||||
                    agent_field.save()
 | 
			
		||||
                elif task.custom_field.type == "multiple":
 | 
			
		||||
                    agent_field.multiple_value = value.split(",")
 | 
			
		||||
                    agent_field.save()
 | 
			
		||||
                elif task.custom_field.type == "checkbox":
 | 
			
		||||
                    agent_field.bool_value = bool(value)
 | 
			
		||||
                    agent_field.save()
 | 
			
		||||
                task.save_collector_results()
 | 
			
		||||
 | 
			
		||||
                status = "passing"
 | 
			
		||||
            else:
 | 
			
		||||
@@ -404,15 +392,6 @@ class TaskRunner(APIView):
 | 
			
		||||
        else:
 | 
			
		||||
            Alert.handle_alert_failure(new_task)
 | 
			
		||||
 | 
			
		||||
        AuditLog.objects.create(
 | 
			
		||||
            username=agent.hostname,
 | 
			
		||||
            agent=agent.hostname,
 | 
			
		||||
            object_type="agent",
 | 
			
		||||
            action="task_run",
 | 
			
		||||
            message=f"Scheduled Task {task.name} was run on {agent.hostname}",
 | 
			
		||||
            after_value=AutomatedTask.serialize(new_task),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -503,6 +482,7 @@ class NewAgent(APIView):
 | 
			
		||||
            action="agent_install",
 | 
			
		||||
            message=f"{request.user} installed new agent {agent.hostname}",
 | 
			
		||||
            after_value=Agent.serialize(agent),
 | 
			
		||||
            debug_info={"ip": request._client_ip},
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        return Response(
 | 
			
		||||
@@ -607,3 +587,16 @@ class AgentRecovery(APIView):
 | 
			
		||||
            reload_nats()
 | 
			
		||||
 | 
			
		||||
        return Response(ret)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AgentHistoryResult(APIView):
 | 
			
		||||
    authentication_classes = [TokenAuthentication]
 | 
			
		||||
    permission_classes = [IsAuthenticated]
 | 
			
		||||
 | 
			
		||||
    def patch(self, request, agentid, pk):
 | 
			
		||||
        _ = get_object_or_404(Agent, agent_id=agentid)
 | 
			
		||||
        hist = get_object_or_404(AgentHistory, pk=pk)
 | 
			
		||||
        s = AgentHistorySerializer(instance=hist, data=request.data, partial=True)
 | 
			
		||||
        s.is_valid(raise_exception=True)
 | 
			
		||||
        s.save()
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 
 | 
			
		||||
@@ -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),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
from django.db import models
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from core.models import CoreSettings
 | 
			
		||||
from django.db import models
 | 
			
		||||
from logs.models import BaseAuditModel
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -28,12 +29,11 @@ class Policy(BaseAuditModel):
 | 
			
		||||
 | 
			
		||||
    def save(self, *args, **kwargs):
 | 
			
		||||
        from alerts.tasks import cache_agents_alert_template
 | 
			
		||||
 | 
			
		||||
        from automation.tasks import generate_agent_checks_task
 | 
			
		||||
 | 
			
		||||
        # get old policy if exists
 | 
			
		||||
        old_policy = type(self).objects.get(pk=self.pk) if self.pk else None
 | 
			
		||||
        super(BaseAuditModel, self).save(*args, **kwargs)
 | 
			
		||||
        super(Policy, self).save(old_model=old_policy, *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
        # generate agent checks only if active and enforced were changed
 | 
			
		||||
        if old_policy:
 | 
			
		||||
@@ -50,7 +50,7 @@ class Policy(BaseAuditModel):
 | 
			
		||||
        from automation.tasks import generate_agent_checks_task
 | 
			
		||||
 | 
			
		||||
        agents = list(self.related_agents().only("pk").values_list("pk", flat=True))
 | 
			
		||||
        super(BaseAuditModel, self).delete(*args, **kwargs)
 | 
			
		||||
        super(Policy, self).delete(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
        generate_agent_checks_task.delay(agents=agents, create_tasks=True)
 | 
			
		||||
 | 
			
		||||
@@ -126,9 +126,9 @@ class Policy(BaseAuditModel):
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def serialize(policy):
 | 
			
		||||
        # serializes the policy and returns json
 | 
			
		||||
        from .serializers import PolicySerializer
 | 
			
		||||
        from .serializers import PolicyAuditSerializer
 | 
			
		||||
 | 
			
		||||
        return PolicySerializer(policy).data
 | 
			
		||||
        return PolicyAuditSerializer(policy).data
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def cascade_policy_tasks(agent):
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										11
									
								
								api/tacticalrmm/automation/permissions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								api/tacticalrmm/automation/permissions.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
			
		||||
from rest_framework import permissions
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.permissions import _has_perm
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AutomationPolicyPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        if r.method == "GET":
 | 
			
		||||
            return _has_perm(r, "can_list_automation_policies")
 | 
			
		||||
        else:
 | 
			
		||||
            return _has_perm(r, "can_manage_automation_policies")
 | 
			
		||||
@@ -8,7 +8,7 @@ from agents.serializers import AgentHostnameSerializer
 | 
			
		||||
from autotasks.models import AutomatedTask
 | 
			
		||||
from checks.models import Check
 | 
			
		||||
from clients.models import Client
 | 
			
		||||
from clients.serializers import ClientSerializer, SiteSerializer
 | 
			
		||||
from clients.serializers import ClientMinimumSerializer, SiteMinimumSerializer
 | 
			
		||||
from winupdate.serializers import WinUpdatePolicySerializer
 | 
			
		||||
 | 
			
		||||
from .models import Policy
 | 
			
		||||
@@ -21,25 +21,70 @@ class PolicySerializer(ModelSerializer):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PolicyTableSerializer(ModelSerializer):
 | 
			
		||||
 | 
			
		||||
    default_server_policy = ReadOnlyField(source="is_default_server_policy")
 | 
			
		||||
    default_workstation_policy = ReadOnlyField(source="is_default_workstation_policy")
 | 
			
		||||
    agents_count = SerializerMethodField(read_only=True)
 | 
			
		||||
    winupdatepolicy = WinUpdatePolicySerializer(many=True, read_only=True)
 | 
			
		||||
    alert_template = ReadOnlyField(source="alert_template.id")
 | 
			
		||||
    excluded_clients = ClientSerializer(many=True)
 | 
			
		||||
    excluded_sites = SiteSerializer(many=True)
 | 
			
		||||
    excluded_agents = AgentHostnameSerializer(many=True)
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Policy
 | 
			
		||||
        fields = "__all__"
 | 
			
		||||
        depth = 1
 | 
			
		||||
 | 
			
		||||
    def get_agents_count(self, policy):
 | 
			
		||||
        return policy.related_agents().count()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PolicyRelatedSerializer(ModelSerializer):
 | 
			
		||||
    workstation_clients = SerializerMethodField()
 | 
			
		||||
    server_clients = SerializerMethodField()
 | 
			
		||||
    workstation_sites = SerializerMethodField()
 | 
			
		||||
    server_sites = SerializerMethodField()
 | 
			
		||||
    agents = SerializerMethodField()
 | 
			
		||||
 | 
			
		||||
    def get_agents(self, policy):
 | 
			
		||||
        return AgentHostnameSerializer(
 | 
			
		||||
            policy.agents.filter_by_role(self.context["user"]).only(
 | 
			
		||||
                "agent_id", "hostname"
 | 
			
		||||
            ),
 | 
			
		||||
            many=True,
 | 
			
		||||
        ).data
 | 
			
		||||
 | 
			
		||||
    def get_workstation_clients(self, policy):
 | 
			
		||||
        return ClientMinimumSerializer(
 | 
			
		||||
            policy.workstation_clients.filter_by_role(self.context["user"]), many=True
 | 
			
		||||
        ).data
 | 
			
		||||
 | 
			
		||||
    def get_server_clients(self, policy):
 | 
			
		||||
        return ClientMinimumSerializer(
 | 
			
		||||
            policy.server_clients.filter_by_role(self.context["user"]), many=True
 | 
			
		||||
        ).data
 | 
			
		||||
 | 
			
		||||
    def get_workstation_sites(self, policy):
 | 
			
		||||
        return SiteMinimumSerializer(
 | 
			
		||||
            policy.workstation_sites.filter_by_role(self.context["user"]), many=True
 | 
			
		||||
        ).data
 | 
			
		||||
 | 
			
		||||
    def get_server_sites(self, policy):
 | 
			
		||||
        return SiteMinimumSerializer(
 | 
			
		||||
            policy.server_sites.filter_by_role(self.context["user"]), many=True
 | 
			
		||||
        ).data
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Policy
 | 
			
		||||
        fields = (
 | 
			
		||||
            "pk",
 | 
			
		||||
            "name",
 | 
			
		||||
            "workstation_clients",
 | 
			
		||||
            "workstation_sites",
 | 
			
		||||
            "server_clients",
 | 
			
		||||
            "server_sites",
 | 
			
		||||
            "agents",
 | 
			
		||||
            "is_default_server_policy",
 | 
			
		||||
            "is_default_workstation_policy",
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PolicyOverviewSerializer(ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Client
 | 
			
		||||
@@ -48,7 +93,6 @@ class PolicyOverviewSerializer(ModelSerializer):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PolicyCheckStatusSerializer(ModelSerializer):
 | 
			
		||||
 | 
			
		||||
    hostname = ReadOnlyField(source="agent.hostname")
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
@@ -57,7 +101,6 @@ class PolicyCheckStatusSerializer(ModelSerializer):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PolicyTaskStatusSerializer(ModelSerializer):
 | 
			
		||||
 | 
			
		||||
    hostname = ReadOnlyField(source="agent.hostname")
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
@@ -65,26 +108,7 @@ class PolicyTaskStatusSerializer(ModelSerializer):
 | 
			
		||||
        fields = "__all__"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PolicyCheckSerializer(ModelSerializer):
 | 
			
		||||
class PolicyAuditSerializer(ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Check
 | 
			
		||||
        fields = (
 | 
			
		||||
            "id",
 | 
			
		||||
            "check_type",
 | 
			
		||||
            "readable_desc",
 | 
			
		||||
            "assignedtask",
 | 
			
		||||
            "text_alert",
 | 
			
		||||
            "email_alert",
 | 
			
		||||
            "dashboard_alert",
 | 
			
		||||
        )
 | 
			
		||||
        depth = 1
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AutoTasksFieldSerializer(ModelSerializer):
 | 
			
		||||
    assigned_check = PolicyCheckSerializer(read_only=True)
 | 
			
		||||
    script = ReadOnlyField(source="script.id")
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = AutomatedTask
 | 
			
		||||
        model = Policy
 | 
			
		||||
        fields = "__all__"
 | 
			
		||||
        depth = 1
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@ from typing import Any, Dict, List, Union
 | 
			
		||||
from tacticalrmm.celery import app
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
@app.task(retry_backoff=5, retry_jitter=True, retry_kwargs={"max_retries": 5})
 | 
			
		||||
def generate_agent_checks_task(
 | 
			
		||||
    policy: int = None,
 | 
			
		||||
    site: int = None,
 | 
			
		||||
@@ -13,7 +13,6 @@ def generate_agent_checks_task(
 | 
			
		||||
    create_tasks: bool = False,
 | 
			
		||||
) -> Union[str, None]:
 | 
			
		||||
    from agents.models import Agent
 | 
			
		||||
 | 
			
		||||
    from automation.models import Policy
 | 
			
		||||
 | 
			
		||||
    p = Policy.objects.get(pk=policy) if policy else None
 | 
			
		||||
@@ -58,7 +57,9 @@ def generate_agent_checks_task(
 | 
			
		||||
    return "ok"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
@app.task(
 | 
			
		||||
    acks_late=True, retry_backoff=5, retry_jitter=True, retry_kwargs={"max_retries": 5}
 | 
			
		||||
)
 | 
			
		||||
# updates policy managed check fields on agents
 | 
			
		||||
def update_policy_check_fields_task(check: int) -> str:
 | 
			
		||||
    from checks.models import Check
 | 
			
		||||
@@ -74,11 +75,10 @@ def update_policy_check_fields_task(check: int) -> str:
 | 
			
		||||
    return "ok"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
@app.task(retry_backoff=5, retry_jitter=True, retry_kwargs={"max_retries": 5})
 | 
			
		||||
# generates policy tasks on agents affected by a policy
 | 
			
		||||
def generate_agent_autotasks_task(policy: int = None) -> str:
 | 
			
		||||
    from agents.models import Agent
 | 
			
		||||
 | 
			
		||||
    from automation.models import Policy
 | 
			
		||||
 | 
			
		||||
    p: Policy = Policy.objects.get(pk=policy)
 | 
			
		||||
@@ -102,7 +102,12 @@ def generate_agent_autotasks_task(policy: int = None) -> str:
 | 
			
		||||
    return "ok"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
@app.task(
 | 
			
		||||
    acks_late=True,
 | 
			
		||||
    retry_backoff=5,
 | 
			
		||||
    retry_jitter=True,
 | 
			
		||||
    retry_kwargs={"max_retries": 5},
 | 
			
		||||
)
 | 
			
		||||
def delete_policy_autotasks_task(task: int) -> str:
 | 
			
		||||
    from autotasks.models import AutomatedTask
 | 
			
		||||
 | 
			
		||||
@@ -122,7 +127,12 @@ def run_win_policy_autotasks_task(task: int) -> str:
 | 
			
		||||
    return "ok"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
@app.task(
 | 
			
		||||
    acks_late=True,
 | 
			
		||||
    retry_backoff=5,
 | 
			
		||||
    retry_jitter=True,
 | 
			
		||||
    retry_kwargs={"max_retries": 5},
 | 
			
		||||
)
 | 
			
		||||
def update_policy_autotasks_fields_task(task: int, update_agent: bool = False) -> str:
 | 
			
		||||
    from autotasks.models import AutomatedTask
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,20 +1,16 @@
 | 
			
		||||
from itertools import cycle
 | 
			
		||||
from unittest.mock import patch
 | 
			
		||||
 | 
			
		||||
from model_bakery import baker, seq
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from core.models import CoreSettings
 | 
			
		||||
from model_bakery import baker, seq
 | 
			
		||||
from tacticalrmm.test import TacticalTestCase
 | 
			
		||||
from winupdate.models import WinUpdatePolicy
 | 
			
		||||
 | 
			
		||||
from .serializers import (
 | 
			
		||||
    AutoTasksFieldSerializer,
 | 
			
		||||
    PolicyCheckSerializer,
 | 
			
		||||
    PolicyCheckStatusSerializer,
 | 
			
		||||
    PolicyOverviewSerializer,
 | 
			
		||||
    PolicySerializer,
 | 
			
		||||
    PolicyTableSerializer,
 | 
			
		||||
    PolicyTaskStatusSerializer,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -27,12 +23,10 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
    def test_get_all_policies(self):
 | 
			
		||||
        url = "/automation/policies/"
 | 
			
		||||
 | 
			
		||||
        policies = baker.make("automation.Policy", _quantity=3)
 | 
			
		||||
        baker.make("automation.Policy", _quantity=3)
 | 
			
		||||
        resp = self.client.get(url, format="json")
 | 
			
		||||
        serializer = PolicyTableSerializer(policies, many=True)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertEqual(resp.data, serializer.data)  # type: ignore
 | 
			
		||||
        self.assertEqual(len(resp.data), 3)
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
@@ -54,6 +48,8 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
    @patch("autotasks.models.AutomatedTask.create_task_on_agent")
 | 
			
		||||
    def test_add_policy(self, create_task):
 | 
			
		||||
        from automation.models import Policy
 | 
			
		||||
 | 
			
		||||
        url = "/automation/policies/"
 | 
			
		||||
 | 
			
		||||
        data = {
 | 
			
		||||
@@ -72,8 +68,12 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        # create policy with tasks and checks
 | 
			
		||||
        policy = baker.make("automation.Policy")
 | 
			
		||||
        self.create_checks(policy=policy)
 | 
			
		||||
        baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3)
 | 
			
		||||
        checks = self.create_checks(policy=policy)
 | 
			
		||||
        tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3)
 | 
			
		||||
 | 
			
		||||
        # assign a task to a check
 | 
			
		||||
        tasks[0].assigned_check = checks[0]  # type: ignore
 | 
			
		||||
        tasks[0].save()  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # test copy tasks and checks to another policy
 | 
			
		||||
        data = {
 | 
			
		||||
@@ -86,8 +86,16 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        resp = self.client.post(f"/automation/policies/", data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertEqual(policy.autotasks.count(), 3)  # type: ignore
 | 
			
		||||
        self.assertEqual(policy.policychecks.count(), 7)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        copied_policy = Policy.objects.get(name=data["name"])
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(copied_policy.autotasks.count(), 3)  # type: ignore
 | 
			
		||||
        self.assertEqual(copied_policy.policychecks.count(), 7)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # make sure correct task was assign to the check
 | 
			
		||||
        self.assertEqual(copied_policy.autotasks.get(name=tasks[0].name).assigned_check.check_type, checks[0].check_type)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        create_task.assert_not_called()
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("post", url)
 | 
			
		||||
 | 
			
		||||
@@ -110,7 +118,7 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
        resp = self.client.put(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        # only called if active or enforced are updated
 | 
			
		||||
        # only called if active, enforced, or excluded objects are updated
 | 
			
		||||
        generate_agent_checks_task.assert_not_called()
 | 
			
		||||
 | 
			
		||||
        data = {
 | 
			
		||||
@@ -120,6 +128,23 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
            "enforced": False,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        resp = self.client.put(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        generate_agent_checks_task.assert_called_with(
 | 
			
		||||
            policy=policy.pk, create_tasks=True  # type: ignore
 | 
			
		||||
        )
 | 
			
		||||
        generate_agent_checks_task.reset_mock()
 | 
			
		||||
 | 
			
		||||
        # make sure policies are re-evaluated when excluded changes
 | 
			
		||||
        agents = baker.make_recipe("agents.agent", _quantity=2)
 | 
			
		||||
        clients = baker.make("clients.Client", _quantity=2)
 | 
			
		||||
        sites = baker.make("clients.Site", _quantity=2)
 | 
			
		||||
        data = {
 | 
			
		||||
            "excluded_agents": [agent.pk for agent in agents],  # type: ignore
 | 
			
		||||
            "excluded_sites": [site.pk for site in sites],  # type: ignore
 | 
			
		||||
            "excluded_clients": [client.pk for client in clients],  # type: ignore
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        resp = self.client.put(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        generate_agent_checks_task.assert_called_with(
 | 
			
		||||
@@ -151,38 +176,6 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("delete", url)
 | 
			
		||||
 | 
			
		||||
    def test_get_all_policy_tasks(self):
 | 
			
		||||
        # create policy with tasks
 | 
			
		||||
        policy = baker.make("automation.Policy")
 | 
			
		||||
        tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3)
 | 
			
		||||
        url = f"/automation/{policy.pk}/policyautomatedtasks/"  # type: ignore
 | 
			
		||||
 | 
			
		||||
        resp = self.client.get(url, format="json")
 | 
			
		||||
        serializer = AutoTasksFieldSerializer(tasks, many=True)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertEqual(resp.data, serializer.data)  # type: ignore
 | 
			
		||||
        self.assertEqual(len(resp.data), 3)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
    def test_get_all_policy_checks(self):
 | 
			
		||||
 | 
			
		||||
        # setup data
 | 
			
		||||
        policy = baker.make("automation.Policy")
 | 
			
		||||
        checks = self.create_checks(policy=policy)
 | 
			
		||||
 | 
			
		||||
        url = f"/automation/{policy.pk}/policychecks/"  # type: ignore
 | 
			
		||||
 | 
			
		||||
        resp = self.client.get(url, format="json")
 | 
			
		||||
        serializer = PolicyCheckSerializer(checks, many=True)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertEqual(resp.data, serializer.data)  # type: ignore
 | 
			
		||||
        self.assertEqual(len(resp.data), 7)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
    def test_get_policy_check_status(self):
 | 
			
		||||
        # setup data
 | 
			
		||||
        site = baker.make("clients.Site")
 | 
			
		||||
@@ -195,14 +188,14 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
            managed_by_policy=True,
 | 
			
		||||
            parent_check=policy_diskcheck.pk,
 | 
			
		||||
        )
 | 
			
		||||
        url = f"/automation/policycheckstatus/{policy_diskcheck.pk}/check/"
 | 
			
		||||
        url = f"/automation/checks/{policy_diskcheck.pk}/status/"
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, format="json")
 | 
			
		||||
        resp = self.client.get(url, format="json")
 | 
			
		||||
        serializer = PolicyCheckStatusSerializer([managed_check], many=True)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertEqual(resp.data, serializer.data)  # type: ignore
 | 
			
		||||
        self.check_not_authenticated("patch", url)
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
    def test_policy_overview(self):
 | 
			
		||||
        from clients.models import Client
 | 
			
		||||
@@ -262,15 +255,15 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
            "autotasks.AutomatedTask", parent_task=task.id, _quantity=5  # type: ignore
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        url = f"/automation/policyautomatedtaskstatus/{task.id}/task/"  # type: ignore
 | 
			
		||||
        url = f"/automation/tasks/{task.id}/status/"  # type: ignore
 | 
			
		||||
 | 
			
		||||
        serializer = PolicyTaskStatusSerializer(policy_tasks, many=True)
 | 
			
		||||
        resp = self.client.patch(url, format="json")
 | 
			
		||||
        resp = self.client.get(url, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertEqual(resp.data, serializer.data)  # type: ignore
 | 
			
		||||
        self.assertEqual(len(resp.data), 5)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("patch", url)
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
    @patch("automation.tasks.run_win_policy_autotasks_task.delay")
 | 
			
		||||
    def test_run_win_task(self, mock_task):
 | 
			
		||||
@@ -283,16 +276,16 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
            _quantity=6,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        url = "/automation/runwintask/1/"
 | 
			
		||||
        resp = self.client.put(url, format="json")
 | 
			
		||||
        url = "/automation/tasks/1/run/"
 | 
			
		||||
        resp = self.client.post(url, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        mock_task.assert_called()  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("put", url)
 | 
			
		||||
        self.check_not_authenticated("post", url)
 | 
			
		||||
 | 
			
		||||
    def test_create_new_patch_policy(self):
 | 
			
		||||
        url = "/automation/winupdatepolicy/"
 | 
			
		||||
        url = "/automation/patchpolicy/"
 | 
			
		||||
 | 
			
		||||
        # test policy doesn't exist
 | 
			
		||||
        data = {"policy": 500}
 | 
			
		||||
@@ -323,15 +316,14 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
    def test_update_patch_policy(self):
 | 
			
		||||
 | 
			
		||||
        # test policy doesn't exist
 | 
			
		||||
        resp = self.client.put("/automation/winupdatepolicy/500/", format="json")
 | 
			
		||||
        resp = self.client.put("/automation/patchpolicy/500/", format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 404)
 | 
			
		||||
 | 
			
		||||
        policy = baker.make("automation.Policy")
 | 
			
		||||
        patch_policy = baker.make("winupdate.WinUpdatePolicy", policy=policy)
 | 
			
		||||
        url = f"/automation/winupdatepolicy/{patch_policy.pk}/"  # type: ignore
 | 
			
		||||
        url = f"/automation/patchpolicy/{patch_policy.pk}/"  # type: ignore
 | 
			
		||||
 | 
			
		||||
        data = {
 | 
			
		||||
            "id": patch_policy.pk,  # type: ignore
 | 
			
		||||
            "policy": policy.pk,  # type: ignore
 | 
			
		||||
            "critical": "approve",
 | 
			
		||||
            "important": "approve",
 | 
			
		||||
@@ -347,7 +339,7 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
        self.check_not_authenticated("put", url)
 | 
			
		||||
 | 
			
		||||
    def test_reset_patch_policy(self):
 | 
			
		||||
        url = "/automation/winupdatepolicy/reset/"
 | 
			
		||||
        url = "/automation/patchpolicy/reset/"
 | 
			
		||||
 | 
			
		||||
        inherit_fields = {
 | 
			
		||||
            "critical": "inherit",
 | 
			
		||||
@@ -376,7 +368,7 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
        # test reset agents in site
 | 
			
		||||
        data = {"site": sites[0].id}  # type: ignore
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        resp = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        agents = Agent.objects.filter(site=sites[0])  # type: ignore
 | 
			
		||||
@@ -388,7 +380,7 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
        # test reset agents in client
 | 
			
		||||
        data = {"client": clients[1].id}  # type: ignore
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        resp = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        agents = Agent.objects.filter(site__client=clients[1])  # type: ignore
 | 
			
		||||
@@ -400,7 +392,7 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
        # test reset all agents
 | 
			
		||||
        data = {}
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        resp = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        agents = Agent.objects.all()
 | 
			
		||||
@@ -408,17 +400,17 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
            for k, v in inherit_fields.items():
 | 
			
		||||
                self.assertEqual(getattr(agent.winupdatepolicy.get(), k), v)
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("patch", url)
 | 
			
		||||
        self.check_not_authenticated("post", url)
 | 
			
		||||
 | 
			
		||||
    def test_delete_patch_policy(self):
 | 
			
		||||
        # test patch policy doesn't exist
 | 
			
		||||
        resp = self.client.delete("/automation/winupdatepolicy/500/", format="json")
 | 
			
		||||
        resp = self.client.delete("/automation/patchpolicy/500/", format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 404)
 | 
			
		||||
 | 
			
		||||
        winupdate_policy = baker.make_recipe(
 | 
			
		||||
            "winupdate.winupdate_policy", policy__name="Test Policy"
 | 
			
		||||
        )
 | 
			
		||||
        url = f"/automation/winupdatepolicy/{winupdate_policy.pk}/"
 | 
			
		||||
        url = f"/automation/patchpolicy/{winupdate_policy.pk}/"
 | 
			
		||||
 | 
			
		||||
        resp = self.client.delete(url, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
@@ -473,7 +465,7 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        # Add Client to Policy
 | 
			
		||||
        policy.server_clients.add(server_agents[13].client)  # type: ignore
 | 
			
		||||
        policy.workstation_clients.add(workstation_agents[15].client)  # type: ignore
 | 
			
		||||
        policy.workstation_clients.add(workstation_agents[13].client)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        resp = self.client.get(
 | 
			
		||||
            f"/automation/policies/{policy.pk}/related/", format="json"  # type: ignore
 | 
			
		||||
@@ -481,22 +473,28 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertEquals(len(resp.data["server_clients"]), 1)  # type: ignore
 | 
			
		||||
        self.assertEquals(len(resp.data["server_sites"]), 5)  # type: ignore
 | 
			
		||||
        self.assertEquals(len(resp.data["server_sites"]), 0)  # type: ignore
 | 
			
		||||
        self.assertEquals(len(resp.data["workstation_clients"]), 1)  # type: ignore
 | 
			
		||||
        self.assertEquals(len(resp.data["workstation_sites"]), 5)  # type: ignore
 | 
			
		||||
        self.assertEquals(len(resp.data["agents"]), 10)  # type: ignore
 | 
			
		||||
        self.assertEquals(len(resp.data["workstation_sites"]), 0)  # type: ignore
 | 
			
		||||
        self.assertEquals(len(resp.data["agents"]), 0)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # Add Site to Policy and the agents and sites length shouldn't change
 | 
			
		||||
        policy.server_sites.add(server_agents[13].site)  # type: ignore
 | 
			
		||||
        policy.workstation_sites.add(workstation_agents[15].site)  # type: ignore
 | 
			
		||||
        self.assertEquals(len(resp.data["server_sites"]), 5)  # type: ignore
 | 
			
		||||
        self.assertEquals(len(resp.data["workstation_sites"]), 5)  # type: ignore
 | 
			
		||||
        self.assertEquals(len(resp.data["agents"]), 10)  # type: ignore
 | 
			
		||||
        # Add Site to Policy
 | 
			
		||||
        policy.server_sites.add(server_agents[10].site)  # type: ignore
 | 
			
		||||
        policy.workstation_sites.add(workstation_agents[10].site)  # type: ignore
 | 
			
		||||
        resp = self.client.get(
 | 
			
		||||
            f"/automation/policies/{policy.pk}/related/", format="json"  # type: ignore
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEquals(len(resp.data["server_sites"]), 1)  # type: ignore
 | 
			
		||||
        self.assertEquals(len(resp.data["workstation_sites"]), 1)  # type: ignore
 | 
			
		||||
        self.assertEquals(len(resp.data["agents"]), 0)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # Add Agent to Policy and the agents length shouldn't change
 | 
			
		||||
        policy.agents.add(server_agents[13])  # type: ignore
 | 
			
		||||
        policy.agents.add(workstation_agents[15])  # type: ignore
 | 
			
		||||
        self.assertEquals(len(resp.data["agents"]), 10)  # type: ignore
 | 
			
		||||
        # Add Agent to Policy
 | 
			
		||||
        policy.agents.add(server_agents[2])  # type: ignore
 | 
			
		||||
        policy.agents.add(workstation_agents[2])  # type: ignore
 | 
			
		||||
        resp = self.client.get(
 | 
			
		||||
            f"/automation/policies/{policy.pk}/related/", format="json"  # type: ignore
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEquals(len(resp.data["agents"]), 2)  # type: ignore
 | 
			
		||||
 | 
			
		||||
    def test_generating_agent_policy_checks(self):
 | 
			
		||||
        from .tasks import generate_agent_checks_task
 | 
			
		||||
@@ -771,6 +769,7 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
    @patch("automation.tasks.generate_agent_checks_task.delay")
 | 
			
		||||
    def test_generating_policy_checks_for_all_agents(self, generate_agent_checks_mock):
 | 
			
		||||
        from core.models import CoreSettings
 | 
			
		||||
 | 
			
		||||
        from .tasks import generate_agent_checks_task
 | 
			
		||||
 | 
			
		||||
        # setup data
 | 
			
		||||
@@ -887,11 +886,13 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
    @patch("autotasks.models.AutomatedTask.create_task_on_agent")
 | 
			
		||||
    @patch("autotasks.models.AutomatedTask.delete_task_on_agent")
 | 
			
		||||
    def test_delete_policy_tasks(self, delete_task_on_agent, create_task):
 | 
			
		||||
        from .tasks import delete_policy_autotasks_task
 | 
			
		||||
        from .tasks import delete_policy_autotasks_task, generate_agent_checks_task
 | 
			
		||||
 | 
			
		||||
        policy = baker.make("automation.Policy", active=True)
 | 
			
		||||
        tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3)
 | 
			
		||||
        baker.make_recipe("agents.server_agent", policy=policy)
 | 
			
		||||
        agent = baker.make_recipe("agents.server_agent", policy=policy)
 | 
			
		||||
 | 
			
		||||
        generate_agent_checks_task(agents=[agent.pk], create_tasks=True)
 | 
			
		||||
 | 
			
		||||
        delete_policy_autotasks_task(task=tasks[0].id)  # type: ignore
 | 
			
		||||
 | 
			
		||||
@@ -900,11 +901,13 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
    @patch("autotasks.models.AutomatedTask.create_task_on_agent")
 | 
			
		||||
    @patch("autotasks.models.AutomatedTask.run_win_task")
 | 
			
		||||
    def test_run_policy_task(self, run_win_task, create_task):
 | 
			
		||||
        from .tasks import run_win_policy_autotasks_task
 | 
			
		||||
        from .tasks import run_win_policy_autotasks_task, generate_agent_checks_task
 | 
			
		||||
 | 
			
		||||
        policy = baker.make("automation.Policy", active=True)
 | 
			
		||||
        tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3)
 | 
			
		||||
        baker.make_recipe("agents.server_agent", policy=policy)
 | 
			
		||||
        agent = baker.make_recipe("agents.server_agent", policy=policy)
 | 
			
		||||
 | 
			
		||||
        generate_agent_checks_task(agents=[agent.pk], create_tasks=True)
 | 
			
		||||
 | 
			
		||||
        run_win_policy_autotasks_task(task=tasks[0].id)  # type: ignore
 | 
			
		||||
 | 
			
		||||
@@ -913,7 +916,10 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
    @patch("autotasks.models.AutomatedTask.create_task_on_agent")
 | 
			
		||||
    @patch("autotasks.models.AutomatedTask.modify_task_on_agent")
 | 
			
		||||
    def test_update_policy_tasks(self, modify_task_on_agent, create_task):
 | 
			
		||||
        from .tasks import update_policy_autotasks_fields_task
 | 
			
		||||
        from .tasks import (
 | 
			
		||||
            update_policy_autotasks_fields_task,
 | 
			
		||||
            generate_agent_checks_task,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # setup data
 | 
			
		||||
        policy = baker.make("automation.Policy", active=True)
 | 
			
		||||
@@ -925,6 +931,8 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
        )
 | 
			
		||||
        agent = baker.make_recipe("agents.server_agent", policy=policy)
 | 
			
		||||
 | 
			
		||||
        generate_agent_checks_task(agents=[agent.pk], create_tasks=True)
 | 
			
		||||
 | 
			
		||||
        tasks[0].enabled = False  # type: ignore
 | 
			
		||||
        tasks[0].save()  # type: ignore
 | 
			
		||||
 | 
			
		||||
@@ -964,6 +972,8 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
    @patch("autotasks.models.AutomatedTask.create_task_on_agent")
 | 
			
		||||
    def test_policy_exclusions(self, create_task):
 | 
			
		||||
        from .tasks import generate_agent_checks_task
 | 
			
		||||
 | 
			
		||||
        # setup data
 | 
			
		||||
        policy = baker.make("automation.Policy", active=True)
 | 
			
		||||
        baker.make_recipe("checks.memory_check", policy=policy)
 | 
			
		||||
@@ -972,6 +982,8 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
            "agents.agent", policy=policy, monitoring_type="server"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        generate_agent_checks_task(agents=[agent.pk], create_tasks=True)
 | 
			
		||||
 | 
			
		||||
        # make sure related agents on policy returns correctly
 | 
			
		||||
        self.assertEqual(policy.related_agents().count(), 1)  # type: ignore
 | 
			
		||||
        self.assertEqual(agent.agentchecks.count(), 1)  # type: ignore
 | 
			
		||||
@@ -1133,3 +1145,9 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
        # should get policies from agent policy
 | 
			
		||||
        self.assertTrue(agent.autotasks.all())
 | 
			
		||||
        self.assertTrue(agent.agentchecks.all())
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestAutomationPermission(TacticalTestCase):
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        self.client_setup()
 | 
			
		||||
        self.setup_coresettings()
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,8 @@
 | 
			
		||||
from django.urls import path
 | 
			
		||||
 | 
			
		||||
from . import views
 | 
			
		||||
from checks.views import GetAddChecks
 | 
			
		||||
from autotasks.views import GetAddAutoTasks
 | 
			
		||||
 | 
			
		||||
urlpatterns = [
 | 
			
		||||
    path("policies/", views.GetAddPolicies.as_view()),
 | 
			
		||||
@@ -8,12 +10,14 @@ urlpatterns = [
 | 
			
		||||
    path("policies/overview/", views.OverviewPolicy.as_view()),
 | 
			
		||||
    path("policies/<int:pk>/", views.GetUpdateDeletePolicy.as_view()),
 | 
			
		||||
    path("sync/", views.PolicySync.as_view()),
 | 
			
		||||
    path("<int:pk>/policychecks/", views.PolicyCheck.as_view()),
 | 
			
		||||
    path("<int:pk>/policyautomatedtasks/", views.PolicyAutoTask.as_view()),
 | 
			
		||||
    path("policycheckstatus/<int:check>/check/", views.PolicyCheck.as_view()),
 | 
			
		||||
    path("policyautomatedtaskstatus/<int:task>/task/", views.PolicyAutoTask.as_view()),
 | 
			
		||||
    path("runwintask/<int:task>/", views.PolicyAutoTask.as_view()),
 | 
			
		||||
    path("winupdatepolicy/", views.UpdatePatchPolicy.as_view()),
 | 
			
		||||
    path("winupdatepolicy/<int:patchpolicy>/", views.UpdatePatchPolicy.as_view()),
 | 
			
		||||
    path("winupdatepolicy/reset/", views.UpdatePatchPolicy.as_view()),
 | 
			
		||||
    # alias to get policy checks
 | 
			
		||||
    path("policies/<int:policy>/checks/", GetAddChecks.as_view()),
 | 
			
		||||
    # alias to get policy tasks
 | 
			
		||||
    path("policies/<int:policy>/tasks/", GetAddAutoTasks.as_view()),
 | 
			
		||||
    path("checks/<int:check>/status/", views.PolicyCheck.as_view()),
 | 
			
		||||
    path("tasks/<int:task>/status/", views.PolicyAutoTask.as_view()),
 | 
			
		||||
    path("tasks/<int:task>/run/", views.PolicyAutoTask.as_view()),
 | 
			
		||||
    path("patchpolicy/", views.UpdatePatchPolicy.as_view()),
 | 
			
		||||
    path("patchpolicy/<int:pk>/", views.UpdatePatchPolicy.as_view()),
 | 
			
		||||
    path("patchpolicy/reset/", views.ResetPatchPolicy.as_view()),
 | 
			
		||||
]
 | 
			
		||||
 
 | 
			
		||||
@@ -1,21 +1,22 @@
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from agents.serializers import AgentHostnameSerializer
 | 
			
		||||
from autotasks.models import AutomatedTask
 | 
			
		||||
from checks.models import Check
 | 
			
		||||
from clients.models import Client
 | 
			
		||||
from clients.serializers import ClientSerializer, SiteSerializer
 | 
			
		||||
from django.shortcuts import get_object_or_404
 | 
			
		||||
from rest_framework.permissions import IsAuthenticated
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
from rest_framework.views import APIView
 | 
			
		||||
from rest_framework.exceptions import PermissionDenied
 | 
			
		||||
from tacticalrmm.utils import notify_error
 | 
			
		||||
from tacticalrmm.permissions import _has_perm_on_client, _has_perm_on_site
 | 
			
		||||
from winupdate.models import WinUpdatePolicy
 | 
			
		||||
from winupdate.serializers import WinUpdatePolicySerializer
 | 
			
		||||
 | 
			
		||||
from .models import Policy
 | 
			
		||||
from .permissions import AutomationPolicyPerms
 | 
			
		||||
from .serializers import (
 | 
			
		||||
    AutoTasksFieldSerializer,
 | 
			
		||||
    PolicyCheckSerializer,
 | 
			
		||||
    PolicyCheckStatusSerializer,
 | 
			
		||||
    PolicyRelatedSerializer,
 | 
			
		||||
    PolicyOverviewSerializer,
 | 
			
		||||
    PolicySerializer,
 | 
			
		||||
    PolicyTableSerializer,
 | 
			
		||||
@@ -24,10 +25,16 @@ from .serializers import (
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetAddPolicies(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, AutomationPolicyPerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request):
 | 
			
		||||
        policies = Policy.objects.all()
 | 
			
		||||
 | 
			
		||||
        return Response(PolicyTableSerializer(policies, many=True).data)
 | 
			
		||||
        return Response(
 | 
			
		||||
            PolicyTableSerializer(
 | 
			
		||||
                policies, context={"user": request.user}, many=True
 | 
			
		||||
            ).data
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
        serializer = PolicySerializer(data=request.data, partial=True)
 | 
			
		||||
@@ -51,18 +58,30 @@ class GetAddPolicies(APIView):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetUpdateDeletePolicy(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, AutomationPolicyPerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request, pk):
 | 
			
		||||
        policy = get_object_or_404(Policy, pk=pk)
 | 
			
		||||
 | 
			
		||||
        return Response(PolicySerializer(policy).data)
 | 
			
		||||
 | 
			
		||||
    def put(self, request, pk):
 | 
			
		||||
        from .tasks import generate_agent_checks_task
 | 
			
		||||
 | 
			
		||||
        policy = get_object_or_404(Policy, pk=pk)
 | 
			
		||||
 | 
			
		||||
        serializer = PolicySerializer(instance=policy, data=request.data, partial=True)
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        serializer.save()
 | 
			
		||||
 | 
			
		||||
        # check for excluding objects and in the request and if present generate policies
 | 
			
		||||
        if (
 | 
			
		||||
            "excluded_sites" in request.data.keys()
 | 
			
		||||
            or "excluded_clients" in request.data.keys()
 | 
			
		||||
            or "excluded_agents" in request.data.keys()
 | 
			
		||||
        ):
 | 
			
		||||
            generate_agent_checks_task.delay(policy=pk, create_tasks=True)
 | 
			
		||||
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
    def delete(self, request, pk):
 | 
			
		||||
@@ -87,18 +106,13 @@ class PolicySync(APIView):
 | 
			
		||||
 | 
			
		||||
class PolicyAutoTask(APIView):
 | 
			
		||||
 | 
			
		||||
    # tasks associated with policy
 | 
			
		||||
    def get(self, request, pk):
 | 
			
		||||
        tasks = AutomatedTask.objects.filter(policy=pk)
 | 
			
		||||
        return Response(AutoTasksFieldSerializer(tasks, many=True).data)
 | 
			
		||||
 | 
			
		||||
    # get status of all tasks
 | 
			
		||||
    def patch(self, request, task):
 | 
			
		||||
    def get(self, request, task):
 | 
			
		||||
        tasks = AutomatedTask.objects.filter(parent_task=task)
 | 
			
		||||
        return Response(PolicyTaskStatusSerializer(tasks, many=True).data)
 | 
			
		||||
 | 
			
		||||
    # bulk run win tasks associated with policy
 | 
			
		||||
    def put(self, request, task):
 | 
			
		||||
    def post(self, request, task):
 | 
			
		||||
        from .tasks import run_win_policy_autotasks_task
 | 
			
		||||
 | 
			
		||||
        run_win_policy_autotasks_task.delay(task=task)
 | 
			
		||||
@@ -106,11 +120,9 @@ class PolicyAutoTask(APIView):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PolicyCheck(APIView):
 | 
			
		||||
    def get(self, request, pk):
 | 
			
		||||
        checks = Check.objects.filter(policy__pk=pk, agent=None)
 | 
			
		||||
        return Response(PolicyCheckSerializer(checks, many=True).data)
 | 
			
		||||
    permission_classes = [IsAuthenticated, AutomationPolicyPerms]
 | 
			
		||||
 | 
			
		||||
    def patch(self, request, check):
 | 
			
		||||
    def get(self, request, check):
 | 
			
		||||
        checks = Check.objects.filter(parent_check=check)
 | 
			
		||||
        return Response(PolicyCheckStatusSerializer(checks, many=True).data)
 | 
			
		||||
 | 
			
		||||
@@ -125,8 +137,6 @@ class OverviewPolicy(APIView):
 | 
			
		||||
class GetRelated(APIView):
 | 
			
		||||
    def get(self, request, pk):
 | 
			
		||||
 | 
			
		||||
        response = {}
 | 
			
		||||
 | 
			
		||||
        policy = (
 | 
			
		||||
            Policy.objects.filter(pk=pk)
 | 
			
		||||
            .prefetch_related(
 | 
			
		||||
@@ -138,47 +148,13 @@ class GetRelated(APIView):
 | 
			
		||||
            .first()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        response["default_server_policy"] = policy.is_default_server_policy
 | 
			
		||||
        response["default_workstation_policy"] = policy.is_default_workstation_policy
 | 
			
		||||
 | 
			
		||||
        response["server_clients"] = ClientSerializer(
 | 
			
		||||
            policy.server_clients.all(), many=True
 | 
			
		||||
        ).data
 | 
			
		||||
        response["workstation_clients"] = ClientSerializer(
 | 
			
		||||
            policy.workstation_clients.all(), many=True
 | 
			
		||||
        ).data
 | 
			
		||||
 | 
			
		||||
        filtered_server_sites = list()
 | 
			
		||||
        filtered_workstation_sites = list()
 | 
			
		||||
 | 
			
		||||
        for client in policy.server_clients.all():
 | 
			
		||||
            for site in client.sites.all():
 | 
			
		||||
                if site not in policy.server_sites.all():
 | 
			
		||||
                    filtered_server_sites.append(site)
 | 
			
		||||
 | 
			
		||||
        response["server_sites"] = SiteSerializer(
 | 
			
		||||
            filtered_server_sites + list(policy.server_sites.all()), many=True
 | 
			
		||||
        ).data
 | 
			
		||||
 | 
			
		||||
        for client in policy.workstation_clients.all():
 | 
			
		||||
            for site in client.sites.all():
 | 
			
		||||
                if site not in policy.workstation_sites.all():
 | 
			
		||||
                    filtered_workstation_sites.append(site)
 | 
			
		||||
 | 
			
		||||
        response["workstation_sites"] = SiteSerializer(
 | 
			
		||||
            filtered_workstation_sites + list(policy.workstation_sites.all()), many=True
 | 
			
		||||
        ).data
 | 
			
		||||
 | 
			
		||||
        response["agents"] = AgentHostnameSerializer(
 | 
			
		||||
            policy.related_agents().only("pk", "hostname"),
 | 
			
		||||
            many=True,
 | 
			
		||||
        ).data
 | 
			
		||||
 | 
			
		||||
        return Response(response)
 | 
			
		||||
        return Response(
 | 
			
		||||
            PolicyRelatedSerializer(policy, context={"user": request.user}).data
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UpdatePatchPolicy(APIView):
 | 
			
		||||
 | 
			
		||||
    permission_classes = [IsAuthenticated, AutomationPolicyPerms]
 | 
			
		||||
    # create new patch policy
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
        policy = get_object_or_404(Policy, pk=request.data["policy"])
 | 
			
		||||
@@ -191,8 +167,8 @@ class UpdatePatchPolicy(APIView):
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
    # update patch policy
 | 
			
		||||
    def put(self, request, patchpolicy):
 | 
			
		||||
        policy = get_object_or_404(WinUpdatePolicy, pk=patchpolicy)
 | 
			
		||||
    def put(self, request, pk):
 | 
			
		||||
        policy = get_object_or_404(WinUpdatePolicy, pk=pk)
 | 
			
		||||
 | 
			
		||||
        serializer = WinUpdatePolicySerializer(
 | 
			
		||||
            instance=policy, data=request.data, partial=True
 | 
			
		||||
@@ -202,20 +178,41 @@ class UpdatePatchPolicy(APIView):
 | 
			
		||||
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
    # bulk reset agent patch policy
 | 
			
		||||
    def patch(self, request):
 | 
			
		||||
    # delete patch policy
 | 
			
		||||
    def delete(self, request, pk):
 | 
			
		||||
        get_object_or_404(WinUpdatePolicy, pk=pk).delete()
 | 
			
		||||
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ResetPatchPolicy(APIView):
 | 
			
		||||
    # bulk reset agent patch policy
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
 | 
			
		||||
        agents = None
 | 
			
		||||
        if "client" in request.data:
 | 
			
		||||
            agents = Agent.objects.prefetch_related("winupdatepolicy").filter(
 | 
			
		||||
                site__client_id=request.data["client"]
 | 
			
		||||
            if not _has_perm_on_client(request.user, request.data["client"]):
 | 
			
		||||
                raise PermissionDenied()
 | 
			
		||||
 | 
			
		||||
            agents = (
 | 
			
		||||
                Agent.objects.filter_by_role(request.user)
 | 
			
		||||
                .prefetch_related("winupdatepolicy")
 | 
			
		||||
                .filter(site__client_id=request.data["client"])
 | 
			
		||||
            )
 | 
			
		||||
        elif "site" in request.data:
 | 
			
		||||
            agents = Agent.objects.prefetch_related("winupdatepolicy").filter(
 | 
			
		||||
                site_id=request.data["site"]
 | 
			
		||||
            if not _has_perm_on_site(request.user, request.data["site"]):
 | 
			
		||||
                raise PermissionDenied()
 | 
			
		||||
 | 
			
		||||
            agents = (
 | 
			
		||||
                Agent.objects.filter_by_role(request.user)
 | 
			
		||||
                .prefetch_related("winupdatepolicy")
 | 
			
		||||
                .filter(site_id=request.data["site"])
 | 
			
		||||
            )
 | 
			
		||||
        else:
 | 
			
		||||
            agents = Agent.objects.prefetch_related("winupdatepolicy").only("pk")
 | 
			
		||||
            agents = (
 | 
			
		||||
                Agent.objects.filter_by_role(request.user)
 | 
			
		||||
                .prefetch_related("winupdatepolicy")
 | 
			
		||||
                .only("pk")
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        for agent in agents:
 | 
			
		||||
            winupdatepolicy = agent.winupdatepolicy.get()
 | 
			
		||||
@@ -240,10 +237,4 @@ class UpdatePatchPolicy(APIView):
 | 
			
		||||
                ]
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
    # delete patch policy
 | 
			
		||||
    def delete(self, request, patchpolicy):
 | 
			
		||||
        get_object_or_404(WinUpdatePolicy, pk=patchpolicy).delete()
 | 
			
		||||
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
        return Response("The patch policy on the affected agents has been reset.")
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
# Generated by Django 3.1.7 on 2021-04-04 00:32
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
# Generated by Django 3.1.7 on 2021-04-27 14:11
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,18 @@
 | 
			
		||||
# Generated by Django 3.2.1 on 2021-05-29 03:26
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('autotasks', '0021_alter_automatedtask_custom_field'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='automatedtask',
 | 
			
		||||
            name='collector_all_output',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -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),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -6,18 +6,16 @@ from typing import List
 | 
			
		||||
 | 
			
		||||
import pytz
 | 
			
		||||
from alerts.models import SEVERITY_CHOICES
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.contrib.postgres.fields import ArrayField
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.db.models.fields import DateTimeField
 | 
			
		||||
from django.db.utils import DatabaseError
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from logs.models import BaseAuditModel
 | 
			
		||||
from loguru import logger
 | 
			
		||||
from logs.models import BaseAuditModel, DebugLog
 | 
			
		||||
from tacticalrmm.models import PermissionQuerySet
 | 
			
		||||
from packaging import version as pyver
 | 
			
		||||
from tacticalrmm.utils import bitdays_to_string
 | 
			
		||||
 | 
			
		||||
logger.configure(**settings.LOG_CONFIG)
 | 
			
		||||
 | 
			
		||||
RUN_TIME_DAY_CHOICES = [
 | 
			
		||||
    (0, "Monday"),
 | 
			
		||||
    (1, "Tuesday"),
 | 
			
		||||
@@ -50,6 +48,8 @@ TASK_STATUS_CHOICES = [
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AutomatedTask(BaseAuditModel):
 | 
			
		||||
    objects = PermissionQuerySet.as_manager()
 | 
			
		||||
 | 
			
		||||
    agent = models.ForeignKey(
 | 
			
		||||
        "agents.Agent",
 | 
			
		||||
        related_name="autotasks",
 | 
			
		||||
@@ -104,6 +104,7 @@ class AutomatedTask(BaseAuditModel):
 | 
			
		||||
    task_type = models.CharField(
 | 
			
		||||
        max_length=100, choices=TASK_TYPE_CHOICES, default="manual"
 | 
			
		||||
    )
 | 
			
		||||
    collector_all_output = models.BooleanField(default=False)
 | 
			
		||||
    run_time_date = DateTimeField(null=True, blank=True)
 | 
			
		||||
    remove_if_not_scheduled = models.BooleanField(default=False)
 | 
			
		||||
    run_asap_after_missed = models.BooleanField(default=False)  # added in agent v1.4.7
 | 
			
		||||
@@ -134,6 +135,31 @@ class AutomatedTask(BaseAuditModel):
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return self.name
 | 
			
		||||
 | 
			
		||||
    def save(self, *args, **kwargs):
 | 
			
		||||
        from autotasks.tasks import enable_or_disable_win_task
 | 
			
		||||
        from automation.tasks import update_policy_autotasks_fields_task
 | 
			
		||||
 | 
			
		||||
        # get old agent if exists
 | 
			
		||||
        old_task = AutomatedTask.objects.get(pk=self.pk) if self.pk else None
 | 
			
		||||
        super(AutomatedTask, self).save(old_model=old_task, *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
        # check if automated task was enabled/disabled and send celery task
 | 
			
		||||
        if old_task and old_task.enabled != self.enabled:
 | 
			
		||||
            if self.agent:
 | 
			
		||||
                enable_or_disable_win_task.delay(pk=self.pk)
 | 
			
		||||
 | 
			
		||||
            # check if automated task was enabled/disabled and send celery task
 | 
			
		||||
            elif old_task.policy:
 | 
			
		||||
                update_policy_autotasks_fields_task.delay(
 | 
			
		||||
                    task=self.pk, update_agent=True
 | 
			
		||||
                )
 | 
			
		||||
        # check if policy task was edited and then check if it was a field worth copying to rest of agent tasks
 | 
			
		||||
        elif old_task and old_task.policy:
 | 
			
		||||
            for field in self.policy_fields_to_copy:
 | 
			
		||||
                if getattr(self, field) != getattr(old_task, field):
 | 
			
		||||
                    update_policy_autotasks_fields_task.delay(task=self.pk)
 | 
			
		||||
                    break
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def schedule(self):
 | 
			
		||||
        if self.task_type == "manual":
 | 
			
		||||
@@ -182,6 +208,7 @@ class AutomatedTask(BaseAuditModel):
 | 
			
		||||
            "remove_if_not_scheduled",
 | 
			
		||||
            "run_asap_after_missed",
 | 
			
		||||
            "custom_field",
 | 
			
		||||
            "collector_all_output",
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
@@ -192,37 +219,31 @@ class AutomatedTask(BaseAuditModel):
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def serialize(task):
 | 
			
		||||
        # serializes the task and returns json
 | 
			
		||||
        from .serializers import TaskSerializer
 | 
			
		||||
        from .serializers import TaskAuditSerializer
 | 
			
		||||
 | 
			
		||||
        return TaskSerializer(task).data
 | 
			
		||||
        return TaskAuditSerializer(task).data
 | 
			
		||||
 | 
			
		||||
    def create_policy_task(self, agent=None, policy=None):
 | 
			
		||||
    def create_policy_task(self, agent=None, policy=None, assigned_check=None):
 | 
			
		||||
 | 
			
		||||
        # added to allow new policy tasks to be assigned to check only when the agent check exists already
 | 
			
		||||
        if (
 | 
			
		||||
            self.assigned_check
 | 
			
		||||
            and agent
 | 
			
		||||
            and agent.agentchecks.filter(parent_check=self.assigned_check.id).exists()
 | 
			
		||||
        ):
 | 
			
		||||
            assigned_check = agent.agentchecks.get(parent_check=self.assigned_check.id)
 | 
			
		||||
 | 
			
		||||
        # if policy is present, then this task is being copied to another policy
 | 
			
		||||
        # if agent is present, then this task is being created on an agent from a policy
 | 
			
		||||
        # exit if neither are set or if both are set
 | 
			
		||||
        if not agent and not policy or agent and policy:
 | 
			
		||||
        # also exit if assigned_check is set because this task will be created when the check is
 | 
			
		||||
        if (
 | 
			
		||||
            (not agent and not policy)
 | 
			
		||||
            or (agent and policy)
 | 
			
		||||
            or (self.assigned_check and not assigned_check)
 | 
			
		||||
        ):
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        assigned_check = None
 | 
			
		||||
 | 
			
		||||
        # get correct assigned check to task if set
 | 
			
		||||
        if agent and self.assigned_check:
 | 
			
		||||
            # check if there is a matching check on the agent
 | 
			
		||||
            if agent.agentchecks.filter(parent_check=self.assigned_check.pk).exists():
 | 
			
		||||
                assigned_check = agent.agentchecks.filter(
 | 
			
		||||
                    parent_check=self.assigned_check.pk
 | 
			
		||||
                ).first()
 | 
			
		||||
        elif policy and self.assigned_check:
 | 
			
		||||
            if policy.policychecks.filter(name=self.assigned_check.name).exists():
 | 
			
		||||
                assigned_check = policy.policychecks.filter(
 | 
			
		||||
                    name=self.assigned_check.name
 | 
			
		||||
                ).first()
 | 
			
		||||
            else:
 | 
			
		||||
                assigned_check = policy.policychecks.filter(
 | 
			
		||||
                    check_type=self.assigned_check.check_type
 | 
			
		||||
                ).first()
 | 
			
		||||
 | 
			
		||||
        task = AutomatedTask.objects.create(
 | 
			
		||||
            agent=agent,
 | 
			
		||||
            policy=policy,
 | 
			
		||||
@@ -232,11 +253,13 @@ class AutomatedTask(BaseAuditModel):
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        for field in self.policy_fields_to_copy:
 | 
			
		||||
            setattr(task, field, getattr(self, field))
 | 
			
		||||
            if field != "assigned_check":
 | 
			
		||||
                setattr(task, field, getattr(self, field))
 | 
			
		||||
 | 
			
		||||
        task.save()
 | 
			
		||||
 | 
			
		||||
        task.create_task_on_agent()
 | 
			
		||||
        if agent:
 | 
			
		||||
            task.create_task_on_agent()
 | 
			
		||||
 | 
			
		||||
    def create_task_on_agent(self):
 | 
			
		||||
        from agents.models import Agent
 | 
			
		||||
@@ -263,7 +286,7 @@ class AutomatedTask(BaseAuditModel):
 | 
			
		||||
 | 
			
		||||
        elif self.task_type == "runonce":
 | 
			
		||||
            # check if scheduled time is in the past
 | 
			
		||||
            agent_tz = pytz.timezone(agent.timezone)
 | 
			
		||||
            agent_tz = pytz.timezone(agent.timezone)  # type: ignore
 | 
			
		||||
            task_time_utc = self.run_time_date.replace(tzinfo=agent_tz).astimezone(
 | 
			
		||||
                pytz.utc
 | 
			
		||||
            )
 | 
			
		||||
@@ -289,7 +312,7 @@ class AutomatedTask(BaseAuditModel):
 | 
			
		||||
                },
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if self.run_asap_after_missed and pyver.parse(agent.version) >= pyver.parse(
 | 
			
		||||
            if self.run_asap_after_missed and pyver.parse(agent.version) >= pyver.parse(  # type: ignore
 | 
			
		||||
                "1.4.7"
 | 
			
		||||
            ):
 | 
			
		||||
                nats_data["schedtaskpayload"]["run_asap_after_missed"] = True
 | 
			
		||||
@@ -310,19 +333,25 @@ class AutomatedTask(BaseAuditModel):
 | 
			
		||||
        else:
 | 
			
		||||
            return "error"
 | 
			
		||||
 | 
			
		||||
        r = asyncio.run(agent.nats_cmd(nats_data, timeout=5))
 | 
			
		||||
        r = asyncio.run(agent.nats_cmd(nats_data, timeout=5))  # type: ignore
 | 
			
		||||
 | 
			
		||||
        if r != "ok":
 | 
			
		||||
            self.sync_status = "initial"
 | 
			
		||||
            self.save(update_fields=["sync_status"])
 | 
			
		||||
            logger.warning(
 | 
			
		||||
                f"Unable to create scheduled task {self.name} on {agent.hostname}. It will be created when the agent checks in."
 | 
			
		||||
            DebugLog.warning(
 | 
			
		||||
                agent=agent,
 | 
			
		||||
                log_type="agent_issues",
 | 
			
		||||
                message=f"Unable to create scheduled task {self.name} on {agent.hostname}. It will be created when the agent checks in.",  # type: ignore
 | 
			
		||||
            )
 | 
			
		||||
            return "timeout"
 | 
			
		||||
        else:
 | 
			
		||||
            self.sync_status = "synced"
 | 
			
		||||
            self.save(update_fields=["sync_status"])
 | 
			
		||||
            logger.info(f"{agent.hostname} task {self.name} was successfully created")
 | 
			
		||||
            DebugLog.info(
 | 
			
		||||
                agent=agent,
 | 
			
		||||
                log_type="agent_issues",
 | 
			
		||||
                message=f"{agent.hostname} task {self.name} was successfully created",  # type: ignore
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        return "ok"
 | 
			
		||||
 | 
			
		||||
@@ -342,19 +371,25 @@ class AutomatedTask(BaseAuditModel):
 | 
			
		||||
                "enabled": self.enabled,
 | 
			
		||||
            },
 | 
			
		||||
        }
 | 
			
		||||
        r = asyncio.run(agent.nats_cmd(nats_data, timeout=5))
 | 
			
		||||
        r = asyncio.run(agent.nats_cmd(nats_data, timeout=5))  # type: ignore
 | 
			
		||||
 | 
			
		||||
        if r != "ok":
 | 
			
		||||
            self.sync_status = "notsynced"
 | 
			
		||||
            self.save(update_fields=["sync_status"])
 | 
			
		||||
            logger.warning(
 | 
			
		||||
                f"Unable to modify scheduled task {self.name} on {agent.hostname}. It will try again on next agent checkin"
 | 
			
		||||
            DebugLog.warning(
 | 
			
		||||
                agent=agent,
 | 
			
		||||
                log_type="agent_issues",
 | 
			
		||||
                message=f"Unable to modify scheduled task {self.name} on {agent.hostname}({agent.pk}). It will try again on next agent checkin",  # type: ignore
 | 
			
		||||
            )
 | 
			
		||||
            return "timeout"
 | 
			
		||||
        else:
 | 
			
		||||
            self.sync_status = "synced"
 | 
			
		||||
            self.save(update_fields=["sync_status"])
 | 
			
		||||
            logger.info(f"{agent.hostname} task {self.name} was successfully modified")
 | 
			
		||||
            DebugLog.info(
 | 
			
		||||
                agent=agent,
 | 
			
		||||
                log_type="agent_issues",
 | 
			
		||||
                message=f"{agent.hostname} task {self.name} was successfully modified",  # type: ignore
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        return "ok"
 | 
			
		||||
 | 
			
		||||
@@ -371,18 +406,29 @@ class AutomatedTask(BaseAuditModel):
 | 
			
		||||
            "func": "delschedtask",
 | 
			
		||||
            "schedtaskpayload": {"name": self.win_task_name},
 | 
			
		||||
        }
 | 
			
		||||
        r = asyncio.run(agent.nats_cmd(nats_data, timeout=10))
 | 
			
		||||
        r = asyncio.run(agent.nats_cmd(nats_data, timeout=10))  # type: ignore
 | 
			
		||||
 | 
			
		||||
        if r != "ok" and "The system cannot find the file specified" not in r:
 | 
			
		||||
            self.sync_status = "pendingdeletion"
 | 
			
		||||
            self.save(update_fields=["sync_status"])
 | 
			
		||||
            logger.warning(
 | 
			
		||||
                f"{agent.hostname} task {self.name} was successfully modified"
 | 
			
		||||
 | 
			
		||||
            try:
 | 
			
		||||
                self.save(update_fields=["sync_status"])
 | 
			
		||||
            except DatabaseError:
 | 
			
		||||
                pass
 | 
			
		||||
 | 
			
		||||
            DebugLog.warning(
 | 
			
		||||
                agent=agent,
 | 
			
		||||
                log_type="agent_issues",
 | 
			
		||||
                message=f"{agent.hostname} task {self.name} will be deleted on next checkin",  # type: ignore
 | 
			
		||||
            )
 | 
			
		||||
            return "timeout"
 | 
			
		||||
        else:
 | 
			
		||||
            self.delete()
 | 
			
		||||
            logger.info(f"{agent.hostname} task {self.name} was deleted")
 | 
			
		||||
            DebugLog.info(
 | 
			
		||||
                agent=agent,
 | 
			
		||||
                log_type="agent_issues",
 | 
			
		||||
                message=f"{agent.hostname}({agent.pk}) task {self.name} was deleted",  # type: ignore
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        return "ok"
 | 
			
		||||
 | 
			
		||||
@@ -395,9 +441,20 @@ class AutomatedTask(BaseAuditModel):
 | 
			
		||||
            .first()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        asyncio.run(agent.nats_cmd({"func": "runtask", "taskpk": self.pk}, wait=False))
 | 
			
		||||
        asyncio.run(agent.nats_cmd({"func": "runtask", "taskpk": self.pk}, wait=False))  # type: ignore
 | 
			
		||||
        return "ok"
 | 
			
		||||
 | 
			
		||||
    def save_collector_results(self):
 | 
			
		||||
 | 
			
		||||
        agent_field = self.custom_field.get_or_create_field_value(self.agent)
 | 
			
		||||
 | 
			
		||||
        value = (
 | 
			
		||||
            self.stdout.strip()
 | 
			
		||||
            if self.collector_all_output
 | 
			
		||||
            else self.stdout.strip().split("\n")[-1].strip()
 | 
			
		||||
        )
 | 
			
		||||
        agent_field.save_to_field(value)
 | 
			
		||||
 | 
			
		||||
    def should_create_alert(self, alert_template=None):
 | 
			
		||||
        return (
 | 
			
		||||
            self.dashboard_alert
 | 
			
		||||
@@ -417,9 +474,9 @@ class AutomatedTask(BaseAuditModel):
 | 
			
		||||
        from core.models import CoreSettings
 | 
			
		||||
 | 
			
		||||
        CORE = CoreSettings.objects.first()
 | 
			
		||||
 | 
			
		||||
        # Format of Email sent when Task has email alert
 | 
			
		||||
        if self.agent:
 | 
			
		||||
            subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Failed"
 | 
			
		||||
            subject = f"{self.agent.client.name}, {self.agent.site.name}, {self.agent.hostname} - {self} Failed"
 | 
			
		||||
        else:
 | 
			
		||||
            subject = f"{self} Failed"
 | 
			
		||||
 | 
			
		||||
@@ -428,16 +485,15 @@ class AutomatedTask(BaseAuditModel):
 | 
			
		||||
            + f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        CORE.send_mail(subject, body, self.agent.alert_template)
 | 
			
		||||
        CORE.send_mail(subject, body, self.agent.alert_template)  # type: ignore
 | 
			
		||||
 | 
			
		||||
    def send_sms(self):
 | 
			
		||||
 | 
			
		||||
        from core.models import CoreSettings
 | 
			
		||||
 | 
			
		||||
        CORE = CoreSettings.objects.first()
 | 
			
		||||
 | 
			
		||||
        # Format of SMS sent when Task has SMS alert
 | 
			
		||||
        if self.agent:
 | 
			
		||||
            subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Failed"
 | 
			
		||||
            subject = f"{self.agent.client.name}, {self.agent.site.name}, {self.agent.hostname} - {self} Failed"
 | 
			
		||||
        else:
 | 
			
		||||
            subject = f"{self} Failed"
 | 
			
		||||
 | 
			
		||||
@@ -446,7 +502,7 @@ class AutomatedTask(BaseAuditModel):
 | 
			
		||||
            + f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        CORE.send_sms(body, alert_template=self.agent.alert_template)
 | 
			
		||||
        CORE.send_sms(body, alert_template=self.agent.alert_template)  # type: ignore
 | 
			
		||||
 | 
			
		||||
    def send_resolved_email(self):
 | 
			
		||||
        from core.models import CoreSettings
 | 
			
		||||
@@ -458,7 +514,7 @@ class AutomatedTask(BaseAuditModel):
 | 
			
		||||
            + f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        CORE.send_mail(subject, body, alert_template=self.agent.alert_template)
 | 
			
		||||
        CORE.send_mail(subject, body, alert_template=self.agent.alert_template)  # type: ignore
 | 
			
		||||
 | 
			
		||||
    def send_resolved_sms(self):
 | 
			
		||||
        from core.models import CoreSettings
 | 
			
		||||
@@ -469,4 +525,4 @@ class AutomatedTask(BaseAuditModel):
 | 
			
		||||
            subject
 | 
			
		||||
            + f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}"
 | 
			
		||||
        )
 | 
			
		||||
        CORE.send_sms(body, alert_template=self.agent.alert_template)
 | 
			
		||||
        CORE.send_sms(body, alert_template=self.agent.alert_template)  # type: ignore
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										21
									
								
								api/tacticalrmm/autotasks/permissions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								api/tacticalrmm/autotasks/permissions.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
			
		||||
from rest_framework import permissions
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.permissions import _has_perm, _has_perm_on_agent
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AutoTaskPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        if r.method == "GET":
 | 
			
		||||
            if "agent_id" in view.kwargs.keys():
 | 
			
		||||
                return _has_perm(r, "can_list_autotasks") and _has_perm_on_agent(
 | 
			
		||||
                    r.user, view.kwargs["agent_id"]
 | 
			
		||||
                )
 | 
			
		||||
            else:
 | 
			
		||||
                return _has_perm(r, "can_list_autotasks")
 | 
			
		||||
        else:
 | 
			
		||||
            return _has_perm(r, "can_manage_autotasks")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RunAutoTaskPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        return _has_perm(r, "can_run_autotasks")
 | 
			
		||||
@@ -10,7 +10,7 @@ from .models import AutomatedTask
 | 
			
		||||
 | 
			
		||||
class TaskSerializer(serializers.ModelSerializer):
 | 
			
		||||
 | 
			
		||||
    assigned_check = CheckSerializer(read_only=True)
 | 
			
		||||
    check_name = serializers.ReadOnlyField(source="assigned_check.readable_desc")
 | 
			
		||||
    schedule = serializers.ReadOnlyField()
 | 
			
		||||
    last_run = serializers.ReadOnlyField(source="last_run_as_timezone")
 | 
			
		||||
    alert_template = serializers.SerializerMethodField()
 | 
			
		||||
@@ -37,19 +37,6 @@ class TaskSerializer(serializers.ModelSerializer):
 | 
			
		||||
        fields = "__all__"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AutoTaskSerializer(serializers.ModelSerializer):
 | 
			
		||||
 | 
			
		||||
    autotasks = TaskSerializer(many=True, read_only=True)
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Agent
 | 
			
		||||
        fields = (
 | 
			
		||||
            "pk",
 | 
			
		||||
            "hostname",
 | 
			
		||||
            "autotasks",
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# below is for the windows agent
 | 
			
		||||
class TaskRunnerScriptField(serializers.ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
@@ -68,6 +55,12 @@ class TaskRunnerGetSerializer(serializers.ModelSerializer):
 | 
			
		||||
 | 
			
		||||
class TaskGOGetSerializer(serializers.ModelSerializer):
 | 
			
		||||
    script = ScriptCheckSerializer(read_only=True)
 | 
			
		||||
    script_args = serializers.SerializerMethodField()
 | 
			
		||||
 | 
			
		||||
    def get_script_args(self, obj):
 | 
			
		||||
        return Script.parse_script_args(
 | 
			
		||||
            agent=obj.agent, shell=obj.script.shell, args=obj.script_args
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = AutomatedTask
 | 
			
		||||
@@ -78,3 +71,9 @@ class TaskRunnerPatchSerializer(serializers.ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = AutomatedTask
 | 
			
		||||
        fields = "__all__"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TaskAuditSerializer(serializers.ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = AutomatedTask
 | 
			
		||||
        fields = "__all__"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,17 +1,15 @@
 | 
			
		||||
import asyncio
 | 
			
		||||
import datetime as dt
 | 
			
		||||
from logging import log
 | 
			
		||||
import random
 | 
			
		||||
from time import sleep
 | 
			
		||||
from typing import Union
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from loguru import logger
 | 
			
		||||
from tacticalrmm.celery import app
 | 
			
		||||
 | 
			
		||||
from autotasks.models import AutomatedTask
 | 
			
		||||
 | 
			
		||||
logger.configure(**settings.LOG_CONFIG)
 | 
			
		||||
from logs.models import DebugLog
 | 
			
		||||
from tacticalrmm.celery import app
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
@@ -53,12 +51,20 @@ def remove_orphaned_win_tasks(agentpk):
 | 
			
		||||
 | 
			
		||||
    agent = Agent.objects.get(pk=agentpk)
 | 
			
		||||
 | 
			
		||||
    logger.info(f"Orphaned task cleanup initiated on {agent.hostname}.")
 | 
			
		||||
    DebugLog.info(
 | 
			
		||||
        agent=agent,
 | 
			
		||||
        log_type="agent_issues",
 | 
			
		||||
        message=f"Orphaned task cleanup initiated on {agent.hostname}.",
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    r = asyncio.run(agent.nats_cmd({"func": "listschedtasks"}, timeout=10))
 | 
			
		||||
 | 
			
		||||
    if not isinstance(r, list) and not r:  # empty list
 | 
			
		||||
        logger.error(f"Unable to clean up scheduled tasks on {agent.hostname}: {r}")
 | 
			
		||||
        DebugLog.error(
 | 
			
		||||
            agent=agent,
 | 
			
		||||
            log_type="agent_issues",
 | 
			
		||||
            message=f"Unable to clean up scheduled tasks on {agent.hostname}: {r}",
 | 
			
		||||
        )
 | 
			
		||||
        return "notlist"
 | 
			
		||||
 | 
			
		||||
    agent_task_names = list(agent.autotasks.values_list("win_task_name", flat=True))
 | 
			
		||||
@@ -83,13 +89,23 @@ def remove_orphaned_win_tasks(agentpk):
 | 
			
		||||
            }
 | 
			
		||||
            ret = asyncio.run(agent.nats_cmd(nats_data, timeout=10))
 | 
			
		||||
            if ret != "ok":
 | 
			
		||||
                logger.error(
 | 
			
		||||
                    f"Unable to clean up orphaned task {task} on {agent.hostname}: {ret}"
 | 
			
		||||
                DebugLog.error(
 | 
			
		||||
                    agent=agent,
 | 
			
		||||
                    log_type="agent_issues",
 | 
			
		||||
                    message=f"Unable to clean up orphaned task {task} on {agent.hostname}: {ret}",
 | 
			
		||||
                )
 | 
			
		||||
            else:
 | 
			
		||||
                logger.info(f"Removed orphaned task {task} from {agent.hostname}")
 | 
			
		||||
                DebugLog.info(
 | 
			
		||||
                    agent=agent,
 | 
			
		||||
                    log_type="agent_issues",
 | 
			
		||||
                    message=f"Removed orphaned task {task} from {agent.hostname}",
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
    logger.info(f"Orphaned task cleanup finished on {agent.hostname}")
 | 
			
		||||
    DebugLog.info(
 | 
			
		||||
        agent=agent,
 | 
			
		||||
        log_type="agent_issues",
 | 
			
		||||
        message=f"Orphaned task cleanup finished on {agent.hostname}",
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
 
 | 
			
		||||
@@ -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,221 @@ 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)
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@ from django.urls import path
 | 
			
		||||
from . import views
 | 
			
		||||
 | 
			
		||||
urlpatterns = [
 | 
			
		||||
    path("<int:pk>/automatedtasks/", views.AutoTask.as_view()),
 | 
			
		||||
    path("automatedtasks/", views.AddAutoTask.as_view()),
 | 
			
		||||
    path("runwintask/<int:pk>/", views.run_task),
 | 
			
		||||
    path("", views.GetAddAutoTasks.as_view()),
 | 
			
		||||
    path("<int:pk>/", views.GetEditDeleteAutoTask.as_view()),
 | 
			
		||||
    path("<int:pk>/run/", views.RunAutoTask.as_view()),
 | 
			
		||||
]
 | 
			
		||||
 
 | 
			
		||||
@@ -1,51 +1,59 @@
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from checks.models import Check
 | 
			
		||||
from django.shortcuts import get_object_or_404
 | 
			
		||||
from rest_framework.decorators import api_view
 | 
			
		||||
from rest_framework.permissions import IsAuthenticated
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
from rest_framework.views import APIView
 | 
			
		||||
from scripts.models import Script
 | 
			
		||||
from tacticalrmm.utils import get_bit_days, get_default_timezone, notify_error
 | 
			
		||||
from rest_framework.exceptions import PermissionDenied
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from automation.models import Policy
 | 
			
		||||
from tacticalrmm.utils import get_bit_days
 | 
			
		||||
from tacticalrmm.permissions import _has_perm_on_agent
 | 
			
		||||
 | 
			
		||||
from .models import AutomatedTask
 | 
			
		||||
from .serializers import AutoTaskSerializer, TaskSerializer
 | 
			
		||||
from .permissions import AutoTaskPerms, RunAutoTaskPerms
 | 
			
		||||
from .serializers import TaskSerializer
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AddAutoTask(APIView):
 | 
			
		||||
class GetAddAutoTasks(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, AutoTaskPerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request, agent_id=None, policy=None):
 | 
			
		||||
 | 
			
		||||
        if agent_id:
 | 
			
		||||
            agent = get_object_or_404(Agent, agent_id=agent_id)
 | 
			
		||||
            tasks = AutomatedTask.objects.filter(agent=agent)
 | 
			
		||||
        elif policy:
 | 
			
		||||
            policy = get_object_or_404(Policy, id=policy)
 | 
			
		||||
            tasks = AutomatedTask.objects.filter(policy=policy)
 | 
			
		||||
        else:
 | 
			
		||||
            tasks = AutomatedTask.objects.filter_by_role(request.user)
 | 
			
		||||
        return Response(TaskSerializer(tasks, many=True).data)
 | 
			
		||||
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
        from automation.models import Policy
 | 
			
		||||
        from automation.tasks import generate_agent_autotasks_task
 | 
			
		||||
 | 
			
		||||
        from autotasks.tasks import create_win_task_schedule
 | 
			
		||||
 | 
			
		||||
        data = request.data
 | 
			
		||||
        script = get_object_or_404(Script, pk=data["autotask"]["script"])
 | 
			
		||||
        data = request.data.copy()
 | 
			
		||||
 | 
			
		||||
        # Determine if adding check to Policy or Agent
 | 
			
		||||
        if "policy" in data:
 | 
			
		||||
            policy = get_object_or_404(Policy, id=data["policy"])
 | 
			
		||||
            # Object used for filter and save
 | 
			
		||||
            parent = {"policy": policy}
 | 
			
		||||
        else:
 | 
			
		||||
            agent = get_object_or_404(Agent, pk=data["agent"])
 | 
			
		||||
            parent = {"agent": agent}
 | 
			
		||||
        # Determine if adding to an agent and replace agent_id with pk
 | 
			
		||||
        if "agent" in data.keys():
 | 
			
		||||
            agent = get_object_or_404(Agent, agent_id=data["agent"])
 | 
			
		||||
 | 
			
		||||
        check = None
 | 
			
		||||
        if data["autotask"]["assigned_check"]:
 | 
			
		||||
            check = get_object_or_404(Check, pk=data["autotask"]["assigned_check"])
 | 
			
		||||
            if not _has_perm_on_agent(request.user, agent.agent_id):
 | 
			
		||||
                raise PermissionDenied()
 | 
			
		||||
 | 
			
		||||
            data["agent"] = agent.pk
 | 
			
		||||
 | 
			
		||||
        bit_weekdays = None
 | 
			
		||||
        if data["autotask"]["run_time_days"]:
 | 
			
		||||
            bit_weekdays = get_bit_days(data["autotask"]["run_time_days"])
 | 
			
		||||
        if "run_time_days" in data.keys():
 | 
			
		||||
            if data["run_time_days"]:
 | 
			
		||||
                bit_weekdays = get_bit_days(data["run_time_days"])
 | 
			
		||||
            data.pop("run_time_days")
 | 
			
		||||
 | 
			
		||||
        del data["autotask"]["run_time_days"]
 | 
			
		||||
        serializer = TaskSerializer(data=data["autotask"], partial=True, context=parent)
 | 
			
		||||
        serializer = TaskSerializer(data=data)
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        task = serializer.save(
 | 
			
		||||
            **parent,
 | 
			
		||||
            script=script,
 | 
			
		||||
            win_task_name=AutomatedTask.generate_task_name(),
 | 
			
		||||
            assigned_check=check,
 | 
			
		||||
            run_time_bit_weekdays=bit_weekdays,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@@ -55,56 +63,35 @@ class AddAutoTask(APIView):
 | 
			
		||||
        elif task.policy:
 | 
			
		||||
            generate_agent_autotasks_task.delay(policy=task.policy.pk)
 | 
			
		||||
 | 
			
		||||
        return Response("Task will be created shortly!")
 | 
			
		||||
        return Response(
 | 
			
		||||
            "The task has been created. It will show up on the agent on next checkin"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AutoTask(APIView):
 | 
			
		||||
class GetEditDeleteAutoTask(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, AutoTaskPerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request, pk):
 | 
			
		||||
 | 
			
		||||
        agent = get_object_or_404(Agent, pk=pk)
 | 
			
		||||
        ctx = {
 | 
			
		||||
            "default_tz": get_default_timezone(),
 | 
			
		||||
            "agent_tz": agent.time_zone,
 | 
			
		||||
        }
 | 
			
		||||
        return Response(AutoTaskSerializer(agent, context=ctx).data)
 | 
			
		||||
        task = get_object_or_404(AutomatedTask, pk=pk)
 | 
			
		||||
 | 
			
		||||
        if task.agent and not _has_perm_on_agent(request.user, task.agent.agent_id):
 | 
			
		||||
            raise PermissionDenied()
 | 
			
		||||
 | 
			
		||||
        return Response(TaskSerializer(task).data)
 | 
			
		||||
 | 
			
		||||
    def put(self, request, pk):
 | 
			
		||||
        from automation.tasks import update_policy_autotasks_fields_task
 | 
			
		||||
 | 
			
		||||
        task = get_object_or_404(AutomatedTask, pk=pk)
 | 
			
		||||
 | 
			
		||||
        if task.agent and not _has_perm_on_agent(request.user, task.agent.agent_id):
 | 
			
		||||
            raise PermissionDenied()
 | 
			
		||||
 | 
			
		||||
        serializer = TaskSerializer(instance=task, data=request.data, partial=True)
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        serializer.save()
 | 
			
		||||
 | 
			
		||||
        if task.policy:
 | 
			
		||||
            update_policy_autotasks_fields_task.delay(task=task.pk)
 | 
			
		||||
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
    def patch(self, request, pk):
 | 
			
		||||
        from automation.tasks import update_policy_autotasks_fields_task
 | 
			
		||||
        from autotasks.tasks import enable_or_disable_win_task
 | 
			
		||||
 | 
			
		||||
        task = get_object_or_404(AutomatedTask, pk=pk)
 | 
			
		||||
 | 
			
		||||
        if "enableordisable" in request.data:
 | 
			
		||||
            action = request.data["enableordisable"]
 | 
			
		||||
            task.enabled = action
 | 
			
		||||
            task.save(update_fields=["enabled"])
 | 
			
		||||
            action = "enabled" if action else "disabled"
 | 
			
		||||
 | 
			
		||||
            if task.policy:
 | 
			
		||||
                update_policy_autotasks_fields_task.delay(
 | 
			
		||||
                    task=task.pk, update_agent=True
 | 
			
		||||
                )
 | 
			
		||||
            elif task.agent:
 | 
			
		||||
                enable_or_disable_win_task.delay(pk=task.pk)
 | 
			
		||||
 | 
			
		||||
            return Response(f"Task will be {action} shortly")
 | 
			
		||||
 | 
			
		||||
        else:
 | 
			
		||||
            return notify_error("The request was invalid")
 | 
			
		||||
        return Response("The task was updated")
 | 
			
		||||
 | 
			
		||||
    def delete(self, request, pk):
 | 
			
		||||
        from automation.tasks import delete_policy_autotasks_task
 | 
			
		||||
@@ -112,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:
 | 
			
		||||
@@ -121,10 +111,16 @@ class AutoTask(APIView):
 | 
			
		||||
        return Response(f"{task.name} will be deleted shortly")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view()
 | 
			
		||||
def run_task(request, pk):
 | 
			
		||||
    from autotasks.tasks import run_win_task
 | 
			
		||||
class RunAutoTask(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, RunAutoTaskPerms]
 | 
			
		||||
 | 
			
		||||
    task = get_object_or_404(AutomatedTask, pk=pk)
 | 
			
		||||
    run_win_task.delay(pk=pk)
 | 
			
		||||
    return Response(f"{task.name} will now be run on {task.agent.hostname}")
 | 
			
		||||
    def post(self, request, pk):
 | 
			
		||||
        from autotasks.tasks import run_win_task
 | 
			
		||||
 | 
			
		||||
        task = get_object_or_404(AutomatedTask, pk=pk)
 | 
			
		||||
 | 
			
		||||
        if task.agent and not _has_perm_on_agent(request.user, task.agent.agent_id):
 | 
			
		||||
            raise PermissionDenied()
 | 
			
		||||
 | 
			
		||||
        run_win_task.delay(pk=pk)
 | 
			
		||||
        return Response(f"{task.name} will now be run on {task.agent.hostname}")
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										22
									
								
								api/tacticalrmm/checks/migrations/0024_auto_20210606_1632.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								api/tacticalrmm/checks/migrations/0024_auto_20210606_1632.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
			
		||||
# Generated by Django 3.2.1 on 2021-06-06 16:32
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('checks', '0023_check_run_interval'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='checkhistory',
 | 
			
		||||
            name='check_history',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='checkhistory',
 | 
			
		||||
            name='check_id',
 | 
			
		||||
            field=models.PositiveIntegerField(default=0),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										23
									
								
								api/tacticalrmm/checks/migrations/0025_auto_20210917_1954.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								api/tacticalrmm/checks/migrations/0025_auto_20210917_1954.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
# Generated by Django 3.2.6 on 2021-09-17 19:54
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("checks", "0024_auto_20210606_1632"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="check",
 | 
			
		||||
            name="created_by",
 | 
			
		||||
            field=models.CharField(blank=True, max_length=255, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="check",
 | 
			
		||||
            name="modified_by",
 | 
			
		||||
            field=models.CharField(blank=True, max_length=255, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -1,10 +1,8 @@
 | 
			
		||||
import asyncio
 | 
			
		||||
import json
 | 
			
		||||
import os
 | 
			
		||||
import string
 | 
			
		||||
from statistics import mean
 | 
			
		||||
from typing import Any
 | 
			
		||||
from packaging import version as pyver
 | 
			
		||||
 | 
			
		||||
import pytz
 | 
			
		||||
from alerts.models import SEVERITY_CHOICES
 | 
			
		||||
@@ -14,11 +12,7 @@ from django.contrib.postgres.fields import ArrayField
 | 
			
		||||
from django.core.validators import MaxValueValidator, MinValueValidator
 | 
			
		||||
from django.db import models
 | 
			
		||||
from logs.models import BaseAuditModel
 | 
			
		||||
from loguru import logger
 | 
			
		||||
 | 
			
		||||
from .utils import bytes2human
 | 
			
		||||
 | 
			
		||||
logger.configure(**settings.LOG_CONFIG)
 | 
			
		||||
from tacticalrmm.models import PermissionQuerySet
 | 
			
		||||
 | 
			
		||||
CHECK_TYPE_CHOICES = [
 | 
			
		||||
    ("diskspace", "Disk Space Check"),
 | 
			
		||||
@@ -57,6 +51,7 @@ EVT_LOG_FAIL_WHEN_CHOICES = [
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Check(BaseAuditModel):
 | 
			
		||||
    objects = PermissionQuerySet.as_manager()
 | 
			
		||||
 | 
			
		||||
    # common fields
 | 
			
		||||
 | 
			
		||||
@@ -237,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",
 | 
			
		||||
@@ -316,9 +311,9 @@ class Check(BaseAuditModel):
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def add_check_history(self, value: int, more_info: Any = None) -> None:
 | 
			
		||||
        CheckHistory.objects.create(check_history=self, y=value, results=more_info)
 | 
			
		||||
        CheckHistory.objects.create(check_id=self.pk, y=value, results=more_info)
 | 
			
		||||
 | 
			
		||||
    def handle_checkv2(self, data):
 | 
			
		||||
    def handle_check(self, data):
 | 
			
		||||
        from alerts.models import Alert
 | 
			
		||||
 | 
			
		||||
        # cpuload or mem checks
 | 
			
		||||
@@ -349,9 +344,6 @@ class Check(BaseAuditModel):
 | 
			
		||||
        elif self.check_type == "diskspace":
 | 
			
		||||
            if data["exists"]:
 | 
			
		||||
                percent_used = round(data["percent_used"])
 | 
			
		||||
                total = bytes2human(data["total"])
 | 
			
		||||
                free = bytes2human(data["free"])
 | 
			
		||||
 | 
			
		||||
                if self.error_threshold and (100 - percent_used) < self.error_threshold:
 | 
			
		||||
                    self.status = "failing"
 | 
			
		||||
                    self.alert_severity = "error"
 | 
			
		||||
@@ -365,7 +357,7 @@ class Check(BaseAuditModel):
 | 
			
		||||
                else:
 | 
			
		||||
                    self.status = "passing"
 | 
			
		||||
 | 
			
		||||
                self.more_info = f"Total: {total}B, Free: {free}B"
 | 
			
		||||
                self.more_info = data["more_info"]
 | 
			
		||||
 | 
			
		||||
                # add check history
 | 
			
		||||
                self.add_check_history(100 - percent_used)
 | 
			
		||||
@@ -381,12 +373,7 @@ class Check(BaseAuditModel):
 | 
			
		||||
            self.stdout = data["stdout"]
 | 
			
		||||
            self.stderr = data["stderr"]
 | 
			
		||||
            self.retcode = data["retcode"]
 | 
			
		||||
            try:
 | 
			
		||||
                # python agent
 | 
			
		||||
                self.execution_time = "{:.4f}".format(data["stop"] - data["start"])
 | 
			
		||||
            except:
 | 
			
		||||
                # golang agent
 | 
			
		||||
                self.execution_time = "{:.4f}".format(data["runtime"])
 | 
			
		||||
            self.execution_time = "{:.4f}".format(data["runtime"])
 | 
			
		||||
 | 
			
		||||
            if data["retcode"] in self.info_return_codes:
 | 
			
		||||
                self.alert_severity = "info"
 | 
			
		||||
@@ -422,22 +409,8 @@ class Check(BaseAuditModel):
 | 
			
		||||
 | 
			
		||||
        # ping checks
 | 
			
		||||
        elif self.check_type == "ping":
 | 
			
		||||
            output = data["output"]
 | 
			
		||||
 | 
			
		||||
            if pyver.parse(self.agent.version) <= pyver.parse("1.5.2"):
 | 
			
		||||
                # DEPRECATED
 | 
			
		||||
                success = ["Reply", "bytes", "time", "TTL"]
 | 
			
		||||
                if data["has_stdout"]:
 | 
			
		||||
                    if all(x in output for x in success):
 | 
			
		||||
                        self.status = "passing"
 | 
			
		||||
                    else:
 | 
			
		||||
                        self.status = "failing"
 | 
			
		||||
                elif data["has_stderr"]:
 | 
			
		||||
                    self.status = "failing"
 | 
			
		||||
            else:
 | 
			
		||||
                self.status = data["status"]
 | 
			
		||||
 | 
			
		||||
            self.more_info = output
 | 
			
		||||
            self.status = data["status"]
 | 
			
		||||
            self.more_info = data["output"]
 | 
			
		||||
            self.save(update_fields=["more_info"])
 | 
			
		||||
 | 
			
		||||
            self.add_check_history(
 | 
			
		||||
@@ -446,41 +419,8 @@ class Check(BaseAuditModel):
 | 
			
		||||
 | 
			
		||||
        # windows service checks
 | 
			
		||||
        elif self.check_type == "winsvc":
 | 
			
		||||
            svc_stat = data["status"]
 | 
			
		||||
            self.more_info = f"Status {svc_stat.upper()}"
 | 
			
		||||
 | 
			
		||||
            if data["exists"]:
 | 
			
		||||
                if svc_stat == "running":
 | 
			
		||||
                    self.status = "passing"
 | 
			
		||||
                elif svc_stat == "start_pending" and self.pass_if_start_pending:
 | 
			
		||||
                    self.status = "passing"
 | 
			
		||||
                else:
 | 
			
		||||
                    if self.agent and self.restart_if_stopped:
 | 
			
		||||
                        nats_data = {
 | 
			
		||||
                            "func": "winsvcaction",
 | 
			
		||||
                            "payload": {"name": self.svc_name, "action": "start"},
 | 
			
		||||
                        }
 | 
			
		||||
                        r = asyncio.run(self.agent.nats_cmd(nats_data, timeout=32))
 | 
			
		||||
                        if r == "timeout" or r == "natsdown":
 | 
			
		||||
                            self.status = "failing"
 | 
			
		||||
                        elif not r["success"] and r["errormsg"]:
 | 
			
		||||
                            self.status = "failing"
 | 
			
		||||
                        elif r["success"]:
 | 
			
		||||
                            self.status = "passing"
 | 
			
		||||
                            self.more_info = f"Status RUNNING"
 | 
			
		||||
                        else:
 | 
			
		||||
                            self.status = "failing"
 | 
			
		||||
                    else:
 | 
			
		||||
                        self.status = "failing"
 | 
			
		||||
 | 
			
		||||
            else:
 | 
			
		||||
                if self.pass_if_svc_not_exist:
 | 
			
		||||
                    self.status = "passing"
 | 
			
		||||
                else:
 | 
			
		||||
                    self.status = "failing"
 | 
			
		||||
 | 
			
		||||
                self.more_info = f"Service {self.svc_name} does not exist"
 | 
			
		||||
 | 
			
		||||
            self.status = data["status"]
 | 
			
		||||
            self.more_info = data["more_info"]
 | 
			
		||||
            self.save(update_fields=["more_info"])
 | 
			
		||||
 | 
			
		||||
            self.add_check_history(
 | 
			
		||||
@@ -488,49 +428,7 @@ class Check(BaseAuditModel):
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        elif self.check_type == "eventlog":
 | 
			
		||||
            log = []
 | 
			
		||||
            is_wildcard = self.event_id_is_wildcard
 | 
			
		||||
            eventType = self.event_type
 | 
			
		||||
            eventID = self.event_id
 | 
			
		||||
            source = self.event_source
 | 
			
		||||
            message = self.event_message
 | 
			
		||||
            r = data["log"]
 | 
			
		||||
 | 
			
		||||
            for i in r:
 | 
			
		||||
                if i["eventType"] == eventType:
 | 
			
		||||
                    if not is_wildcard and not int(i["eventID"]) == eventID:
 | 
			
		||||
                        continue
 | 
			
		||||
 | 
			
		||||
                    if not source and not message:
 | 
			
		||||
                        if is_wildcard:
 | 
			
		||||
                            log.append(i)
 | 
			
		||||
                        elif int(i["eventID"]) == eventID:
 | 
			
		||||
                            log.append(i)
 | 
			
		||||
                        continue
 | 
			
		||||
 | 
			
		||||
                    if source and message:
 | 
			
		||||
                        if is_wildcard:
 | 
			
		||||
                            if source in i["source"] and message in i["message"]:
 | 
			
		||||
                                log.append(i)
 | 
			
		||||
 | 
			
		||||
                        elif int(i["eventID"]) == eventID:
 | 
			
		||||
                            if source in i["source"] and message in i["message"]:
 | 
			
		||||
                                log.append(i)
 | 
			
		||||
 | 
			
		||||
                        continue
 | 
			
		||||
 | 
			
		||||
                    if source and source in i["source"]:
 | 
			
		||||
                        if is_wildcard:
 | 
			
		||||
                            log.append(i)
 | 
			
		||||
                        elif int(i["eventID"]) == eventID:
 | 
			
		||||
                            log.append(i)
 | 
			
		||||
 | 
			
		||||
                    if message and message in i["message"]:
 | 
			
		||||
                        if is_wildcard:
 | 
			
		||||
                            log.append(i)
 | 
			
		||||
                        elif int(i["eventID"]) == eventID:
 | 
			
		||||
                            log.append(i)
 | 
			
		||||
 | 
			
		||||
            log = data["log"]
 | 
			
		||||
            if self.fail_when == "contains":
 | 
			
		||||
                if log and len(log) >= self.number_of_events_b4_alert:
 | 
			
		||||
                    self.status = "failing"
 | 
			
		||||
@@ -561,33 +459,23 @@ class Check(BaseAuditModel):
 | 
			
		||||
 | 
			
		||||
        elif self.status == "passing":
 | 
			
		||||
            self.fail_count = 0
 | 
			
		||||
            self.save(update_fields=["status", "fail_count", "alert_severity"])
 | 
			
		||||
            self.save()
 | 
			
		||||
            if Alert.objects.filter(assigned_check=self, resolved=False).exists():
 | 
			
		||||
                Alert.handle_alert_resolve(self)
 | 
			
		||||
 | 
			
		||||
        return self.status
 | 
			
		||||
 | 
			
		||||
    def handle_assigned_task(self) -> None:
 | 
			
		||||
        for task in self.assignedtask.all():  # type: ignore
 | 
			
		||||
            if task.enabled:
 | 
			
		||||
                task.run_win_task()
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def serialize(check):
 | 
			
		||||
        # serializes the check and returns json
 | 
			
		||||
        from .serializers import CheckSerializer
 | 
			
		||||
        from .serializers import CheckAuditSerializer
 | 
			
		||||
 | 
			
		||||
        return CheckSerializer(check).data
 | 
			
		||||
 | 
			
		||||
    # for policy diskchecks
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def all_disks():
 | 
			
		||||
        return [f"{i}:" for i in string.ascii_uppercase]
 | 
			
		||||
 | 
			
		||||
    # for policy service checks
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def load_default_services():
 | 
			
		||||
        with open(
 | 
			
		||||
            os.path.join(settings.BASE_DIR, "services/default_services.json")
 | 
			
		||||
        ) as f:
 | 
			
		||||
            default_services = json.load(f)
 | 
			
		||||
 | 
			
		||||
        return default_services
 | 
			
		||||
        return CheckAuditSerializer(check).data
 | 
			
		||||
 | 
			
		||||
    def create_policy_check(self, agent=None, policy=None):
 | 
			
		||||
 | 
			
		||||
@@ -603,6 +491,14 @@ class Check(BaseAuditModel):
 | 
			
		||||
            script=self.script,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        for task in self.assignedtask.all():  # type: ignore
 | 
			
		||||
            if policy or (
 | 
			
		||||
                agent and not agent.autotasks.filter(parent_task=task.pk).exists()
 | 
			
		||||
            ):
 | 
			
		||||
                task.create_policy_task(
 | 
			
		||||
                    agent=agent, policy=policy, assigned_check=check
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
        for field in self.policy_fields_to_copy:
 | 
			
		||||
            setattr(check, field, getattr(self, field))
 | 
			
		||||
 | 
			
		||||
@@ -775,14 +671,12 @@ class Check(BaseAuditModel):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CheckHistory(models.Model):
 | 
			
		||||
    check_history = models.ForeignKey(
 | 
			
		||||
        Check,
 | 
			
		||||
        related_name="check_history",
 | 
			
		||||
        on_delete=models.CASCADE,
 | 
			
		||||
    )
 | 
			
		||||
    objects = PermissionQuerySet.as_manager()
 | 
			
		||||
 | 
			
		||||
    check_id = models.PositiveIntegerField(default=0)
 | 
			
		||||
    x = models.DateTimeField(auto_now_add=True)
 | 
			
		||||
    y = models.PositiveIntegerField(null=True, blank=True, default=None)
 | 
			
		||||
    results = models.JSONField(null=True, blank=True)
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return self.check_history.readable_desc
 | 
			
		||||
        return str(self.x)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										23
									
								
								api/tacticalrmm/checks/permissions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								api/tacticalrmm/checks/permissions.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
from rest_framework import permissions
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.permissions import _has_perm, _has_perm_on_agent
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ChecksPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        if r.method == "GET" or r.method == "PATCH":
 | 
			
		||||
            if "agent_id" in view.kwargs.keys():
 | 
			
		||||
                return _has_perm(r, "can_list_checks") and _has_perm_on_agent(
 | 
			
		||||
                    r.user, view.kwargs["agent_id"]
 | 
			
		||||
                )
 | 
			
		||||
            else:
 | 
			
		||||
                return _has_perm(r, "can_list_checks")
 | 
			
		||||
        else:
 | 
			
		||||
            return _has_perm(r, "can_manage_checks")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RunChecksPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        return _has_perm(r, "can_run_checks") and _has_perm_on_agent(
 | 
			
		||||
            r.user, view.kwargs["agent_id"]
 | 
			
		||||
        )
 | 
			
		||||
@@ -3,9 +3,10 @@ import validators as _v
 | 
			
		||||
from rest_framework import serializers
 | 
			
		||||
 | 
			
		||||
from autotasks.models import AutomatedTask
 | 
			
		||||
from scripts.serializers import ScriptCheckSerializer, ScriptSerializer
 | 
			
		||||
from scripts.serializers import ScriptCheckSerializer
 | 
			
		||||
 | 
			
		||||
from .models import Check, CheckHistory
 | 
			
		||||
from scripts.models import Script
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AssignedTaskField(serializers.ModelSerializer):
 | 
			
		||||
@@ -17,7 +18,6 @@ class AssignedTaskField(serializers.ModelSerializer):
 | 
			
		||||
class CheckSerializer(serializers.ModelSerializer):
 | 
			
		||||
 | 
			
		||||
    readable_desc = serializers.ReadOnlyField()
 | 
			
		||||
    script = ScriptSerializer(read_only=True)
 | 
			
		||||
    assigned_task = serializers.SerializerMethodField()
 | 
			
		||||
    last_run = serializers.ReadOnlyField(source="last_run_as_timezone")
 | 
			
		||||
    history_info = serializers.ReadOnlyField()
 | 
			
		||||
@@ -56,6 +56,11 @@ class CheckSerializer(serializers.ModelSerializer):
 | 
			
		||||
    def validate(self, val):
 | 
			
		||||
        try:
 | 
			
		||||
            check_type = val["check_type"]
 | 
			
		||||
            filter = (
 | 
			
		||||
                {"agent": val["agent"]}
 | 
			
		||||
                if "agent" in val.keys()
 | 
			
		||||
                else {"policy": val["policy"]}
 | 
			
		||||
            )
 | 
			
		||||
        except KeyError:
 | 
			
		||||
            return val
 | 
			
		||||
 | 
			
		||||
@@ -64,7 +69,7 @@ class CheckSerializer(serializers.ModelSerializer):
 | 
			
		||||
        if check_type == "diskspace":
 | 
			
		||||
            if not self.instance:  # only on create
 | 
			
		||||
                checks = (
 | 
			
		||||
                    Check.objects.filter(**self.context)
 | 
			
		||||
                    Check.objects.filter(**filter)
 | 
			
		||||
                    .filter(check_type="diskspace")
 | 
			
		||||
                    .exclude(managed_by_policy=True)
 | 
			
		||||
                )
 | 
			
		||||
@@ -101,7 +106,7 @@ class CheckSerializer(serializers.ModelSerializer):
 | 
			
		||||
 | 
			
		||||
        if check_type == "cpuload" and not self.instance:
 | 
			
		||||
            if (
 | 
			
		||||
                Check.objects.filter(**self.context, check_type="cpuload")
 | 
			
		||||
                Check.objects.filter(**filter, check_type="cpuload")
 | 
			
		||||
                .exclude(managed_by_policy=True)
 | 
			
		||||
                .exists()
 | 
			
		||||
            ):
 | 
			
		||||
@@ -125,7 +130,7 @@ class CheckSerializer(serializers.ModelSerializer):
 | 
			
		||||
 | 
			
		||||
        if check_type == "memory" and not self.instance:
 | 
			
		||||
            if (
 | 
			
		||||
                Check.objects.filter(**self.context, check_type="memory")
 | 
			
		||||
                Check.objects.filter(**filter, check_type="memory")
 | 
			
		||||
                .exclude(managed_by_policy=True)
 | 
			
		||||
                .exists()
 | 
			
		||||
            ):
 | 
			
		||||
@@ -158,13 +163,16 @@ class AssignedTaskCheckRunnerField(serializers.ModelSerializer):
 | 
			
		||||
 | 
			
		||||
class CheckRunnerGetSerializer(serializers.ModelSerializer):
 | 
			
		||||
    # only send data needed for agent to run a check
 | 
			
		||||
    assigned_tasks = serializers.SerializerMethodField()
 | 
			
		||||
    script = ScriptCheckSerializer(read_only=True)
 | 
			
		||||
    script_args = serializers.SerializerMethodField()
 | 
			
		||||
 | 
			
		||||
    def get_assigned_tasks(self, obj):
 | 
			
		||||
        if obj.assignedtask.exists():
 | 
			
		||||
            tasks = obj.assignedtask.all()
 | 
			
		||||
            return AssignedTaskCheckRunnerField(tasks, many=True).data
 | 
			
		||||
    def get_script_args(self, obj):
 | 
			
		||||
        if obj.check_type != "script":
 | 
			
		||||
            return []
 | 
			
		||||
 | 
			
		||||
        return Script.parse_script_args(
 | 
			
		||||
            agent=obj.agent, shell=obj.script.shell, args=obj.script_args
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Check
 | 
			
		||||
@@ -193,6 +201,7 @@ class CheckRunnerGetSerializer(serializers.ModelSerializer):
 | 
			
		||||
            "modified_by",
 | 
			
		||||
            "modified_time",
 | 
			
		||||
            "history",
 | 
			
		||||
            "dashboard_alert",
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -215,3 +224,9 @@ class CheckHistorySerializer(serializers.ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = CheckHistory
 | 
			
		||||
        fields = ("x", "y", "results")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CheckAuditSerializer(serializers.ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Check
 | 
			
		||||
        fields = "__all__"
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -3,10 +3,9 @@ from django.urls import path
 | 
			
		||||
from . import views
 | 
			
		||||
 | 
			
		||||
urlpatterns = [
 | 
			
		||||
    path("checks/", views.AddCheck.as_view()),
 | 
			
		||||
    path("<int:pk>/check/", views.GetUpdateDeleteCheck.as_view()),
 | 
			
		||||
    path("<pk>/loadchecks/", views.load_checks),
 | 
			
		||||
    path("getalldisks/", views.get_disks_for_policies),
 | 
			
		||||
    path("runchecks/<pk>/", views.run_checks),
 | 
			
		||||
    path("history/<int:checkpk>/", views.CheckHistory.as_view()),
 | 
			
		||||
    path("", views.GetAddChecks.as_view()),
 | 
			
		||||
    path("<int:pk>/", views.GetUpdateDeleteCheck.as_view()),
 | 
			
		||||
    path("<int:pk>/reset/", views.ResetCheck.as_view()),
 | 
			
		||||
    path("<agent:agent_id>/run/", views.run_checks),
 | 
			
		||||
    path("<int:pk>/history/", views.GetCheckHistory.as_view()),
 | 
			
		||||
]
 | 
			
		||||
 
 | 
			
		||||
@@ -1,60 +1,65 @@
 | 
			
		||||
import asyncio
 | 
			
		||||
from datetime import datetime as dt
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from automation.models import Policy
 | 
			
		||||
from django.db.models import Q
 | 
			
		||||
from django.shortcuts import get_object_or_404
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from packaging import version as pyver
 | 
			
		||||
from rest_framework.decorators import api_view
 | 
			
		||||
from rest_framework.decorators import api_view, permission_classes
 | 
			
		||||
from rest_framework.permissions import IsAuthenticated
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
from rest_framework.views import APIView
 | 
			
		||||
from scripts.models import Script
 | 
			
		||||
from tacticalrmm.utils import notify_error
 | 
			
		||||
from rest_framework.exceptions import PermissionDenied
 | 
			
		||||
 | 
			
		||||
from .models import Check
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from automation.models import Policy
 | 
			
		||||
from tacticalrmm.utils import notify_error
 | 
			
		||||
from tacticalrmm.permissions import _has_perm_on_agent
 | 
			
		||||
 | 
			
		||||
from .models import Check, CheckHistory
 | 
			
		||||
from .permissions import ChecksPerms, RunChecksPerms
 | 
			
		||||
from .serializers import CheckHistorySerializer, CheckSerializer
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AddCheck(APIView):
 | 
			
		||||
class GetAddChecks(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, ChecksPerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request, agent_id=None, policy=None):
 | 
			
		||||
        if agent_id:
 | 
			
		||||
            agent = get_object_or_404(Agent, agent_id=agent_id)
 | 
			
		||||
            checks = Check.objects.filter(agent=agent)
 | 
			
		||||
        elif policy:
 | 
			
		||||
            policy = get_object_or_404(Policy, id=policy)
 | 
			
		||||
            checks = Check.objects.filter(policy=policy)
 | 
			
		||||
        else:
 | 
			
		||||
            checks = Check.objects.filter_by_role(request.user)
 | 
			
		||||
        return Response(CheckSerializer(checks, many=True).data)
 | 
			
		||||
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
        from automation.tasks import generate_agent_checks_task
 | 
			
		||||
 | 
			
		||||
        policy = None
 | 
			
		||||
        agent = None
 | 
			
		||||
        data = request.data.copy()
 | 
			
		||||
        # Determine if adding check to Agent and replace agent_id with pk
 | 
			
		||||
        if "agent" in data.keys():
 | 
			
		||||
            agent = get_object_or_404(Agent, agent_id=data["agent"])
 | 
			
		||||
            if not _has_perm_on_agent(request.user, agent.agent_id):
 | 
			
		||||
                raise PermissionDenied()
 | 
			
		||||
 | 
			
		||||
        # Determine if adding check to Policy or Agent
 | 
			
		||||
        if "policy" in request.data:
 | 
			
		||||
            policy = get_object_or_404(Policy, id=request.data["policy"])
 | 
			
		||||
            # Object used for filter and save
 | 
			
		||||
            parent = {"policy": policy}
 | 
			
		||||
        else:
 | 
			
		||||
            agent = get_object_or_404(Agent, pk=request.data["pk"])
 | 
			
		||||
            parent = {"agent": agent}
 | 
			
		||||
 | 
			
		||||
        script = None
 | 
			
		||||
        if "script" in request.data["check"]:
 | 
			
		||||
            script = get_object_or_404(Script, pk=request.data["check"]["script"])
 | 
			
		||||
            data["agent"] = agent.pk
 | 
			
		||||
 | 
			
		||||
        # set event id to 0 if wildcard because it needs to be an integer field for db
 | 
			
		||||
        # will be ignored anyway by the agent when doing wildcard check
 | 
			
		||||
        if (
 | 
			
		||||
            request.data["check"]["check_type"] == "eventlog"
 | 
			
		||||
            and request.data["check"]["event_id_is_wildcard"]
 | 
			
		||||
        ):
 | 
			
		||||
            request.data["check"]["event_id"] = 0
 | 
			
		||||
        if data["check_type"] == "eventlog" and data["event_id_is_wildcard"]:
 | 
			
		||||
            data["event_id"] = 0
 | 
			
		||||
 | 
			
		||||
        serializer = CheckSerializer(
 | 
			
		||||
            data=request.data["check"], partial=True, context=parent
 | 
			
		||||
        )
 | 
			
		||||
        serializer = CheckSerializer(data=data, partial=True)
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        new_check = serializer.save(**parent, script=script)
 | 
			
		||||
        new_check = serializer.save()
 | 
			
		||||
 | 
			
		||||
        # Generate policy Checks
 | 
			
		||||
        if policy:
 | 
			
		||||
            generate_agent_checks_task.delay(policy=policy.pk)
 | 
			
		||||
        elif agent:
 | 
			
		||||
        if "policy" in data.keys():
 | 
			
		||||
            generate_agent_checks_task.delay(policy=data["policy"])
 | 
			
		||||
        elif "agent" in data.keys():
 | 
			
		||||
            checks = agent.agentchecks.filter(  # type: ignore
 | 
			
		||||
                check_type=new_check.check_type, managed_by_policy=True
 | 
			
		||||
            )
 | 
			
		||||
@@ -76,44 +81,43 @@ class AddCheck(APIView):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetUpdateDeleteCheck(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, ChecksPerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request, pk):
 | 
			
		||||
        check = get_object_or_404(Check, pk=pk)
 | 
			
		||||
        if check.agent and not _has_perm_on_agent(request.user, check.agent.agent_id):
 | 
			
		||||
            raise PermissionDenied()
 | 
			
		||||
 | 
			
		||||
        return Response(CheckSerializer(check).data)
 | 
			
		||||
 | 
			
		||||
    def patch(self, request, pk):
 | 
			
		||||
        from automation.tasks import (
 | 
			
		||||
            update_policy_check_fields_task,
 | 
			
		||||
        )
 | 
			
		||||
    def put(self, request, pk):
 | 
			
		||||
        from automation.tasks import update_policy_check_fields_task
 | 
			
		||||
 | 
			
		||||
        check = get_object_or_404(Check, pk=pk)
 | 
			
		||||
 | 
			
		||||
        data = request.data.copy()
 | 
			
		||||
 | 
			
		||||
        if check.agent and not _has_perm_on_agent(request.user, check.agent.agent_id):
 | 
			
		||||
            raise PermissionDenied()
 | 
			
		||||
 | 
			
		||||
        # remove fields that should not be changed when editing a check from the frontend
 | 
			
		||||
        if (
 | 
			
		||||
            "check_alert" not in request.data.keys()
 | 
			
		||||
            and "check_reset" not in request.data.keys()
 | 
			
		||||
        ):
 | 
			
		||||
            [request.data.pop(i) for i in check.non_editable_fields]
 | 
			
		||||
        [data.pop(i) for i in Check.non_editable_fields() if i in data.keys()]
 | 
			
		||||
 | 
			
		||||
        # set event id to 0 if wildcard because it needs to be an integer field for db
 | 
			
		||||
        # will be ignored anyway by the agent when doing wildcard check
 | 
			
		||||
        if check.check_type == "eventlog":
 | 
			
		||||
            try:
 | 
			
		||||
                request.data["event_id_is_wildcard"]
 | 
			
		||||
                data["event_id_is_wildcard"]
 | 
			
		||||
            except KeyError:
 | 
			
		||||
                pass
 | 
			
		||||
            else:
 | 
			
		||||
                if request.data["event_id_is_wildcard"]:
 | 
			
		||||
                    request.data["event_id"] = 0
 | 
			
		||||
                if data["event_id_is_wildcard"]:
 | 
			
		||||
                    data["event_id"] = 0
 | 
			
		||||
 | 
			
		||||
        serializer = CheckSerializer(instance=check, data=request.data, partial=True)
 | 
			
		||||
        serializer = CheckSerializer(instance=check, data=data, partial=True)
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        check = serializer.save()
 | 
			
		||||
 | 
			
		||||
        # resolve any alerts that are open
 | 
			
		||||
        if "check_reset" in request.data.keys():
 | 
			
		||||
            if check.alert.filter(resolved=False).exists():
 | 
			
		||||
                check.alert.get(resolved=False).resolve()
 | 
			
		||||
 | 
			
		||||
        if check.policy:
 | 
			
		||||
            update_policy_check_fields_task.delay(check=check.pk)
 | 
			
		||||
 | 
			
		||||
@@ -124,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
 | 
			
		||||
@@ -132,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 CheckHistory(APIView):
 | 
			
		||||
    def patch(self, request, checkpk):
 | 
			
		||||
        check = get_object_or_404(Check, pk=checkpk)
 | 
			
		||||
class ResetCheck(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, ChecksPerms]
 | 
			
		||||
 | 
			
		||||
    def post(self, request, pk):
 | 
			
		||||
        check = get_object_or_404(Check, pk=pk)
 | 
			
		||||
 | 
			
		||||
        if check.agent and not _has_perm_on_agent(request.user, check.agent.agent_id):
 | 
			
		||||
            raise PermissionDenied()
 | 
			
		||||
 | 
			
		||||
        check.status = "passing"
 | 
			
		||||
        check.save()
 | 
			
		||||
 | 
			
		||||
        # resolve any alerts that are open
 | 
			
		||||
        if check.alert.filter(resolved=False).exists():
 | 
			
		||||
            check.alert.get(resolved=False).resolve()
 | 
			
		||||
 | 
			
		||||
        return Response("The check status was reset")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetCheckHistory(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, ChecksPerms]
 | 
			
		||||
 | 
			
		||||
    def patch(self, request, pk):
 | 
			
		||||
        check = get_object_or_404(Check, pk=pk)
 | 
			
		||||
 | 
			
		||||
        if check.agent and not _has_perm_on_agent(request.user, check.agent.agent_id):
 | 
			
		||||
            raise PermissionDenied()
 | 
			
		||||
 | 
			
		||||
        timeFilter = Q()
 | 
			
		||||
 | 
			
		||||
@@ -155,7 +186,7 @@ class CheckHistory(APIView):
 | 
			
		||||
                    - djangotime.timedelta(days=request.data["timeFilter"]),
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
        check_history = check.check_history.filter(timeFilter).order_by("-x")  # type: ignore
 | 
			
		||||
        check_history = CheckHistory.objects.filter(check_id=pk).filter(timeFilter).order_by("-x")  # type: ignore
 | 
			
		||||
 | 
			
		||||
        return Response(
 | 
			
		||||
            CheckHistorySerializer(
 | 
			
		||||
@@ -165,8 +196,9 @@ class CheckHistory(APIView):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view()
 | 
			
		||||
def run_checks(request, pk):
 | 
			
		||||
    agent = get_object_or_404(Agent, pk=pk)
 | 
			
		||||
@permission_classes([IsAuthenticated, RunChecksPerms])
 | 
			
		||||
def run_checks(request, agent_id):
 | 
			
		||||
    agent = get_object_or_404(Agent, agent_id=agent_id)
 | 
			
		||||
 | 
			
		||||
    if pyver.parse(agent.version) >= pyver.parse("1.4.1"):
 | 
			
		||||
        r = asyncio.run(agent.nats_cmd({"func": "runchecks"}, timeout=15))
 | 
			
		||||
@@ -179,14 +211,3 @@ def run_checks(request, pk):
 | 
			
		||||
    else:
 | 
			
		||||
        asyncio.run(agent.nats_cmd({"func": "runchecks"}, wait=False))
 | 
			
		||||
        return Response(f"Checks will now be re-run on {agent.hostname}")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view()
 | 
			
		||||
def load_checks(request, pk):
 | 
			
		||||
    checks = Check.objects.filter(agent__pk=pk)
 | 
			
		||||
    return Response(CheckSerializer(checks, many=True).data)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view()
 | 
			
		||||
def get_disks_for_policies(request):
 | 
			
		||||
    return Response(Check.all_disks())
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,33 @@
 | 
			
		||||
# Generated by Django 3.2.6 on 2021-10-10 02:49
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('clients', '0017_auto_20210417_0125'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='client',
 | 
			
		||||
            name='created_by',
 | 
			
		||||
            field=models.CharField(blank=True, max_length=255, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='client',
 | 
			
		||||
            name='modified_by',
 | 
			
		||||
            field=models.CharField(blank=True, max_length=255, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='site',
 | 
			
		||||
            name='created_by',
 | 
			
		||||
            field=models.CharField(blank=True, max_length=255, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='site',
 | 
			
		||||
            name='modified_by',
 | 
			
		||||
            field=models.CharField(blank=True, max_length=255, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -0,0 +1,17 @@
 | 
			
		||||
# Generated by Django 3.2.6 on 2021-10-28 00:12
 | 
			
		||||
 | 
			
		||||
from django.db import migrations
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('clients', '0018_auto_20211010_0249'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='deployment',
 | 
			
		||||
            name='client',
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -5,9 +5,12 @@ from django.db import models
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from logs.models import BaseAuditModel
 | 
			
		||||
from tacticalrmm.models import PermissionQuerySet
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Client(BaseAuditModel):
 | 
			
		||||
    objects = PermissionQuerySet.as_manager()
 | 
			
		||||
 | 
			
		||||
    name = models.CharField(max_length=255, unique=True)
 | 
			
		||||
    block_policy_inheritance = models.BooleanField(default=False)
 | 
			
		||||
    workstation_policy = models.ForeignKey(
 | 
			
		||||
@@ -33,13 +36,17 @@ class Client(BaseAuditModel):
 | 
			
		||||
        blank=True,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def save(self, *args, **kw):
 | 
			
		||||
    def save(self, *args, **kwargs):
 | 
			
		||||
        from alerts.tasks import cache_agents_alert_template
 | 
			
		||||
        from automation.tasks import generate_agent_checks_task
 | 
			
		||||
 | 
			
		||||
        # get old client if exists
 | 
			
		||||
        old_client = type(self).objects.get(pk=self.pk) if self.pk else None
 | 
			
		||||
        super(BaseAuditModel, self).save(*args, **kw)
 | 
			
		||||
        old_client = Client.objects.get(pk=self.pk) if self.pk else None
 | 
			
		||||
        super(Client, self).save(
 | 
			
		||||
            old_model=old_client,
 | 
			
		||||
            *args,
 | 
			
		||||
            **kwargs,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # check if polcies have changed and initiate task to reapply policies if so
 | 
			
		||||
        if old_client:
 | 
			
		||||
@@ -50,7 +57,6 @@ class Client(BaseAuditModel):
 | 
			
		||||
                    old_client.block_policy_inheritance != self.block_policy_inheritance
 | 
			
		||||
                )
 | 
			
		||||
            ):
 | 
			
		||||
 | 
			
		||||
                generate_agent_checks_task.delay(
 | 
			
		||||
                    client=self.pk,
 | 
			
		||||
                    create_tasks=True,
 | 
			
		||||
@@ -87,12 +93,20 @@ class Client(BaseAuditModel):
 | 
			
		||||
                "offline_time",
 | 
			
		||||
            )
 | 
			
		||||
            .filter(site__client=self)
 | 
			
		||||
            .prefetch_related("agentchecks")
 | 
			
		||||
            .prefetch_related("agentchecks", "autotasks")
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        data = {"error": False, "warning": False}
 | 
			
		||||
 | 
			
		||||
        for agent in agents:
 | 
			
		||||
            if agent.maintenance_mode:
 | 
			
		||||
                break
 | 
			
		||||
 | 
			
		||||
            if agent.overdue_email_alert or agent.overdue_text_alert:
 | 
			
		||||
                if agent.status == "overdue":
 | 
			
		||||
                    data["error"] = True
 | 
			
		||||
                    break
 | 
			
		||||
 | 
			
		||||
            if agent.checks["has_failing_checks"]:
 | 
			
		||||
 | 
			
		||||
                if agent.checks["warning"]:
 | 
			
		||||
@@ -102,22 +116,25 @@ class Client(BaseAuditModel):
 | 
			
		||||
                    data["error"] = True
 | 
			
		||||
                    break
 | 
			
		||||
 | 
			
		||||
            if agent.overdue_email_alert or agent.overdue_text_alert:
 | 
			
		||||
                if agent.status == "overdue":
 | 
			
		||||
                    data["error"] = True
 | 
			
		||||
                    break
 | 
			
		||||
            if agent.autotasks.exists():  # type: ignore
 | 
			
		||||
                for i in agent.autotasks.all():  # type: ignore
 | 
			
		||||
                    if i.status == "failing" and i.alert_severity == "error":
 | 
			
		||||
                        data["error"] = True
 | 
			
		||||
                        break
 | 
			
		||||
 | 
			
		||||
        return data
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def serialize(client):
 | 
			
		||||
        # serializes the client and returns json
 | 
			
		||||
        from .serializers import ClientSerializer
 | 
			
		||||
        from .serializers import ClientAuditSerializer
 | 
			
		||||
 | 
			
		||||
        return ClientSerializer(client).data
 | 
			
		||||
        # serializes the client and returns json
 | 
			
		||||
        return ClientAuditSerializer(client).data
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Site(BaseAuditModel):
 | 
			
		||||
    objects = PermissionQuerySet.as_manager()
 | 
			
		||||
 | 
			
		||||
    client = models.ForeignKey(Client, related_name="sites", on_delete=models.CASCADE)
 | 
			
		||||
    name = models.CharField(max_length=255)
 | 
			
		||||
    block_policy_inheritance = models.BooleanField(default=False)
 | 
			
		||||
@@ -144,13 +161,17 @@ class Site(BaseAuditModel):
 | 
			
		||||
        blank=True,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def save(self, *args, **kw):
 | 
			
		||||
    def save(self, *args, **kwargs):
 | 
			
		||||
        from alerts.tasks import cache_agents_alert_template
 | 
			
		||||
        from automation.tasks import generate_agent_checks_task
 | 
			
		||||
 | 
			
		||||
        # get old client if exists
 | 
			
		||||
        old_site = type(self).objects.get(pk=self.pk) if self.pk else None
 | 
			
		||||
        super(Site, self).save(*args, **kw)
 | 
			
		||||
        old_site = Site.objects.get(pk=self.pk) if self.pk else None
 | 
			
		||||
        super(Site, self).save(
 | 
			
		||||
            old_model=old_site,
 | 
			
		||||
            *args,
 | 
			
		||||
            **kwargs,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # check if polcies have changed and initiate task to reapply policies if so
 | 
			
		||||
        if old_site:
 | 
			
		||||
@@ -159,11 +180,10 @@ class Site(BaseAuditModel):
 | 
			
		||||
                or (old_site.workstation_policy != self.workstation_policy)
 | 
			
		||||
                or (old_site.block_policy_inheritance != self.block_policy_inheritance)
 | 
			
		||||
            ):
 | 
			
		||||
 | 
			
		||||
                generate_agent_checks_task.delay(site=self.pk, create_tasks=True)
 | 
			
		||||
 | 
			
		||||
                if old_site.alert_template != self.alert_template:
 | 
			
		||||
                    cache_agents_alert_template.delay()
 | 
			
		||||
            if old_site.alert_template != self.alert_template:
 | 
			
		||||
                cache_agents_alert_template.delay()
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        ordering = ("name",)
 | 
			
		||||
@@ -192,12 +212,19 @@ class Site(BaseAuditModel):
 | 
			
		||||
                "offline_time",
 | 
			
		||||
            )
 | 
			
		||||
            .filter(site=self)
 | 
			
		||||
            .prefetch_related("agentchecks")
 | 
			
		||||
            .prefetch_related("agentchecks", "autotasks")
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        data = {"error": False, "warning": False}
 | 
			
		||||
 | 
			
		||||
        for agent in agents:
 | 
			
		||||
            if agent.maintenance_mode:
 | 
			
		||||
                break
 | 
			
		||||
 | 
			
		||||
            if agent.overdue_email_alert or agent.overdue_text_alert:
 | 
			
		||||
                if agent.status == "overdue":
 | 
			
		||||
                    data["error"] = True
 | 
			
		||||
                    break
 | 
			
		||||
 | 
			
		||||
            if agent.checks["has_failing_checks"]:
 | 
			
		||||
                if agent.checks["warning"]:
 | 
			
		||||
@@ -207,19 +234,20 @@ class Site(BaseAuditModel):
 | 
			
		||||
                    data["error"] = True
 | 
			
		||||
                    break
 | 
			
		||||
 | 
			
		||||
            if agent.overdue_email_alert or agent.overdue_text_alert:
 | 
			
		||||
                if agent.status == "overdue":
 | 
			
		||||
                    data["error"] = True
 | 
			
		||||
                    break
 | 
			
		||||
            if agent.autotasks.exists():  # type: ignore
 | 
			
		||||
                for i in agent.autotasks.all():  # type: ignore
 | 
			
		||||
                    if i.status == "failing" and i.alert_severity == "error":
 | 
			
		||||
                        data["error"] = True
 | 
			
		||||
                        break
 | 
			
		||||
 | 
			
		||||
        return data
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def serialize(site):
 | 
			
		||||
        # serializes the site and returns json
 | 
			
		||||
        from .serializers import SiteSerializer
 | 
			
		||||
        from .serializers import SiteAuditSerializer
 | 
			
		||||
 | 
			
		||||
        return SiteSerializer(site).data
 | 
			
		||||
        # serializes the site and returns json
 | 
			
		||||
        return SiteAuditSerializer(site).data
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
MON_TYPE_CHOICES = [
 | 
			
		||||
@@ -234,10 +262,9 @@ ARCH_CHOICES = [
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Deployment(models.Model):
 | 
			
		||||
    objects = PermissionQuerySet.as_manager()
 | 
			
		||||
 | 
			
		||||
    uid = models.UUIDField(primary_key=False, default=uuid.uuid4, editable=False)
 | 
			
		||||
    client = models.ForeignKey(
 | 
			
		||||
        "clients.Client", related_name="deployclients", on_delete=models.CASCADE
 | 
			
		||||
    )
 | 
			
		||||
    site = models.ForeignKey(
 | 
			
		||||
        "clients.Site", related_name="deploysites", on_delete=models.CASCADE
 | 
			
		||||
    )
 | 
			
		||||
@@ -256,6 +283,10 @@ class Deployment(models.Model):
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return f"{self.client} - {self.site} - {self.mon_type}"
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def client(self):
 | 
			
		||||
        return self.site.client
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ClientCustomField(models.Model):
 | 
			
		||||
    client = models.ForeignKey(
 | 
			
		||||
@@ -291,6 +322,22 @@ class ClientCustomField(models.Model):
 | 
			
		||||
        else:
 | 
			
		||||
            return self.string_value
 | 
			
		||||
 | 
			
		||||
    def save_to_field(self, value):
 | 
			
		||||
        if self.field.type in [
 | 
			
		||||
            "text",
 | 
			
		||||
            "number",
 | 
			
		||||
            "single",
 | 
			
		||||
            "datetime",
 | 
			
		||||
        ]:
 | 
			
		||||
            self.string_value = value
 | 
			
		||||
            self.save()
 | 
			
		||||
        elif type == "multiple":
 | 
			
		||||
            self.multiple_value = value.split(",")
 | 
			
		||||
            self.save()
 | 
			
		||||
        elif type == "checkbox":
 | 
			
		||||
            self.bool_value = bool(value)
 | 
			
		||||
            self.save()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SiteCustomField(models.Model):
 | 
			
		||||
    site = models.ForeignKey(
 | 
			
		||||
@@ -325,3 +372,19 @@ class SiteCustomField(models.Model):
 | 
			
		||||
            return self.bool_value
 | 
			
		||||
        else:
 | 
			
		||||
            return self.string_value
 | 
			
		||||
 | 
			
		||||
    def save_to_field(self, value):
 | 
			
		||||
        if self.field.type in [
 | 
			
		||||
            "text",
 | 
			
		||||
            "number",
 | 
			
		||||
            "single",
 | 
			
		||||
            "datetime",
 | 
			
		||||
        ]:
 | 
			
		||||
            self.string_value = value
 | 
			
		||||
            self.save()
 | 
			
		||||
        elif type == "multiple":
 | 
			
		||||
            self.multiple_value = value.split(",")
 | 
			
		||||
            self.save()
 | 
			
		||||
        elif type == "checkbox":
 | 
			
		||||
            self.bool_value = bool(value)
 | 
			
		||||
            self.save()
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										45
									
								
								api/tacticalrmm/clients/permissions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								api/tacticalrmm/clients/permissions.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,45 @@
 | 
			
		||||
from rest_framework import permissions
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.permissions import _has_perm, _has_perm_on_client, _has_perm_on_site
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ClientsPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        if r.method == "GET":
 | 
			
		||||
            if "pk" in view.kwargs.keys():
 | 
			
		||||
                return _has_perm(r, "can_list_clients") and _has_perm_on_client(
 | 
			
		||||
                    r.user, view.kwargs["pk"]
 | 
			
		||||
                )
 | 
			
		||||
            else:
 | 
			
		||||
                return _has_perm(r, "can_list_clients")
 | 
			
		||||
        elif r.method == "PUT" or r.method == "DELETE":
 | 
			
		||||
            return _has_perm(r, "can_manage_clients") and _has_perm_on_client(
 | 
			
		||||
                r.user, view.kwargs["pk"]
 | 
			
		||||
            )
 | 
			
		||||
        else:
 | 
			
		||||
            return _has_perm(r, "can_manage_clients")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SitesPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        if r.method == "GET":
 | 
			
		||||
            if "pk" in view.kwargs.keys():
 | 
			
		||||
                return _has_perm(r, "can_list_sites") and _has_perm_on_site(
 | 
			
		||||
                    r.user, view.kwargs["pk"]
 | 
			
		||||
                )
 | 
			
		||||
            else:
 | 
			
		||||
                return _has_perm(r, "can_list_sites")
 | 
			
		||||
        elif r.method == "PUT" or r.method == "DELETE":
 | 
			
		||||
            return _has_perm(r, "can_manage_sites") and _has_perm_on_site(
 | 
			
		||||
                r.user, view.kwargs["pk"]
 | 
			
		||||
            )
 | 
			
		||||
        else:
 | 
			
		||||
            return _has_perm(r, "can_manage_sites")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DeploymentPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        if r.method == "GET":
 | 
			
		||||
            return _has_perm(r, "can_list_deployments")
 | 
			
		||||
        else:
 | 
			
		||||
            return _has_perm(r, "can_manage_deployments")
 | 
			
		||||
@@ -1,4 +1,9 @@
 | 
			
		||||
from rest_framework.serializers import ModelSerializer, ReadOnlyField, ValidationError
 | 
			
		||||
from rest_framework.serializers import (
 | 
			
		||||
    ModelSerializer,
 | 
			
		||||
    ReadOnlyField,
 | 
			
		||||
    ValidationError,
 | 
			
		||||
    SerializerMethodField,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
from .models import Client, ClientCustomField, Deployment, Site, SiteCustomField
 | 
			
		||||
 | 
			
		||||
@@ -26,6 +31,8 @@ class SiteSerializer(ModelSerializer):
 | 
			
		||||
    client_name = ReadOnlyField(source="client.name")
 | 
			
		||||
    custom_fields = SiteCustomFieldSerializer(many=True, read_only=True)
 | 
			
		||||
    agent_count = ReadOnlyField()
 | 
			
		||||
    maintenance_mode = ReadOnlyField(source="has_maintenanace_mode_agents")
 | 
			
		||||
    failing_checks = ReadOnlyField(source="has_failing_checks")
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Site
 | 
			
		||||
@@ -40,6 +47,8 @@ class SiteSerializer(ModelSerializer):
 | 
			
		||||
            "custom_fields",
 | 
			
		||||
            "agent_count",
 | 
			
		||||
            "block_policy_inheritance",
 | 
			
		||||
            "maintenance_mode",
 | 
			
		||||
            "failing_checks",
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def validate(self, val):
 | 
			
		||||
@@ -49,6 +58,20 @@ class SiteSerializer(ModelSerializer):
 | 
			
		||||
        return val
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SiteMinimumSerializer(ModelSerializer):
 | 
			
		||||
    client_name = ReadOnlyField(source="client.name")
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Site
 | 
			
		||||
        fields = "__all__"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ClientMinimumSerializer(ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Client
 | 
			
		||||
        fields = "__all__"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ClientCustomFieldSerializer(ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = ClientCustomField
 | 
			
		||||
@@ -69,9 +92,17 @@ class ClientCustomFieldSerializer(ModelSerializer):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ClientSerializer(ModelSerializer):
 | 
			
		||||
    sites = SiteSerializer(many=True, read_only=True)
 | 
			
		||||
    sites = SerializerMethodField()
 | 
			
		||||
    custom_fields = ClientCustomFieldSerializer(many=True, read_only=True)
 | 
			
		||||
    agent_count = ReadOnlyField()
 | 
			
		||||
    maintenance_mode = ReadOnlyField(source="has_maintenanace_mode_agents")
 | 
			
		||||
    failing_checks = ReadOnlyField(source="has_failing_checks")
 | 
			
		||||
 | 
			
		||||
    def get_sites(self, obj):
 | 
			
		||||
        return SiteSerializer(
 | 
			
		||||
            obj.sites.select_related("client").filter_by_role(self.context["user"]),
 | 
			
		||||
            many=True,
 | 
			
		||||
        ).data
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Client
 | 
			
		||||
@@ -85,6 +116,8 @@ class ClientSerializer(ModelSerializer):
 | 
			
		||||
            "sites",
 | 
			
		||||
            "custom_fields",
 | 
			
		||||
            "agent_count",
 | 
			
		||||
            "maintenance_mode",
 | 
			
		||||
            "failing_checks",
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def validate(self, val):
 | 
			
		||||
@@ -94,25 +127,6 @@ class ClientSerializer(ModelSerializer):
 | 
			
		||||
        return val
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SiteTreeSerializer(ModelSerializer):
 | 
			
		||||
    maintenance_mode = ReadOnlyField(source="has_maintenanace_mode_agents")
 | 
			
		||||
    failing_checks = ReadOnlyField(source="has_failing_checks")
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Site
 | 
			
		||||
        fields = "__all__"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ClientTreeSerializer(ModelSerializer):
 | 
			
		||||
    sites = SiteTreeSerializer(many=True, read_only=True)
 | 
			
		||||
    maintenance_mode = ReadOnlyField(source="has_maintenanace_mode_agents")
 | 
			
		||||
    failing_checks = ReadOnlyField(source="has_failing_checks")
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Client
 | 
			
		||||
        fields = "__all__"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DeploymentSerializer(ModelSerializer):
 | 
			
		||||
    client_id = ReadOnlyField(source="client.id")
 | 
			
		||||
    site_id = ReadOnlyField(source="site.id")
 | 
			
		||||
@@ -134,3 +148,15 @@ class DeploymentSerializer(ModelSerializer):
 | 
			
		||||
            "install_flags",
 | 
			
		||||
            "created",
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SiteAuditSerializer(ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Site
 | 
			
		||||
        fields = "__all__"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ClientAuditSerializer(ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Client
 | 
			
		||||
        fields = "__all__"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,19 +1,22 @@
 | 
			
		||||
import uuid
 | 
			
		||||
from unittest.mock import patch
 | 
			
		||||
from itertools import cycle
 | 
			
		||||
 | 
			
		||||
from model_bakery import baker
 | 
			
		||||
from rest_framework.serializers import ValidationError
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.test import TacticalTestCase
 | 
			
		||||
 | 
			
		||||
from .models import Client, ClientCustomField, Deployment, Site, SiteCustomField
 | 
			
		||||
from .serializers import (
 | 
			
		||||
    ClientSerializer,
 | 
			
		||||
    ClientTreeSerializer,
 | 
			
		||||
    DeploymentSerializer,
 | 
			
		||||
    SiteSerializer,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
base_url = "/clients"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestClientViews(TacticalTestCase):
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
@@ -25,16 +28,15 @@ class TestClientViews(TacticalTestCase):
 | 
			
		||||
        baker.make("clients.Client", _quantity=5)
 | 
			
		||||
        clients = Client.objects.all()
 | 
			
		||||
 | 
			
		||||
        url = "/clients/clients/"
 | 
			
		||||
        url = f"{base_url}/"
 | 
			
		||||
        r = self.client.get(url, format="json")
 | 
			
		||||
        serializer = ClientSerializer(clients, many=True)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(r.data, serializer.data)  # type: ignore
 | 
			
		||||
        self.assertEqual(len(r.data), 5)
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
    def test_add_client(self):
 | 
			
		||||
        url = "/clients/clients/"
 | 
			
		||||
        url = f"{base_url}/"
 | 
			
		||||
 | 
			
		||||
        # test successfull add client
 | 
			
		||||
        payload = {
 | 
			
		||||
@@ -115,11 +117,9 @@ class TestClientViews(TacticalTestCase):
 | 
			
		||||
        # setup data
 | 
			
		||||
        client = baker.make("clients.Client")
 | 
			
		||||
 | 
			
		||||
        url = f"/clients/{client.id}/client/"  # type: ignore
 | 
			
		||||
        url = f"{base_url}/{client.id}/"  # type: ignore
 | 
			
		||||
        r = self.client.get(url, format="json")
 | 
			
		||||
        serializer = ClientSerializer(client)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(r.data, serializer.data)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
@@ -128,12 +128,12 @@ class TestClientViews(TacticalTestCase):
 | 
			
		||||
        client = baker.make("clients.Client", name="OldClientName")
 | 
			
		||||
 | 
			
		||||
        # test invalid id
 | 
			
		||||
        r = self.client.put("/clients/500/client/", format="json")
 | 
			
		||||
        r = self.client.put(f"{base_url}/500/", format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 404)
 | 
			
		||||
 | 
			
		||||
        # test successfull edit client
 | 
			
		||||
        data = {"client": {"name": "NewClientName"}, "custom_fields": []}
 | 
			
		||||
        url = f"/clients/{client.id}/client/"  # type: ignore
 | 
			
		||||
        url = f"{base_url}/{client.id}/"  # type: ignore
 | 
			
		||||
        r = self.client.put(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertTrue(Client.objects.filter(name="NewClientName").exists())
 | 
			
		||||
@@ -141,7 +141,6 @@ class TestClientViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        # test edit client with | in name
 | 
			
		||||
        data = {"client": {"name": "NewClie|ntName"}, "custom_fields": []}
 | 
			
		||||
        url = f"/clients/{client.id}/client/"  # type: ignore
 | 
			
		||||
        r = self.client.put(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 400)
 | 
			
		||||
 | 
			
		||||
@@ -189,10 +188,10 @@ class TestClientViews(TacticalTestCase):
 | 
			
		||||
        agent = baker.make_recipe("agents.agent", site=site_to_move)
 | 
			
		||||
 | 
			
		||||
        # test invalid id
 | 
			
		||||
        r = self.client.delete("/clients/334/953/", format="json")
 | 
			
		||||
        r = self.client.delete(f"{base_url}/334/", format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 404)
 | 
			
		||||
 | 
			
		||||
        url = f"/clients/{client_to_delete.id}/{site_to_move.id}/"  # type: ignore
 | 
			
		||||
        url = f"/clients/{client_to_delete.id}/?site_to_move={site_to_move.id}"  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # test successful deletion
 | 
			
		||||
        r = self.client.delete(url, format="json")
 | 
			
		||||
@@ -208,7 +207,7 @@ class TestClientViews(TacticalTestCase):
 | 
			
		||||
        baker.make("clients.Site", _quantity=5)
 | 
			
		||||
        sites = Site.objects.all()
 | 
			
		||||
 | 
			
		||||
        url = "/clients/sites/"
 | 
			
		||||
        url = f"{base_url}/sites/"
 | 
			
		||||
        r = self.client.get(url, format="json")
 | 
			
		||||
        serializer = SiteSerializer(sites, many=True)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
@@ -221,7 +220,7 @@ class TestClientViews(TacticalTestCase):
 | 
			
		||||
        client = baker.make("clients.Client")
 | 
			
		||||
        site = baker.make("clients.Site", client=client)
 | 
			
		||||
 | 
			
		||||
        url = "/clients/sites/"
 | 
			
		||||
        url = f"{base_url}/sites/"
 | 
			
		||||
 | 
			
		||||
        # test success add
 | 
			
		||||
        payload = {
 | 
			
		||||
@@ -279,7 +278,7 @@ class TestClientViews(TacticalTestCase):
 | 
			
		||||
        # setup data
 | 
			
		||||
        site = baker.make("clients.Site")
 | 
			
		||||
 | 
			
		||||
        url = f"/clients/sites/{site.id}/"  # type: ignore
 | 
			
		||||
        url = f"{base_url}/sites/{site.id}/"  # type: ignore
 | 
			
		||||
        r = self.client.get(url, format="json")
 | 
			
		||||
        serializer = SiteSerializer(site)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
@@ -293,7 +292,7 @@ class TestClientViews(TacticalTestCase):
 | 
			
		||||
        site = baker.make("clients.Site", client=client)
 | 
			
		||||
 | 
			
		||||
        # test invalid id
 | 
			
		||||
        r = self.client.put("/clients/sites/688/", format="json")
 | 
			
		||||
        r = self.client.put(f"{base_url}/sites/688/", format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 404)
 | 
			
		||||
 | 
			
		||||
        data = {
 | 
			
		||||
@@ -301,7 +300,7 @@ class TestClientViews(TacticalTestCase):
 | 
			
		||||
            "custom_fields": [],
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        url = f"/clients/sites/{site.id}/"  # type: ignore
 | 
			
		||||
        url = f"{base_url}/sites/{site.id}/"  # type: ignore
 | 
			
		||||
        r = self.client.put(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertTrue(
 | 
			
		||||
@@ -358,10 +357,10 @@ class TestClientViews(TacticalTestCase):
 | 
			
		||||
        agent = baker.make_recipe("agents.agent", site=site_to_delete)
 | 
			
		||||
 | 
			
		||||
        # test invalid id
 | 
			
		||||
        r = self.client.delete("/clients/500/445/", format="json")
 | 
			
		||||
        r = self.client.delete("{base_url}/500/", format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 404)
 | 
			
		||||
 | 
			
		||||
        url = f"/clients/sites/{site_to_delete.id}/{site_to_move.id}/"  # type: ignore
 | 
			
		||||
        url = f"/clients/sites/{site_to_delete.id}/?move_to_site={site_to_move.id}"  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # test deleting with last site under client
 | 
			
		||||
        r = self.client.delete(url, format="json")
 | 
			
		||||
@@ -378,25 +377,11 @@ class TestClientViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("delete", url)
 | 
			
		||||
 | 
			
		||||
    def test_get_tree(self):
 | 
			
		||||
        # setup data
 | 
			
		||||
        baker.make("clients.Site", _quantity=10)
 | 
			
		||||
        clients = Client.objects.all()
 | 
			
		||||
 | 
			
		||||
        url = "/clients/tree/"
 | 
			
		||||
 | 
			
		||||
        r = self.client.get(url, format="json")
 | 
			
		||||
        serializer = ClientTreeSerializer(clients, many=True)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(r.data, serializer.data)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
    def test_get_deployments(self):
 | 
			
		||||
        # setup data
 | 
			
		||||
        deployments = baker.make("clients.Deployment", _quantity=5)
 | 
			
		||||
 | 
			
		||||
        url = "/clients/deployments/"
 | 
			
		||||
        url = f"{base_url}/deployments/"
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
        serializer = DeploymentSerializer(deployments, many=True)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
@@ -408,7 +393,7 @@ class TestClientViews(TacticalTestCase):
 | 
			
		||||
        # setup data
 | 
			
		||||
        site = baker.make("clients.Site")
 | 
			
		||||
 | 
			
		||||
        url = "/clients/deployments/"
 | 
			
		||||
        url = f"{base_url}/deployments/"
 | 
			
		||||
        payload = {
 | 
			
		||||
            "client": site.client.id,  # type: ignore
 | 
			
		||||
            "site": site.id,  # type: ignore
 | 
			
		||||
@@ -437,21 +422,19 @@ class TestClientViews(TacticalTestCase):
 | 
			
		||||
        # setup data
 | 
			
		||||
        deployment = baker.make("clients.Deployment")
 | 
			
		||||
 | 
			
		||||
        url = "/clients/deployments/"
 | 
			
		||||
 | 
			
		||||
        url = f"/clients/{deployment.id}/deployment/"  # type: ignore
 | 
			
		||||
        url = f"{base_url}/deployments/{deployment.id}/"  # type: ignore
 | 
			
		||||
        r = self.client.delete(url)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertFalse(Deployment.objects.filter(pk=deployment.id).exists())  # type: ignore
 | 
			
		||||
 | 
			
		||||
        url = "/clients/32348/deployment/"
 | 
			
		||||
        url = f"{base_url}/deployments/32348/"
 | 
			
		||||
        r = self.client.delete(url)
 | 
			
		||||
        self.assertEqual(r.status_code, 404)
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("delete", url)
 | 
			
		||||
 | 
			
		||||
    def test_generate_deployment(self):
 | 
			
		||||
        # TODO complete this
 | 
			
		||||
    @patch("tacticalrmm.utils.generate_winagent_exe", return_value=Response("ok"))
 | 
			
		||||
    def test_generate_deployment(self, post):
 | 
			
		||||
        url = "/clients/asdkj234kasdasjd-asdkj234-asdk34-sad/deploy/"
 | 
			
		||||
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
@@ -462,3 +445,429 @@ class TestClientViews(TacticalTestCase):
 | 
			
		||||
        url = f"/clients/{uid}/deploy/"
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
        self.assertEqual(r.status_code, 404)
 | 
			
		||||
 | 
			
		||||
        # test valid download
 | 
			
		||||
        deployment = baker.make(
 | 
			
		||||
            "clients.Deployment",
 | 
			
		||||
            install_flags={"rdp": True, "ping": False, "power": False},
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        url = f"/clients/{deployment.uid}/deploy/"
 | 
			
		||||
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestClientPermissions(TacticalTestCase):
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        self.client_setup()
 | 
			
		||||
        self.setup_coresettings()
 | 
			
		||||
 | 
			
		||||
    def test_get_clients_permissions(self):
 | 
			
		||||
        # create user with empty role
 | 
			
		||||
        user = self.create_user_with_roles([])
 | 
			
		||||
        self.client.force_authenticate(user=user)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        url = f"{base_url}/"
 | 
			
		||||
 | 
			
		||||
        clients = baker.make("clients.Client", _quantity=5)
 | 
			
		||||
 | 
			
		||||
        # test getting all clients
 | 
			
		||||
 | 
			
		||||
        # user with empty role should fail
 | 
			
		||||
        self.check_not_authorized("get", url)
 | 
			
		||||
 | 
			
		||||
        # add can_list_agents roles and should succeed
 | 
			
		||||
        user.role.can_list_clients = True
 | 
			
		||||
        user.role.save()
 | 
			
		||||
 | 
			
		||||
        # all agents should be returned
 | 
			
		||||
        response = self.check_authorized("get", url)
 | 
			
		||||
        self.assertEqual(len(response.data), 5)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # limit user to specific client. only 1 client should be returned
 | 
			
		||||
        user.role.can_view_clients.set([clients[3]])
 | 
			
		||||
        response = self.check_authorized("get", url)
 | 
			
		||||
        self.assertEqual(len(response.data), 1)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # 2 should be returned now
 | 
			
		||||
        user.role.can_view_clients.set([clients[0], clients[1]])
 | 
			
		||||
        response = self.check_authorized("get", url)
 | 
			
		||||
        self.assertEqual(len(response.data), 2)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # limit to a specific site. The site shouldn't be in client returned sites
 | 
			
		||||
        sites = baker.make("clients.Site", client=clients[4], _quantity=3)
 | 
			
		||||
        baker.make("clients.Site", client=clients[0], _quantity=4)
 | 
			
		||||
        baker.make("clients.Site", client=clients[1], _quantity=5)
 | 
			
		||||
 | 
			
		||||
        user.role.can_view_sites.set([sites[0]])
 | 
			
		||||
        response = self.check_authorized("get", url)
 | 
			
		||||
        self.assertEqual(len(response.data), 3)  # type: ignore
 | 
			
		||||
        for client in response.data:  # type: ignore
 | 
			
		||||
            if client["id"] == clients[0].id:
 | 
			
		||||
                self.assertEqual(len(client["sites"]), 4)
 | 
			
		||||
            elif client["id"] == clients[1].id:
 | 
			
		||||
                self.assertEqual(len(client["sites"]), 5)
 | 
			
		||||
            elif client["id"] == clients[4].id:
 | 
			
		||||
                self.assertEqual(len(client["sites"]), 1)
 | 
			
		||||
 | 
			
		||||
        # make sure superusers work
 | 
			
		||||
        self.check_authorized_superuser("get", url)
 | 
			
		||||
 | 
			
		||||
    @patch("clients.models.Client.save")
 | 
			
		||||
    @patch("clients.models.Client.delete")
 | 
			
		||||
    def test_add_clients_permissions(self, save, delete):
 | 
			
		||||
 | 
			
		||||
        data = {"client": {"name": "Client Name"}, "site": {"name": "Site Name"}}
 | 
			
		||||
 | 
			
		||||
        url = f"{base_url}/"
 | 
			
		||||
 | 
			
		||||
        # test superuser access
 | 
			
		||||
        self.check_authorized_superuser("post", url, data)
 | 
			
		||||
 | 
			
		||||
        user = self.create_user_with_roles([])
 | 
			
		||||
        self.client.force_authenticate(user=user)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # test user without role
 | 
			
		||||
        self.check_not_authorized("post", url, data)
 | 
			
		||||
 | 
			
		||||
        # add user to role and test
 | 
			
		||||
        user.role.can_manage_clients = True
 | 
			
		||||
        user.role.save()
 | 
			
		||||
 | 
			
		||||
        self.check_authorized("post", url, data)
 | 
			
		||||
 | 
			
		||||
    @patch("clients.models.Client.delete")
 | 
			
		||||
    def test_get_edit_delete_clients_permissions(self, delete):
 | 
			
		||||
        # create user with empty role
 | 
			
		||||
        user = self.create_user_with_roles([])
 | 
			
		||||
        self.client.force_authenticate(user=user)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        client = baker.make("clients.Client")
 | 
			
		||||
        unauthorized_client = baker.make("clients.Client")
 | 
			
		||||
 | 
			
		||||
        methods = ["get", "put", "delete"]
 | 
			
		||||
        url = f"{base_url}/{client.id}/"
 | 
			
		||||
 | 
			
		||||
        # test user with no roles
 | 
			
		||||
        for method in methods:
 | 
			
		||||
            self.check_not_authorized(method, url)
 | 
			
		||||
 | 
			
		||||
        # add correct roles for view edit and delete
 | 
			
		||||
        user.role.can_list_clients = True
 | 
			
		||||
        user.role.can_manage_clients = True
 | 
			
		||||
        user.role.save()
 | 
			
		||||
 | 
			
		||||
        for method in methods:
 | 
			
		||||
            self.check_authorized(method, url)
 | 
			
		||||
 | 
			
		||||
        # test limiting users to clients and sites
 | 
			
		||||
 | 
			
		||||
        # limit to client
 | 
			
		||||
        user.role.can_view_clients.set([client])
 | 
			
		||||
 | 
			
		||||
        for method in methods:
 | 
			
		||||
            self.check_not_authorized(method, f"{base_url}/{unauthorized_client.id}/")
 | 
			
		||||
            self.check_authorized(method, url)
 | 
			
		||||
 | 
			
		||||
        # make sure superusers work
 | 
			
		||||
        for method in methods:
 | 
			
		||||
            self.check_authorized_superuser(
 | 
			
		||||
                method, f"{base_url}/{unauthorized_client.id}/"
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
    def test_get_sites_permissions(self):
 | 
			
		||||
        # create user with empty role
 | 
			
		||||
        user = self.create_user_with_roles([])
 | 
			
		||||
        self.client.force_authenticate(user=user)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        url = f"{base_url}/sites/"
 | 
			
		||||
 | 
			
		||||
        clients = baker.make("clients.Client", _quantity=3)
 | 
			
		||||
        sites = baker.make("clients.Site", client=cycle(clients), _quantity=10)
 | 
			
		||||
 | 
			
		||||
        # test getting all sites
 | 
			
		||||
 | 
			
		||||
        # user with empty role should fail
 | 
			
		||||
        self.check_not_authorized("get", url)
 | 
			
		||||
 | 
			
		||||
        # add can_list_sites roles and should succeed
 | 
			
		||||
        user.role.can_list_sites = True
 | 
			
		||||
        user.role.save()
 | 
			
		||||
 | 
			
		||||
        # all sites should be returned
 | 
			
		||||
        response = self.check_authorized("get", url)
 | 
			
		||||
        self.assertEqual(len(response.data), 10)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # limit user to specific site. only 1 site should be returned
 | 
			
		||||
        user.role.can_view_sites.set([sites[3]])
 | 
			
		||||
        response = self.check_authorized("get", url)
 | 
			
		||||
        self.assertEqual(len(response.data), 1)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # 2 should be returned now
 | 
			
		||||
        user.role.can_view_sites.set([sites[0], sites[1]])
 | 
			
		||||
        response = self.check_authorized("get", url)
 | 
			
		||||
        self.assertEqual(len(response.data), 2)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # check if limiting user to client works
 | 
			
		||||
        user.role.can_view_sites.clear()
 | 
			
		||||
        user.role.can_view_clients.set([clients[0]])
 | 
			
		||||
        response = self.check_authorized("get", url)
 | 
			
		||||
        self.assertEqual(len(response.data), 4)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # add a site to see if the results still work
 | 
			
		||||
        user.role.can_view_sites.set([sites[1], sites[0]])
 | 
			
		||||
        response = self.check_authorized("get", url)
 | 
			
		||||
        self.assertEqual(len(response.data), 5)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # make sure superusers work
 | 
			
		||||
        self.check_authorized_superuser("get", url)
 | 
			
		||||
 | 
			
		||||
    @patch("clients.models.Site.save")
 | 
			
		||||
    @patch("clients.models.Site.delete")
 | 
			
		||||
    def test_add_sites_permissions(self, delete, save):
 | 
			
		||||
        client = baker.make("clients.Client")
 | 
			
		||||
        unauthorized_client = baker.make("clients.Client")
 | 
			
		||||
        data = {"client": client.id, "name": "Site Name"}
 | 
			
		||||
 | 
			
		||||
        url = f"{base_url}/sites/"
 | 
			
		||||
 | 
			
		||||
        # test superuser access
 | 
			
		||||
        self.check_authorized_superuser("post", url, data)
 | 
			
		||||
 | 
			
		||||
        user = self.create_user_with_roles([])
 | 
			
		||||
        self.client.force_authenticate(user=user)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # test user without role
 | 
			
		||||
        self.check_not_authorized("post", url, data)
 | 
			
		||||
 | 
			
		||||
        # add user to role and test
 | 
			
		||||
        user.role.can_manage_sites = True
 | 
			
		||||
        user.role.save()
 | 
			
		||||
 | 
			
		||||
        self.check_authorized("post", url, data)
 | 
			
		||||
 | 
			
		||||
        # limit to client and test
 | 
			
		||||
        user.role.can_view_clients.set([client])
 | 
			
		||||
        self.check_authorized("post", url, data)
 | 
			
		||||
 | 
			
		||||
        # test adding to unauthorized client
 | 
			
		||||
        data = {"client": unauthorized_client.id, "name": "Site Name"}
 | 
			
		||||
        self.check_not_authorized("post", url, data)
 | 
			
		||||
 | 
			
		||||
    @patch("clients.models.Site.delete")
 | 
			
		||||
    def test_get_edit_delete_sites_permissions(self, delete):
 | 
			
		||||
        # create user with empty role
 | 
			
		||||
        user = self.create_user_with_roles([])
 | 
			
		||||
        self.client.force_authenticate(user=user)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        site = baker.make("clients.Site")
 | 
			
		||||
        unauthorized_site = baker.make("clients.Site")
 | 
			
		||||
 | 
			
		||||
        methods = ["get", "put", "delete"]
 | 
			
		||||
        url = f"{base_url}/sites/{site.id}/"
 | 
			
		||||
 | 
			
		||||
        # test user with no roles
 | 
			
		||||
        for method in methods:
 | 
			
		||||
            self.check_not_authorized(method, url)
 | 
			
		||||
 | 
			
		||||
        # add correct roles for view edit and delete
 | 
			
		||||
        user.role.can_list_sites = True
 | 
			
		||||
        user.role.can_manage_sites = True
 | 
			
		||||
        user.role.save()
 | 
			
		||||
 | 
			
		||||
        for method in methods:
 | 
			
		||||
            self.check_authorized(method, url)
 | 
			
		||||
 | 
			
		||||
        # test limiting users to clients and sites
 | 
			
		||||
 | 
			
		||||
        # limit to site
 | 
			
		||||
        user.role.can_view_sites.set([site])
 | 
			
		||||
 | 
			
		||||
        for method in methods:
 | 
			
		||||
            self.check_not_authorized(method, f"{base_url}/{unauthorized_site.id}/")
 | 
			
		||||
            self.check_authorized(method, url)
 | 
			
		||||
 | 
			
		||||
        # test limit to only client
 | 
			
		||||
        user.role.can_view_sites.clear()
 | 
			
		||||
        user.role.can_view_clients.set([site.client])
 | 
			
		||||
 | 
			
		||||
        for method in methods:
 | 
			
		||||
            self.check_not_authorized(method, f"{base_url}/{unauthorized_site.id}/")
 | 
			
		||||
            self.check_authorized(method, url)
 | 
			
		||||
 | 
			
		||||
        # make sure superusers work
 | 
			
		||||
        for method in methods:
 | 
			
		||||
            self.check_authorized_superuser(
 | 
			
		||||
                method, f"{base_url}/{unauthorized_site.id}/"
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
    def test_get_pendingactions_permissions(self):
 | 
			
		||||
        url = f"{base_url}/deployments/"
 | 
			
		||||
 | 
			
		||||
        site = baker.make("clients.Site")
 | 
			
		||||
        other_site = baker.make("clients.Site")
 | 
			
		||||
        deployments = baker.make("clients.Deployment", site=site, _quantity=5)
 | 
			
		||||
        other_deployments = baker.make(
 | 
			
		||||
            "clients.Deployment", site=other_site, _quantity=7
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # test getting all deployments
 | 
			
		||||
        # make sure superusers work
 | 
			
		||||
        self.check_authorized_superuser("get", url)
 | 
			
		||||
 | 
			
		||||
        # create user with empty role
 | 
			
		||||
        user = self.create_user_with_roles([])
 | 
			
		||||
        self.client.force_authenticate(user=user)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # user with empty role should fail
 | 
			
		||||
        self.check_not_authorized("get", url)
 | 
			
		||||
 | 
			
		||||
        # add can_list_sites roles and should succeed
 | 
			
		||||
        user.role.can_list_deployments = True
 | 
			
		||||
        user.role.save()
 | 
			
		||||
 | 
			
		||||
        # all sites should be returned
 | 
			
		||||
        response = self.check_authorized("get", url)
 | 
			
		||||
        self.assertEqual(len(response.data), 12)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # limit user to specific site. only 1 site should be returned
 | 
			
		||||
        user.role.can_view_sites.set([site])
 | 
			
		||||
        response = self.check_authorized("get", url)
 | 
			
		||||
        self.assertEqual(len(response.data), 5)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # all should be returned now
 | 
			
		||||
        user.role.can_view_clients.set([other_site.client])
 | 
			
		||||
        response = self.check_authorized("get", url)
 | 
			
		||||
        self.assertEqual(len(response.data), 12)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # check if limiting user to client works
 | 
			
		||||
        user.role.can_view_sites.clear()
 | 
			
		||||
        user.role.can_view_clients.set([other_site.client])
 | 
			
		||||
        response = self.check_authorized("get", url)
 | 
			
		||||
        self.assertEqual(len(response.data), 7)  # type: ignore
 | 
			
		||||
 | 
			
		||||
    @patch("clients.models.Deployment.save")
 | 
			
		||||
    def test_add_deployments_permissions(self, save):
 | 
			
		||||
        site = baker.make("clients.Site")
 | 
			
		||||
        unauthorized_site = baker.make("clients.Site")
 | 
			
		||||
        data = {
 | 
			
		||||
            "site": site.id,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        # test adding to unauthorized client
 | 
			
		||||
        unauthorized_data = {
 | 
			
		||||
            "site": unauthorized_site.id,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        url = f"{base_url}/deployments/"
 | 
			
		||||
 | 
			
		||||
        # test superuser access
 | 
			
		||||
        self.check_authorized_superuser("post", url, data)
 | 
			
		||||
 | 
			
		||||
        user = self.create_user_with_roles([])
 | 
			
		||||
        self.client.force_authenticate(user=user)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # test user without role
 | 
			
		||||
        self.check_not_authorized("post", url, data)
 | 
			
		||||
 | 
			
		||||
        # add user to role and test
 | 
			
		||||
        user.role.can_manage_deployments = True
 | 
			
		||||
        user.role.save()
 | 
			
		||||
 | 
			
		||||
        self.check_authorized("post", url, data)
 | 
			
		||||
 | 
			
		||||
        # limit to client and test
 | 
			
		||||
        user.role.can_view_clients.set([site.client])
 | 
			
		||||
        self.check_authorized("post", url, data)
 | 
			
		||||
        self.check_not_authorized("post", url, unauthorized_data)
 | 
			
		||||
 | 
			
		||||
        # limit to site and test
 | 
			
		||||
        user.role.can_view_clients.clear()
 | 
			
		||||
        user.role.can_view_sites.set([site])
 | 
			
		||||
        self.check_authorized("post", url, data)
 | 
			
		||||
        self.check_not_authorized("post", url, unauthorized_data)
 | 
			
		||||
 | 
			
		||||
    @patch("clients.models.Deployment.delete")
 | 
			
		||||
    def test_delete_deployments_permissions(self, delete):
 | 
			
		||||
        site = baker.make("clients.Site")
 | 
			
		||||
        unauthorized_site = baker.make("clients.Site")
 | 
			
		||||
        deployment = baker.make("clients.Deployment", site=site)
 | 
			
		||||
        unauthorized_deployment = baker.make(
 | 
			
		||||
            "clients.Deployment", site=unauthorized_site
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        url = f"{base_url}/deployments/{deployment.id}/"
 | 
			
		||||
        unauthorized_url = f"{base_url}/deployments/{unauthorized_deployment.id}/"
 | 
			
		||||
 | 
			
		||||
        # make sure superusers work
 | 
			
		||||
        self.check_authorized_superuser("delete", url)
 | 
			
		||||
        self.check_authorized_superuser("delete", unauthorized_url)
 | 
			
		||||
 | 
			
		||||
        # create user with empty role
 | 
			
		||||
        user = self.create_user_with_roles([])
 | 
			
		||||
        self.client.force_authenticate(user=user)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # make sure user with empty role is unauthorized
 | 
			
		||||
        self.check_not_authorized("delete", url)
 | 
			
		||||
        self.check_not_authorized("delete", unauthorized_url)
 | 
			
		||||
 | 
			
		||||
        # add correct roles for view edit and delete
 | 
			
		||||
        user.role.can_manage_deployments = True
 | 
			
		||||
        user.role.save()
 | 
			
		||||
 | 
			
		||||
        self.check_authorized("delete", url)
 | 
			
		||||
        self.check_authorized("delete", unauthorized_url)
 | 
			
		||||
 | 
			
		||||
        # test limiting users to clients and sites
 | 
			
		||||
 | 
			
		||||
        # limit to site
 | 
			
		||||
        user.role.can_view_sites.set([site])
 | 
			
		||||
 | 
			
		||||
        # recreate deployment since it is being deleted even though I am mocking delete on Deployment model???
 | 
			
		||||
        unauthorized_deployment = baker.make(
 | 
			
		||||
            "clients.Deployment", site=unauthorized_site
 | 
			
		||||
        )
 | 
			
		||||
        unauthorized_url = f"{base_url}/deployments/{unauthorized_deployment.id}/"
 | 
			
		||||
 | 
			
		||||
        self.check_authorized("delete", url)
 | 
			
		||||
        self.check_not_authorized("delete", unauthorized_url)
 | 
			
		||||
 | 
			
		||||
        # test limit to only client
 | 
			
		||||
        user.role.can_view_sites.clear()
 | 
			
		||||
        user.role.can_view_clients.set([site.client])
 | 
			
		||||
 | 
			
		||||
        self.check_authorized("delete", url)
 | 
			
		||||
        self.check_not_authorized("delete", unauthorized_url)
 | 
			
		||||
 | 
			
		||||
    def test_restricted_user_creating_clients(self):
 | 
			
		||||
        from accounts.models import User
 | 
			
		||||
 | 
			
		||||
        # when a user that is limited to a specific subset of clients creates a client. It should allow access to that client
 | 
			
		||||
        client = baker.make("clients.Client")
 | 
			
		||||
        user = self.create_user_with_roles(["can_manage_clients"])
 | 
			
		||||
        self.client.force_authenticate(user=user)  # type: ignore
 | 
			
		||||
        user.role.can_view_clients.set([client])
 | 
			
		||||
 | 
			
		||||
        data = {"client": {"name": "New Client"}, "site": {"name": "New Site"}}
 | 
			
		||||
 | 
			
		||||
        self.client.post(f"{base_url}/", data, format="json")
 | 
			
		||||
 | 
			
		||||
        # make sure two clients are allowed now
 | 
			
		||||
        self.assertEqual(User.objects.get(id=user.id).role.can_view_clients.count(), 2)
 | 
			
		||||
 | 
			
		||||
    def test_restricted_user_creating_sites(self):
 | 
			
		||||
        from accounts.models import User
 | 
			
		||||
 | 
			
		||||
        # when a user that is limited to a specific subset of clients creates a client. It should allow access to that client
 | 
			
		||||
        site = baker.make("clients.Site")
 | 
			
		||||
        user = self.create_user_with_roles(["can_manage_sites"])
 | 
			
		||||
        self.client.force_authenticate(user=user)  # type: ignore
 | 
			
		||||
        user.role.can_view_sites.set([site])
 | 
			
		||||
 | 
			
		||||
        data = {"site": {"client": site.client.id, "name": "New Site"}}
 | 
			
		||||
 | 
			
		||||
        self.client.post(f"{base_url}/sites/", data, format="json")
 | 
			
		||||
 | 
			
		||||
        # make sure two sites are allowed now
 | 
			
		||||
        self.assertEqual(User.objects.get(id=user.id).role.can_view_sites.count(), 2)
 | 
			
		||||
 
 | 
			
		||||
@@ -3,14 +3,11 @@ from django.urls import path
 | 
			
		||||
from . import views
 | 
			
		||||
 | 
			
		||||
urlpatterns = [
 | 
			
		||||
    path("clients/", views.GetAddClients.as_view()),
 | 
			
		||||
    path("<int:pk>/client/", views.GetUpdateClient.as_view()),
 | 
			
		||||
    path("<int:pk>/<int:sitepk>/", views.DeleteClient.as_view()),
 | 
			
		||||
    path("tree/", views.GetClientTree.as_view()),
 | 
			
		||||
    path("", views.GetAddClients.as_view()),
 | 
			
		||||
    path("<int:pk>/", views.GetUpdateDeleteClient.as_view()),
 | 
			
		||||
    path("sites/", views.GetAddSites.as_view()),
 | 
			
		||||
    path("sites/<int:pk>/", views.GetUpdateSite.as_view()),
 | 
			
		||||
    path("sites/<int:pk>/<int:sitepk>/", views.DeleteSite.as_view()),
 | 
			
		||||
    path("sites/<int:pk>/", views.GetUpdateDeleteSite.as_view()),
 | 
			
		||||
    path("deployments/", views.AgentDeployment.as_view()),
 | 
			
		||||
    path("<int:pk>/deployment/", views.AgentDeployment.as_view()),
 | 
			
		||||
    path("deployments/<int:pk>/", views.AgentDeployment.as_view()),
 | 
			
		||||
    path("<str:uid>/deploy/", views.GenerateAgent.as_view()),
 | 
			
		||||
]
 | 
			
		||||
 
 | 
			
		||||
@@ -3,35 +3,43 @@ import re
 | 
			
		||||
import uuid
 | 
			
		||||
 | 
			
		||||
import pytz
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.shortcuts import get_object_or_404
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from loguru import logger
 | 
			
		||||
from rest_framework.permissions import AllowAny
 | 
			
		||||
from rest_framework.permissions import AllowAny, IsAuthenticated
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
from rest_framework.views import APIView
 | 
			
		||||
from rest_framework.exceptions import PermissionDenied
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from core.models import CoreSettings
 | 
			
		||||
from tacticalrmm.utils import notify_error
 | 
			
		||||
from tacticalrmm.permissions import _has_perm_on_client, _has_perm_on_site
 | 
			
		||||
 | 
			
		||||
from .models import Client, ClientCustomField, Deployment, Site, SiteCustomField
 | 
			
		||||
from .permissions import (
 | 
			
		||||
    ClientsPerms,
 | 
			
		||||
    DeploymentPerms,
 | 
			
		||||
    SitesPerms,
 | 
			
		||||
)
 | 
			
		||||
from .serializers import (
 | 
			
		||||
    ClientCustomFieldSerializer,
 | 
			
		||||
    ClientSerializer,
 | 
			
		||||
    ClientTreeSerializer,
 | 
			
		||||
    DeploymentSerializer,
 | 
			
		||||
    SiteCustomFieldSerializer,
 | 
			
		||||
    SiteSerializer,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
logger.configure(**settings.LOG_CONFIG)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetAddClients(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, ClientsPerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request):
 | 
			
		||||
        clients = Client.objects.all()
 | 
			
		||||
        return Response(ClientSerializer(clients, many=True).data)
 | 
			
		||||
        clients = Client.objects.select_related(
 | 
			
		||||
            "workstation_policy", "server_policy", "alert_template"
 | 
			
		||||
        ).filter_by_role(request.user)
 | 
			
		||||
        return Response(
 | 
			
		||||
            ClientSerializer(clients, context={"user": request.user}, many=True).data
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
        # create client
 | 
			
		||||
@@ -68,13 +76,19 @@ class GetAddClients(APIView):
 | 
			
		||||
                serializer.is_valid(raise_exception=True)
 | 
			
		||||
                serializer.save()
 | 
			
		||||
 | 
			
		||||
        return Response(f"{client} was added!")
 | 
			
		||||
        # add user to allowed clients in role if restricted user created the client
 | 
			
		||||
        if request.user.role and request.user.role.can_view_clients.exists():
 | 
			
		||||
            request.user.role.can_view_clients.add(client)
 | 
			
		||||
 | 
			
		||||
        return Response(f"{client.name} was added")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetUpdateClient(APIView):
 | 
			
		||||
class GetUpdateDeleteClient(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, ClientsPerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request, pk):
 | 
			
		||||
        client = get_object_or_404(Client, pk=pk)
 | 
			
		||||
        return Response(ClientSerializer(client).data)
 | 
			
		||||
        return Response(ClientSerializer(client, context={"user": request.user}).data)
 | 
			
		||||
 | 
			
		||||
    def put(self, request, pk):
 | 
			
		||||
        client = get_object_or_404(Client, pk=pk)
 | 
			
		||||
@@ -106,42 +120,41 @@ class GetUpdateClient(APIView):
 | 
			
		||||
                    serializer.is_valid(raise_exception=True)
 | 
			
		||||
                    serializer.save()
 | 
			
		||||
 | 
			
		||||
        return Response("The Client was updated")
 | 
			
		||||
        return Response("{client} was updated")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DeleteClient(APIView):
 | 
			
		||||
    def delete(self, request, pk, sitepk):
 | 
			
		||||
    def delete(self, request, pk):
 | 
			
		||||
        from automation.tasks import generate_agent_checks_task
 | 
			
		||||
 | 
			
		||||
        client = get_object_or_404(Client, pk=pk)
 | 
			
		||||
        agents = Agent.objects.filter(site__client=client)
 | 
			
		||||
 | 
			
		||||
        if not sitepk:
 | 
			
		||||
        # only run tasks if it affects clients
 | 
			
		||||
        if client.agent_count > 0 and "move_to_site" in request.query_params.keys():
 | 
			
		||||
            agents = Agent.objects.filter(site__client=client)
 | 
			
		||||
            site = get_object_or_404(Site, pk=request.query_params["move_to_site"])
 | 
			
		||||
            agents.update(site=site)
 | 
			
		||||
            generate_agent_checks_task.delay(all=True, create_tasks=True)
 | 
			
		||||
 | 
			
		||||
        elif client.agent_count > 0:
 | 
			
		||||
            return notify_error(
 | 
			
		||||
                "There needs to be a site specified to move existing agents to"
 | 
			
		||||
                "Agents exist under this client. There needs to be a site specified to move existing agents to"
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        site = get_object_or_404(Site, pk=sitepk)
 | 
			
		||||
        agents.update(site=site)
 | 
			
		||||
 | 
			
		||||
        generate_agent_checks_task.delay(all=True, create_tasks=True)
 | 
			
		||||
 | 
			
		||||
        client.delete()
 | 
			
		||||
        return Response(f"{client.name} was deleted!")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetClientTree(APIView):
 | 
			
		||||
    def get(self, request):
 | 
			
		||||
        clients = Client.objects.all()
 | 
			
		||||
        return Response(ClientTreeSerializer(clients, many=True).data)
 | 
			
		||||
        return Response(f"{client.name} was deleted")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetAddSites(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, SitesPerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request):
 | 
			
		||||
        sites = Site.objects.all()
 | 
			
		||||
        sites = Site.objects.filter_by_role(request.user)
 | 
			
		||||
        return Response(SiteSerializer(sites, many=True).data)
 | 
			
		||||
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
 | 
			
		||||
        if not _has_perm_on_client(request.user, request.data["site"]["client"]):
 | 
			
		||||
            raise PermissionDenied()
 | 
			
		||||
 | 
			
		||||
        serializer = SiteSerializer(data=request.data["site"])
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        site = serializer.save()
 | 
			
		||||
@@ -158,10 +171,16 @@ class GetAddSites(APIView):
 | 
			
		||||
                serializer.is_valid(raise_exception=True)
 | 
			
		||||
                serializer.save()
 | 
			
		||||
 | 
			
		||||
        # add user to allowed sites in role if restricted user created the client
 | 
			
		||||
        if request.user.role and request.user.role.can_view_sites.exists():
 | 
			
		||||
            request.user.role.can_view_sites.add(site)
 | 
			
		||||
 | 
			
		||||
        return Response(f"Site {site.name} was added!")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetUpdateSite(APIView):
 | 
			
		||||
class GetUpdateDeleteSite(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, SitesPerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request, pk):
 | 
			
		||||
        site = get_object_or_404(Site, pk=pk)
 | 
			
		||||
        return Response(SiteSerializer(site).data)
 | 
			
		||||
@@ -201,51 +220,55 @@ class GetUpdateSite(APIView):
 | 
			
		||||
                    serializer.is_valid(raise_exception=True)
 | 
			
		||||
                    serializer.save()
 | 
			
		||||
 | 
			
		||||
        return Response("Site was edited!")
 | 
			
		||||
        return Response("Site was edited")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DeleteSite(APIView):
 | 
			
		||||
    def delete(self, request, pk, sitepk):
 | 
			
		||||
    def delete(self, request, pk):
 | 
			
		||||
        from automation.tasks import generate_agent_checks_task
 | 
			
		||||
 | 
			
		||||
        site = get_object_or_404(Site, pk=pk)
 | 
			
		||||
        if site.client.sites.count() == 1:
 | 
			
		||||
            return notify_error("A client must have at least 1 site.")
 | 
			
		||||
 | 
			
		||||
        agents = Agent.objects.filter(site=site)
 | 
			
		||||
        # only run tasks if it affects clients
 | 
			
		||||
        if site.agent_count > 0 and "move_to_site" in request.query_params.keys():
 | 
			
		||||
            agents = Agent.objects.filter(site=site)
 | 
			
		||||
            new_site = get_object_or_404(Site, pk=request.query_params["move_to_site"])
 | 
			
		||||
            agents.update(site=new_site)
 | 
			
		||||
            generate_agent_checks_task.delay(all=True, create_tasks=True)
 | 
			
		||||
 | 
			
		||||
        if not sitepk:
 | 
			
		||||
        elif site.agent_count > 0:
 | 
			
		||||
            return notify_error(
 | 
			
		||||
                "There needs to be a site specified to move the agents to"
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        agent_site = get_object_or_404(Site, pk=sitepk)
 | 
			
		||||
 | 
			
		||||
        agents.update(site=agent_site)
 | 
			
		||||
 | 
			
		||||
        generate_agent_checks_task.delay(all=True, create_tasks=True)
 | 
			
		||||
 | 
			
		||||
        site.delete()
 | 
			
		||||
        return Response(f"{site.name} was deleted!")
 | 
			
		||||
        return Response(f"{site.name} was deleted")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AgentDeployment(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, DeploymentPerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request):
 | 
			
		||||
        deps = Deployment.objects.all()
 | 
			
		||||
        deps = Deployment.objects.filter_by_role(request.user)
 | 
			
		||||
        return Response(DeploymentSerializer(deps, many=True).data)
 | 
			
		||||
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
        from knox.models import AuthToken
 | 
			
		||||
        from accounts.models import User
 | 
			
		||||
 | 
			
		||||
        client = get_object_or_404(Client, pk=request.data["client"])
 | 
			
		||||
        site = get_object_or_404(Site, pk=request.data["site"])
 | 
			
		||||
 | 
			
		||||
        if not _has_perm_on_site(request.user, site.pk):
 | 
			
		||||
            raise PermissionDenied()
 | 
			
		||||
 | 
			
		||||
        installer_user = User.objects.filter(is_installer_user=True).first()
 | 
			
		||||
 | 
			
		||||
        expires = dt.datetime.strptime(
 | 
			
		||||
            request.data["expires"], "%Y-%m-%d %H:%M"
 | 
			
		||||
        ).astimezone(pytz.timezone("UTC"))
 | 
			
		||||
        now = djangotime.now()
 | 
			
		||||
        delta = expires - now
 | 
			
		||||
        obj, token = AuthToken.objects.create(user=request.user, expiry=delta)
 | 
			
		||||
        obj, token = AuthToken.objects.create(user=installer_user, expiry=delta)
 | 
			
		||||
 | 
			
		||||
        flags = {
 | 
			
		||||
            "power": request.data["power"],
 | 
			
		||||
@@ -254,7 +277,6 @@ class AgentDeployment(APIView):
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Deployment(
 | 
			
		||||
            client=client,
 | 
			
		||||
            site=site,
 | 
			
		||||
            expiry=expires,
 | 
			
		||||
            mon_type=request.data["agenttype"],
 | 
			
		||||
@@ -263,17 +285,21 @@ class AgentDeployment(APIView):
 | 
			
		||||
            token_key=token,
 | 
			
		||||
            install_flags=flags,
 | 
			
		||||
        ).save()
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
        return Response("The deployment was added successfully")
 | 
			
		||||
 | 
			
		||||
    def delete(self, request, pk):
 | 
			
		||||
        d = get_object_or_404(Deployment, pk=pk)
 | 
			
		||||
 | 
			
		||||
        if not _has_perm_on_site(request.user, d.site.pk):
 | 
			
		||||
            raise PermissionDenied()
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            d.auth_token.delete()
 | 
			
		||||
        except:
 | 
			
		||||
            pass
 | 
			
		||||
 | 
			
		||||
        d.delete()
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
        return Response("The deployment was deleted")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GenerateAgent(APIView):
 | 
			
		||||
 
 | 
			
		||||
@@ -53,9 +53,9 @@ If (Get-Service $serviceName -ErrorAction SilentlyContinue) {
 | 
			
		||||
      Write-Output "Waiting for network"
 | 
			
		||||
      Start-Sleep -s 5
 | 
			
		||||
      $X += 1      
 | 
			
		||||
    } until(($connectreult = Test-NetConnection $apilink[2] -Port 443 | ? { $_.TcpTestSucceeded }) -or $X -eq 3)
 | 
			
		||||
    } until(($connectresult = Test-NetConnection $apilink[2] -Port 443 | ? { $_.TcpTestSucceeded }) -or $X -eq 3)
 | 
			
		||||
    
 | 
			
		||||
    if ($connectreult.TcpTestSucceeded -eq $true){
 | 
			
		||||
    if ($connectresult.TcpTestSucceeded -eq $true){
 | 
			
		||||
        Try
 | 
			
		||||
        {  
 | 
			
		||||
            Invoke-WebRequest -Uri $downloadlink -OutFile $OutPath\$output
 | 
			
		||||
 
 | 
			
		||||
@@ -1,32 +1,22 @@
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from scripts.models import Script
 | 
			
		||||
from logs.models import PendingAction
 | 
			
		||||
from scripts.models import Script
 | 
			
		||||
from accounts.models import User
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
    help = "Collection of tasks to run after updating the rmm, after migrations"
 | 
			
		||||
 | 
			
		||||
    def handle(self, *args, **kwargs):
 | 
			
		||||
        # 10-16-2020 changed the type of the agent's 'disks' model field
 | 
			
		||||
        # from a dict of dicts, to a list of disks in the golang agent
 | 
			
		||||
        # the following will convert dicts to lists for agent's still on the python agent
 | 
			
		||||
        agents = Agent.objects.only("pk", "disks")
 | 
			
		||||
        for agent in agents:
 | 
			
		||||
            if agent.disks is not None and isinstance(agent.disks, dict):
 | 
			
		||||
                new = []
 | 
			
		||||
                for k, v in agent.disks.items():
 | 
			
		||||
                    new.append(v)
 | 
			
		||||
 | 
			
		||||
                agent.disks = new
 | 
			
		||||
                agent.save(update_fields=["disks"])
 | 
			
		||||
                self.stdout.write(
 | 
			
		||||
                    self.style.SUCCESS(f"Migrated disks on {agent.hostname}")
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
        # remove task pending actions. deprecated 4/20/2021
 | 
			
		||||
        PendingAction.objects.filter(action_type="taskaction").delete()
 | 
			
		||||
 | 
			
		||||
        # 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()
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										22
									
								
								api/tacticalrmm/core/migrations/0022_urlaction.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								api/tacticalrmm/core/migrations/0022_urlaction.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
			
		||||
# Generated by Django 3.1.7 on 2021-05-02 02:13
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('core', '0021_customfield_hide_in_ui'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='URLAction',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
			
		||||
                ('name', models.CharField(max_length=25)),
 | 
			
		||||
                ('desc', models.CharField(blank=True, max_length=100, null=True)),
 | 
			
		||||
                ('pattern', models.TextField()),
 | 
			
		||||
            ],
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -0,0 +1,18 @@
 | 
			
		||||
# Generated by Django 3.2.2 on 2021-05-14 04:30
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('core', '0022_urlaction'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='coresettings',
 | 
			
		||||
            name='clear_faults_days',
 | 
			
		||||
            field=models.IntegerField(default=0),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										23
									
								
								api/tacticalrmm/core/migrations/0024_auto_20210707_1828.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								api/tacticalrmm/core/migrations/0024_auto_20210707_1828.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
# Generated by Django 3.2.1 on 2021-07-07 18:28
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('core', '0023_coresettings_clear_faults_days'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='coresettings',
 | 
			
		||||
            name='agent_history_prune_days',
 | 
			
		||||
            field=models.PositiveIntegerField(default=30),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='coresettings',
 | 
			
		||||
            name='resolved_alerts_prune_days',
 | 
			
		||||
            field=models.PositiveIntegerField(default=0),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user