Compare commits
	
		
			520 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					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 | ||
| 
						 | 
					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 | 
@@ -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:
 | 
			
		||||
@@ -127,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:
 | 
			
		||||
@@ -156,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:
 | 
			
		||||
@@ -174,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:
 | 
			
		||||
@@ -192,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:
 | 
			
		||||
@@ -221,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"
 | 
			
		||||
@@ -234,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"
 | 
			
		||||
@@ -258,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
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@ asyncio-nats-client
 | 
			
		||||
celery
 | 
			
		||||
channels
 | 
			
		||||
channels_redis
 | 
			
		||||
django-ipware
 | 
			
		||||
Django
 | 
			
		||||
django-cors-headers
 | 
			
		||||
django-rest-knox
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -48,3 +48,4 @@ 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/)
 | 
			
		||||
 
 | 
			
		||||
@@ -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,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
 | 
			
		||||
 | 
			
		||||
@@ -24,6 +25,7 @@ CLIENT_TREE_SORT_CHOICES = [
 | 
			
		||||
 | 
			
		||||
class User(AbstractUser, BaseAuditModel):
 | 
			
		||||
    is_active = models.BooleanField(default=True)
 | 
			
		||||
    block_dashboard_login = models.BooleanField(default=False)
 | 
			
		||||
    totp_key = models.CharField(max_length=50, null=True, blank=True)
 | 
			
		||||
    dark_mode = models.BooleanField(default=True)
 | 
			
		||||
    show_community_scripts = models.BooleanField(default=True)
 | 
			
		||||
@@ -46,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",
 | 
			
		||||
@@ -59,7 +64,7 @@ class User(AbstractUser, BaseAuditModel):
 | 
			
		||||
        "accounts.Role",
 | 
			
		||||
        null=True,
 | 
			
		||||
        blank=True,
 | 
			
		||||
        related_name="roles",
 | 
			
		||||
        related_name="users",
 | 
			
		||||
        on_delete=models.SET_NULL,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
@@ -71,11 +76,13 @@ class User(AbstractUser, BaseAuditModel):
 | 
			
		||||
        return UserSerializer(user).data
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Role(models.Model):
 | 
			
		||||
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)
 | 
			
		||||
@@ -87,91 +94,107 @@ class Role(models.Model):
 | 
			
		||||
    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 perms():
 | 
			
		||||
        return [
 | 
			
		||||
            "is_superuser",
 | 
			
		||||
            "can_use_mesh",
 | 
			
		||||
            "can_uninstall_agents",
 | 
			
		||||
            "can_update_agents",
 | 
			
		||||
            "can_edit_agent",
 | 
			
		||||
            "can_manage_procs",
 | 
			
		||||
            "can_view_eventlogs",
 | 
			
		||||
            "can_send_cmd",
 | 
			
		||||
            "can_reboot_agents",
 | 
			
		||||
            "can_install_agents",
 | 
			
		||||
            "can_run_scripts",
 | 
			
		||||
            "can_run_bulk",
 | 
			
		||||
            "can_manage_notes",
 | 
			
		||||
            "can_edit_core_settings",
 | 
			
		||||
            "can_do_server_maint",
 | 
			
		||||
            "can_code_sign",
 | 
			
		||||
            "can_manage_checks",
 | 
			
		||||
            "can_run_checks",
 | 
			
		||||
            "can_manage_clients",
 | 
			
		||||
            "can_manage_sites",
 | 
			
		||||
            "can_manage_deployments",
 | 
			
		||||
            "can_manage_automation_policies",
 | 
			
		||||
            "can_manage_autotasks",
 | 
			
		||||
            "can_run_autotasks",
 | 
			
		||||
            "can_view_auditlogs",
 | 
			
		||||
            "can_manage_pendingactions",
 | 
			
		||||
            "can_view_debuglogs",
 | 
			
		||||
            "can_manage_scripts",
 | 
			
		||||
            "can_manage_alerts",
 | 
			
		||||
            "can_manage_winsvcs",
 | 
			
		||||
            "can_manage_software",
 | 
			
		||||
            "can_manage_winupdates",
 | 
			
		||||
            "can_manage_accounts",
 | 
			
		||||
            "can_manage_roles",
 | 
			
		||||
        ]
 | 
			
		||||
    def serialize(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
 | 
			
		||||
 
 | 
			
		||||
@@ -6,14 +6,38 @@ from tacticalrmm.permissions import _has_perm
 | 
			
		||||
class AccountsPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        if r.method == "GET":
 | 
			
		||||
            return True
 | 
			
		||||
            return _has_perm(r, "can_list_accounts")
 | 
			
		||||
        else:
 | 
			
		||||
 | 
			
		||||
        return _has_perm(r, "can_manage_accounts")
 | 
			
		||||
            # allow users to reset their own password/2fa see issue #686
 | 
			
		||||
            base_path = "/accounts/users/"
 | 
			
		||||
            paths = ["reset/", "reset_totp/"]
 | 
			
		||||
 | 
			
		||||
            if r.path in [base_path + i for i in paths]:
 | 
			
		||||
                from accounts.models import User
 | 
			
		||||
 | 
			
		||||
                try:
 | 
			
		||||
                    user = User.objects.get(pk=r.data["id"])
 | 
			
		||||
                except User.DoesNotExist:
 | 
			
		||||
                    pass
 | 
			
		||||
                else:
 | 
			
		||||
                    if user == r.user:
 | 
			
		||||
                        return True
 | 
			
		||||
 | 
			
		||||
            return _has_perm(r, "can_manage_accounts")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RolesPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        if r.method == "GET":
 | 
			
		||||
            return True
 | 
			
		||||
            return _has_perm(r, "can_list_roles")
 | 
			
		||||
        else:
 | 
			
		||||
            return _has_perm(r, "can_manage_roles")
 | 
			
		||||
 | 
			
		||||
        return _has_perm(r, "can_manage_roles")
 | 
			
		||||
 | 
			
		||||
class APIKeyPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        if r.method == "GET":
 | 
			
		||||
            return _has_perm(r, "can_list_api_keys")
 | 
			
		||||
 | 
			
		||||
        return _has_perm(r, "can_manage_api_keys")
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,11 @@
 | 
			
		||||
import pyotp
 | 
			
		||||
from rest_framework.serializers import ModelSerializer, SerializerMethodField
 | 
			
		||||
from rest_framework.serializers import (
 | 
			
		||||
    ModelSerializer,
 | 
			
		||||
    SerializerMethodField,
 | 
			
		||||
    ReadOnlyField,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
from .models import User, Role
 | 
			
		||||
from .models import APIKey, User, Role
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UserUISerializer(ModelSerializer):
 | 
			
		||||
@@ -16,6 +20,8 @@ class UserUISerializer(ModelSerializer):
 | 
			
		||||
            "client_tree_sort",
 | 
			
		||||
            "client_tree_splitter",
 | 
			
		||||
            "loading_bar_color",
 | 
			
		||||
            "clear_search_when_switching",
 | 
			
		||||
            "block_dashboard_login",
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -30,7 +36,9 @@ class UserSerializer(ModelSerializer):
 | 
			
		||||
            "email",
 | 
			
		||||
            "is_active",
 | 
			
		||||
            "last_login",
 | 
			
		||||
            "last_login_ip",
 | 
			
		||||
            "role",
 | 
			
		||||
            "block_dashboard_login",
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -53,6 +61,38 @@ class TOTPSetupSerializer(ModelSerializer):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RoleSerializer(ModelSerializer):
 | 
			
		||||
    user_count = SerializerMethodField()
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Role
 | 
			
		||||
        fields = "__all__"
 | 
			
		||||
 | 
			
		||||
    def get_user_count(self, obj):
 | 
			
		||||
        return obj.users.count()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RoleAuditSerializer(ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Role
 | 
			
		||||
        fields = "__all__"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class APIKeySerializer(ModelSerializer):
 | 
			
		||||
 | 
			
		||||
    username = ReadOnlyField(source="user.username")
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = APIKey
 | 
			
		||||
        fields = "__all__"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class APIKeyAuditSerializer(ModelSerializer):
 | 
			
		||||
    username = ReadOnlyField(source="user.username")
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = APIKey
 | 
			
		||||
        fields = [
 | 
			
		||||
            "name",
 | 
			
		||||
            "username",
 | 
			
		||||
            "expiration",
 | 
			
		||||
        ]
 | 
			
		||||
 
 | 
			
		||||
@@ -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,7 +9,8 @@ urlpatterns = [
 | 
			
		||||
    path("users/reset_totp/", views.UserActions.as_view()),
 | 
			
		||||
    path("users/setup_totp/", views.TOTPSetup.as_view()),
 | 
			
		||||
    path("users/ui/", views.UserUI.as_view()),
 | 
			
		||||
    path("permslist/", views.PermsList.as_view()),
 | 
			
		||||
    path("roles/", views.GetAddRoles.as_view()),
 | 
			
		||||
    path("<int:pk>/role/", views.GetUpdateDeleteRole.as_view()),
 | 
			
		||||
    path("roles/<int:pk>/", views.GetUpdateDeleteRole.as_view()),
 | 
			
		||||
    path("apikeys/", views.GetAddAPIKeys.as_view()),
 | 
			
		||||
    path("apikeys/<int:pk>/", views.GetUpdateDeleteAPIKey.as_view()),
 | 
			
		||||
]
 | 
			
		||||
 
 | 
			
		||||
@@ -3,23 +3,24 @@ 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, 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, Role
 | 
			
		||||
from .permissions import AccountsPerms, RolesPerms
 | 
			
		||||
from .models import APIKey, Role, User
 | 
			
		||||
from .permissions import APIKeyPerms, AccountsPerms, RolesPerms
 | 
			
		||||
from .serializers import (
 | 
			
		||||
    APIKeySerializer,
 | 
			
		||||
    RoleSerializer,
 | 
			
		||||
    TOTPSetupSerializer,
 | 
			
		||||
    UserSerializer,
 | 
			
		||||
    UserUISerializer,
 | 
			
		||||
    RoleSerializer,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -40,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)
 | 
			
		||||
@@ -66,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)
 | 
			
		||||
 | 
			
		||||
@@ -76,18 +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)
 | 
			
		||||
 | 
			
		||||
@@ -104,8 +130,10 @@ class GetAddUsers(APIView):
 | 
			
		||||
                f"ERROR: User {request.data['username']} already exists!"
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        user.first_name = request.data["first_name"]
 | 
			
		||||
        user.last_name = request.data["last_name"]
 | 
			
		||||
        if "first_name" in request.data.keys():
 | 
			
		||||
            user.first_name = request.data["first_name"]
 | 
			
		||||
        if "last_name" in request.data.keys():
 | 
			
		||||
            user.last_name = request.data["last_name"]
 | 
			
		||||
        if "role" in request.data.keys() and isinstance(request.data["role"], int):
 | 
			
		||||
            role = get_object_or_404(Role, pk=request.data["role"])
 | 
			
		||||
            user.role = role
 | 
			
		||||
@@ -196,11 +224,6 @@ class UserUI(APIView):
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PermsList(APIView):
 | 
			
		||||
    def get(self, request):
 | 
			
		||||
        return Response(Role.perms())
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetAddRoles(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, RolesPerms]
 | 
			
		||||
 | 
			
		||||
@@ -212,7 +235,7 @@ class GetAddRoles(APIView):
 | 
			
		||||
        serializer = RoleSerializer(data=request.data)
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        serializer.save()
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
        return Response("Role was added")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetUpdateDeleteRole(APIView):
 | 
			
		||||
@@ -227,9 +250,48 @@ class GetUpdateDeleteRole(APIView):
 | 
			
		||||
        serializer = RoleSerializer(instance=role, data=request.data)
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        serializer.save()
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
        return Response("Role was edited")
 | 
			
		||||
 | 
			
		||||
    def delete(self, request, pk):
 | 
			
		||||
        role = get_object_or_404(Role, pk=pk)
 | 
			
		||||
        role.delete()
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
        return Response("Role was removed")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetAddAPIKeys(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, APIKeyPerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request):
 | 
			
		||||
        apikeys = APIKey.objects.all()
 | 
			
		||||
        return Response(APIKeySerializer(apikeys, many=True).data)
 | 
			
		||||
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
        # generate a random API Key
 | 
			
		||||
        from django.utils.crypto import get_random_string
 | 
			
		||||
 | 
			
		||||
        request.data["key"] = get_random_string(length=32).upper()
 | 
			
		||||
        serializer = APIKeySerializer(data=request.data)
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        obj = serializer.save()
 | 
			
		||||
        return Response("The API Key was added")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetUpdateDeleteAPIKey(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, APIKeyPerms]
 | 
			
		||||
 | 
			
		||||
    def put(self, request, pk):
 | 
			
		||||
        apikey = get_object_or_404(APIKey, pk=pk)
 | 
			
		||||
 | 
			
		||||
        # remove API key is present in request data
 | 
			
		||||
        if "key" in request.data.keys():
 | 
			
		||||
            request.data.pop("key")
 | 
			
		||||
 | 
			
		||||
        serializer = APIKeySerializer(instance=apikey, data=request.data, partial=True)
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        serializer.save()
 | 
			
		||||
        return Response("The API Key was edited")
 | 
			
		||||
 | 
			
		||||
    def delete(self, request, pk):
 | 
			
		||||
        apikey = get_object_or_404(APIKey, pk=pk)
 | 
			
		||||
        apikey.delete()
 | 
			
		||||
        return Response("The API Key was deleted")
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
@@ -325,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
 | 
			
		||||
@@ -343,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"}
 | 
			
		||||
@@ -445,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 (
 | 
			
		||||
@@ -454,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
 | 
			
		||||
@@ -483,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 (
 | 
			
		||||
@@ -493,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
 | 
			
		||||
@@ -608,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:
 | 
			
		||||
@@ -697,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)
 | 
			
		||||
 | 
			
		||||
@@ -739,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()
 | 
			
		||||
@@ -752,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:
 | 
			
		||||
@@ -772,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)
 | 
			
		||||
@@ -807,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}, "
 | 
			
		||||
@@ -822,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}, "
 | 
			
		||||
@@ -837,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,
 | 
			
		||||
        )
 | 
			
		||||
@@ -846,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,
 | 
			
		||||
        )
 | 
			
		||||
@@ -862,6 +868,8 @@ RECOVERY_CHOICES = [
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RecoveryAction(models.Model):
 | 
			
		||||
    objects = PermissionQuerySet.as_manager()
 | 
			
		||||
 | 
			
		||||
    agent = models.ForeignKey(
 | 
			
		||||
        Agent,
 | 
			
		||||
        related_name="recoveryactions",
 | 
			
		||||
@@ -876,6 +884,8 @@ class RecoveryAction(models.Model):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Note(models.Model):
 | 
			
		||||
    objects = PermissionQuerySet.as_manager()
 | 
			
		||||
 | 
			
		||||
    agent = models.ForeignKey(
 | 
			
		||||
        Agent,
 | 
			
		||||
        related_name="notes",
 | 
			
		||||
@@ -896,6 +906,8 @@ class Note(models.Model):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AgentCustomField(models.Model):
 | 
			
		||||
    objects = PermissionQuerySet.as_manager()
 | 
			
		||||
 | 
			
		||||
    agent = models.ForeignKey(
 | 
			
		||||
        Agent,
 | 
			
		||||
        related_name="custom_fields",
 | 
			
		||||
@@ -928,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}"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,16 +1,42 @@
 | 
			
		||||
from rest_framework import permissions
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.permissions import _has_perm
 | 
			
		||||
from tacticalrmm.permissions import _has_perm, _has_perm_on_agent
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AgentPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        if r.method == "GET":
 | 
			
		||||
            if "agent_id" in view.kwargs.keys():
 | 
			
		||||
                return _has_perm(r, "can_list_agents") and _has_perm_on_agent(
 | 
			
		||||
                    r.user, view.kwargs["agent_id"]
 | 
			
		||||
                )
 | 
			
		||||
            else:
 | 
			
		||||
                return _has_perm(r, "can_list_agents")
 | 
			
		||||
        elif r.method == "DELETE":
 | 
			
		||||
            return _has_perm(r, "can_uninstall_agents") and _has_perm_on_agent(
 | 
			
		||||
                r.user, view.kwargs["agent_id"]
 | 
			
		||||
            )
 | 
			
		||||
        else:
 | 
			
		||||
            if r.path == "/agents/maintenance/bulk/":
 | 
			
		||||
                return _has_perm(r, "can_edit_agent")
 | 
			
		||||
            else:
 | 
			
		||||
                return _has_perm(r, "can_edit_agent") and _has_perm_on_agent(
 | 
			
		||||
                    r.user, view.kwargs["agent_id"]
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RecoverAgentPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        return _has_perm(r, "can_recover_agents") and _has_perm_on_agent(
 | 
			
		||||
            r.user, view.kwargs["agent_id"]
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class MeshPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        return _has_perm(r, "can_use_mesh")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UninstallPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        return _has_perm(r, "can_uninstall_agents")
 | 
			
		||||
        return _has_perm(r, "can_use_mesh") and _has_perm_on_agent(
 | 
			
		||||
            r.user, view.kwargs["agent_id"]
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UpdateAgentPerms(permissions.BasePermission):
 | 
			
		||||
@@ -18,29 +44,39 @@ class UpdateAgentPerms(permissions.BasePermission):
 | 
			
		||||
        return _has_perm(r, "can_update_agents")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class EditAgentPerms(permissions.BasePermission):
 | 
			
		||||
class PingAgentPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        return _has_perm(r, "can_edit_agent")
 | 
			
		||||
        return _has_perm(r, "can_ping_agents") and _has_perm_on_agent(
 | 
			
		||||
            r.user, view.kwargs["agent_id"]
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ManageProcPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        return _has_perm(r, "can_manage_procs")
 | 
			
		||||
        return _has_perm(r, "can_manage_procs") and _has_perm_on_agent(
 | 
			
		||||
            r.user, view.kwargs["agent_id"]
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class EvtLogPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        return _has_perm(r, "can_view_eventlogs")
 | 
			
		||||
        return _has_perm(r, "can_view_eventlogs") and _has_perm_on_agent(
 | 
			
		||||
            r.user, view.kwargs["agent_id"]
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SendCMDPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        return _has_perm(r, "can_send_cmd")
 | 
			
		||||
        return _has_perm(r, "can_send_cmd") and _has_perm_on_agent(
 | 
			
		||||
            r.user, view.kwargs["agent_id"]
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RebootAgentPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        return _has_perm(r, "can_reboot_agents")
 | 
			
		||||
        return _has_perm(r, "can_reboot_agents") and _has_perm_on_agent(
 | 
			
		||||
            r.user, view.kwargs["agent_id"]
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class InstallAgentPerms(permissions.BasePermission):
 | 
			
		||||
@@ -50,14 +86,38 @@ class InstallAgentPerms(permissions.BasePermission):
 | 
			
		||||
 | 
			
		||||
class RunScriptPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        return _has_perm(r, "can_run_scripts")
 | 
			
		||||
        return _has_perm(r, "can_run_scripts") and _has_perm_on_agent(
 | 
			
		||||
            r.user, view.kwargs["agent_id"]
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ManageNotesPerms(permissions.BasePermission):
 | 
			
		||||
class AgentNotesPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        return _has_perm(r, "can_manage_notes")
 | 
			
		||||
 | 
			
		||||
        # permissions for GET /agents/notes/ endpoint
 | 
			
		||||
        if r.method == "GET":
 | 
			
		||||
 | 
			
		||||
            # permissions for /agents/<agent_id>/notes endpoint
 | 
			
		||||
            if "agent_id" in view.kwargs.keys():
 | 
			
		||||
                return _has_perm(r, "can_list_notes") and _has_perm_on_agent(
 | 
			
		||||
                    r.user, view.kwargs["agent_id"]
 | 
			
		||||
                )
 | 
			
		||||
            else:
 | 
			
		||||
                return _has_perm(r, "can_list_notes")
 | 
			
		||||
        else:
 | 
			
		||||
            return _has_perm(r, "can_manage_notes")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RunBulkPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        return _has_perm(r, "can_run_bulk")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AgentHistoryPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        if "agent_id" in view.kwargs.keys():
 | 
			
		||||
            return _has_perm(r, "can_list_agent_history") and _has_perm_on_agent(
 | 
			
		||||
                r.user, view.kwargs["agent_id"]
 | 
			
		||||
            )
 | 
			
		||||
        else:
 | 
			
		||||
            return _has_perm(r, "can_list_agent_history")
 | 
			
		||||
 
 | 
			
		||||
@@ -1,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)
 | 
			
		||||
 | 
			
		||||
@@ -232,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()
 | 
			
		||||
@@ -255,28 +243,32 @@ 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
 | 
			
		||||
@@ -307,19 +299,73 @@ def clear_faults_task(older_than_days: int) -> None:
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
def monitor_agents_task() -> None:
 | 
			
		||||
    agents = Agent.objects.only(
 | 
			
		||||
        "pk", "agent_id", "last_seen", "overdue_time", "offline_time"
 | 
			
		||||
    )
 | 
			
		||||
    ids = [i.agent_id for i in agents if i.status != "online"]
 | 
			
		||||
    run_nats_api_cmd("monitor", ids)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
def get_wmi_task() -> None:
 | 
			
		||||
    agents = Agent.objects.only(
 | 
			
		||||
        "pk", "agent_id", "last_seen", "overdue_time", "offline_time"
 | 
			
		||||
    )
 | 
			
		||||
    ids = [i.agent_id for i in agents if i.status == "online"]
 | 
			
		||||
    run_nats_api_cmd("wmi", ids)
 | 
			
		||||
    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,55 +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, 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 (
 | 
			
		||||
    EditAgentPerms,
 | 
			
		||||
    AgentHistoryPerms,
 | 
			
		||||
    AgentPerms,
 | 
			
		||||
    EvtLogPerms,
 | 
			
		||||
    InstallAgentPerms,
 | 
			
		||||
    ManageNotesPerms,
 | 
			
		||||
    RecoverAgentPerms,
 | 
			
		||||
    AgentNotesPerms,
 | 
			
		||||
    ManageProcPerms,
 | 
			
		||||
    MeshPerms,
 | 
			
		||||
    RebootAgentPerms,
 | 
			
		||||
    RunBulkPerms,
 | 
			
		||||
    RunScriptPerms,
 | 
			
		||||
    SendCMDPerms,
 | 
			
		||||
    UninstallPerms,
 | 
			
		||||
    PingAgentPerms,
 | 
			
		||||
    UpdateAgentPerms,
 | 
			
		||||
)
 | 
			
		||||
from .serializers import (
 | 
			
		||||
    AgentCustomFieldSerializer,
 | 
			
		||||
    AgentEditSerializer,
 | 
			
		||||
    AgentHistorySerializer,
 | 
			
		||||
    AgentHostnameSerializer,
 | 
			
		||||
    AgentOverdueActionSerializer,
 | 
			
		||||
    AgentSerializer,
 | 
			
		||||
    AgentTableSerializer,
 | 
			
		||||
    NoteSerializer,
 | 
			
		||||
    NotesSerializer,
 | 
			
		||||
    AgentNoteSerializer,
 | 
			
		||||
)
 | 
			
		||||
from .tasks import run_script_email_results_task, send_agent_update_task
 | 
			
		||||
 | 
			
		||||
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],
 | 
			
		||||
@@ -68,20 +273,24 @@ def get_agent_versions(request):
 | 
			
		||||
@api_view(["POST"])
 | 
			
		||||
@permission_classes([IsAuthenticated, UpdateAgentPerms])
 | 
			
		||||
def update_agents(request):
 | 
			
		||||
    q = Agent.objects.filter(pk__in=request.data["pks"]).only("pk", "version")
 | 
			
		||||
    pks: list[int] = [
 | 
			
		||||
        i.pk
 | 
			
		||||
    q = (
 | 
			
		||||
        Agent.objects.filter_by_role(request.user)
 | 
			
		||||
        .filter(agent_id__in=request.data["agent_ids"])
 | 
			
		||||
        .only("agent_id", "version")
 | 
			
		||||
    )
 | 
			
		||||
    agent_ids: list[str] = [
 | 
			
		||||
        i.agent_id
 | 
			
		||||
        for i in q
 | 
			
		||||
        if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER)
 | 
			
		||||
    ]
 | 
			
		||||
    send_agent_update_task.delay(pks=pks)
 | 
			
		||||
    send_agent_update_task.delay(agent_ids=agent_ids)
 | 
			
		||||
    return Response("ok")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view()
 | 
			
		||||
@permission_classes([IsAuthenticated, UninstallPerms])
 | 
			
		||||
def ping(request, pk):
 | 
			
		||||
    agent = get_object_or_404(Agent, pk=pk)
 | 
			
		||||
@api_view(["GET"])
 | 
			
		||||
@permission_classes([IsAuthenticated, PingAgentPerms])
 | 
			
		||||
def ping(request, agent_id):
 | 
			
		||||
    agent = get_object_or_404(Agent, agent_id=agent_id)
 | 
			
		||||
    status = "offline"
 | 
			
		||||
    attempts = 0
 | 
			
		||||
    while 1:
 | 
			
		||||
@@ -99,127 +308,12 @@ def ping(request, pk):
 | 
			
		||||
    return Response({"name": agent.hostname, "status": status})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view(["DELETE"])
 | 
			
		||||
@permission_classes([IsAuthenticated, UninstallPerms])
 | 
			
		||||
def uninstall(request):
 | 
			
		||||
    agent = get_object_or_404(Agent, pk=request.data["pk"])
 | 
			
		||||
    asyncio.run(agent.nats_cmd({"func": "uninstall"}, wait=False))
 | 
			
		||||
    name = agent.hostname
 | 
			
		||||
    agent.delete()
 | 
			
		||||
    reload_nats()
 | 
			
		||||
    return Response(f"{name} will now be uninstalled.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view(["PATCH", "PUT"])
 | 
			
		||||
@permission_classes([IsAuthenticated, EditAgentPerms])
 | 
			
		||||
def edit_agent(request):
 | 
			
		||||
    agent = get_object_or_404(Agent, pk=request.data["id"])
 | 
			
		||||
 | 
			
		||||
    a_serializer = 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()
 | 
			
		||||
@permission_classes([IsAuthenticated, MeshPerms])
 | 
			
		||||
def meshcentral(request, pk):
 | 
			
		||||
    agent = get_object_or_404(Agent, pk=pk)
 | 
			
		||||
    core = CoreSettings.objects.first()
 | 
			
		||||
 | 
			
		||||
    token = agent.get_login_token(
 | 
			
		||||
        key=core.mesh_token, user=f"user//{core.mesh_username}"
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    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()
 | 
			
		||||
@permission_classes([IsAuthenticated, ManageProcPerms])
 | 
			
		||||
def kill_proc(request, pk, pid):
 | 
			
		||||
    agent = get_object_or_404(Agent, pk=pk)
 | 
			
		||||
    r = asyncio.run(
 | 
			
		||||
        agent.nats_cmd({"func": "killproc", "procpid": int(pid)}, timeout=15)
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    if r == "timeout":
 | 
			
		||||
        return notify_error("Unable to contact the agent")
 | 
			
		||||
    elif r != "ok":
 | 
			
		||||
        return notify_error(r)
 | 
			
		||||
 | 
			
		||||
    return Response("ok")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view()
 | 
			
		||||
@api_view(["GET"])
 | 
			
		||||
@permission_classes([IsAuthenticated, EvtLogPerms])
 | 
			
		||||
def get_event_log(request, pk, logtype, days):
 | 
			
		||||
    agent = get_object_or_404(Agent, pk=pk)
 | 
			
		||||
def get_event_log(request, agent_id, logtype, days):
 | 
			
		||||
    agent = get_object_or_404(Agent, agent_id=agent_id)
 | 
			
		||||
    timeout = 180 if logtype == "Security" else 30
 | 
			
		||||
 | 
			
		||||
    data = {
 | 
			
		||||
        "func": "eventlog",
 | 
			
		||||
        "timeout": timeout,
 | 
			
		||||
@@ -229,7 +323,7 @@ def get_event_log(request, pk, logtype, days):
 | 
			
		||||
        },
 | 
			
		||||
    }
 | 
			
		||||
    r = asyncio.run(agent.nats_cmd(data, timeout=timeout + 2))
 | 
			
		||||
    if r == "timeout":
 | 
			
		||||
    if r == "timeout" or r == "natsdown":
 | 
			
		||||
        return notify_error("Unable to contact the agent")
 | 
			
		||||
 | 
			
		||||
    return Response(r)
 | 
			
		||||
@@ -237,8 +331,8 @@ def get_event_log(request, pk, logtype, days):
 | 
			
		||||
 | 
			
		||||
@api_view(["POST"])
 | 
			
		||||
@permission_classes([IsAuthenticated, SendCMDPerms])
 | 
			
		||||
def send_raw_cmd(request):
 | 
			
		||||
    agent = get_object_or_404(Agent, pk=request.data["pk"])
 | 
			
		||||
def send_raw_cmd(request, agent_id):
 | 
			
		||||
    agent = get_object_or_404(Agent, agent_id=agent_id)
 | 
			
		||||
    timeout = int(request.data["timeout"])
 | 
			
		||||
    data = {
 | 
			
		||||
        "func": "rawcmd",
 | 
			
		||||
@@ -248,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":
 | 
			
		||||
@@ -255,87 +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")
 | 
			
		||||
@@ -343,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")
 | 
			
		||||
@@ -388,6 +425,7 @@ class Reboot(APIView):
 | 
			
		||||
@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
 | 
			
		||||
 | 
			
		||||
@@ -396,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":
 | 
			
		||||
@@ -503,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)
 | 
			
		||||
@@ -521,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
 | 
			
		||||
@@ -559,28 +607,43 @@ def recover(request):
 | 
			
		||||
 | 
			
		||||
@api_view(["POST"])
 | 
			
		||||
@permission_classes([IsAuthenticated, RunScriptPerms])
 | 
			
		||||
def run_script(request):
 | 
			
		||||
    agent = get_object_or_404(Agent, pk=request.data["pk"])
 | 
			
		||||
    script = get_object_or_404(Script, pk=request.data["scriptPK"])
 | 
			
		||||
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,
 | 
			
		||||
@@ -589,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=90))
 | 
			
		||||
    if r != "ok":
 | 
			
		||||
        return notify_error("Unable to contact the agent")
 | 
			
		||||
 | 
			
		||||
    return Response(f"Repaired mesh agent on {agent.hostname}")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view(["POST"])
 | 
			
		||||
def get_mesh_exe(request, arch):
 | 
			
		||||
    filename = "meshagent.exe" if arch == "64" else "meshagent-x86.exe"
 | 
			
		||||
@@ -628,34 +723,62 @@ def get_mesh_exe(request, arch):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetAddNotes(APIView):
 | 
			
		||||
    def get(self, request, pk):
 | 
			
		||||
        agent = get_object_or_404(Agent, pk=pk)
 | 
			
		||||
        return Response(NotesSerializer(agent).data)
 | 
			
		||||
    permission_classes = [IsAuthenticated, AgentNotesPerms]
 | 
			
		||||
 | 
			
		||||
    def post(self, request, pk):
 | 
			
		||||
        agent = get_object_or_404(Agent, pk=pk)
 | 
			
		||||
        serializer = NoteSerializer(data=request.data, partial=True)
 | 
			
		||||
    def get(self, request, agent_id=None):
 | 
			
		||||
        if agent_id:
 | 
			
		||||
            agent = get_object_or_404(Agent, agent_id=agent_id)
 | 
			
		||||
            notes = Note.objects.filter(agent=agent)
 | 
			
		||||
        else:
 | 
			
		||||
            notes = Note.objects.filter_by_role(request.user)
 | 
			
		||||
 | 
			
		||||
        return Response(AgentNoteSerializer(notes, many=True).data)
 | 
			
		||||
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
 | 
			
		||||
        if not _has_perm_on_agent(request.user, agent.agent_id):
 | 
			
		||||
            raise PermissionDenied()
 | 
			
		||||
 | 
			
		||||
        data = {
 | 
			
		||||
            "note": request.data["note"],
 | 
			
		||||
            "agent": agent.pk,
 | 
			
		||||
            "user": request.user.pk,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        serializer = AgentNoteSerializer(data=data)
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        serializer.save(agent=agent, user=request.user)
 | 
			
		||||
        serializer.save()
 | 
			
		||||
        return Response("Note added!")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetEditDeleteNote(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, ManageNotesPerms]
 | 
			
		||||
    permission_classes = [IsAuthenticated, AgentNotesPerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request, pk):
 | 
			
		||||
        note = get_object_or_404(Note, pk=pk)
 | 
			
		||||
        return Response(NoteSerializer(note).data)
 | 
			
		||||
 | 
			
		||||
    def patch(self, request, pk):
 | 
			
		||||
        if not _has_perm_on_agent(request.user, note.agent.agent_id):
 | 
			
		||||
            raise PermissionDenied()
 | 
			
		||||
 | 
			
		||||
        return Response(AgentNoteSerializer(note).data)
 | 
			
		||||
 | 
			
		||||
    def put(self, request, pk):
 | 
			
		||||
        note = get_object_or_404(Note, pk=pk)
 | 
			
		||||
        serializer = NoteSerializer(instance=note, data=request.data, partial=True)
 | 
			
		||||
 | 
			
		||||
        if not _has_perm_on_agent(request.user, note.agent.agent_id):
 | 
			
		||||
            raise PermissionDenied()
 | 
			
		||||
 | 
			
		||||
        serializer = AgentNoteSerializer(instance=note, data=request.data, partial=True)
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        serializer.save()
 | 
			
		||||
        return Response("Note edited!")
 | 
			
		||||
 | 
			
		||||
    def delete(self, request, pk):
 | 
			
		||||
        note = get_object_or_404(Note, pk=pk)
 | 
			
		||||
 | 
			
		||||
        if not _has_perm_on_agent(request.user, note.agent.agent_id):
 | 
			
		||||
            raise PermissionDenied()
 | 
			
		||||
 | 
			
		||||
        note.delete()
 | 
			
		||||
        return Response("Note was deleted!")
 | 
			
		||||
 | 
			
		||||
@@ -663,17 +786,31 @@ class GetEditDeleteNote(APIView):
 | 
			
		||||
@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")
 | 
			
		||||
 | 
			
		||||
@@ -684,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]):
 | 
			
		||||
@@ -451,7 +464,7 @@ class Alert(models.Model):
 | 
			
		||||
                try:
 | 
			
		||||
                    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 (
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,55 @@
 | 
			
		||||
from django.shortcuts import get_object_or_404
 | 
			
		||||
from rest_framework import permissions
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.permissions import _has_perm
 | 
			
		||||
from tacticalrmm.permissions import _has_perm, _has_perm_on_agent
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ManageAlertsPerms(permissions.BasePermission):
 | 
			
		||||
def _has_perm_on_alert(user, id: int):
 | 
			
		||||
    from alerts.models import Alert
 | 
			
		||||
 | 
			
		||||
    role = user.role
 | 
			
		||||
    if user.is_superuser or (role and getattr(role, "is_superuser")):
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
    # make sure non-superusers with empty roles aren't permitted
 | 
			
		||||
    elif not role:
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
    alert = get_object_or_404(Alert, id=id)
 | 
			
		||||
 | 
			
		||||
    if alert.agent:
 | 
			
		||||
        agent_id = alert.agent.agent_id
 | 
			
		||||
    elif alert.assigned_check:
 | 
			
		||||
        agent_id = alert.assigned_check.agent.agent_id
 | 
			
		||||
    elif alert.assigned_task:
 | 
			
		||||
        agent_id = alert.assigned_task.agent.agent_id
 | 
			
		||||
    else:
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
    return _has_perm_on_agent(user, agent_id)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AlertPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        if r.method == "GET" or r.method == "PATCH":
 | 
			
		||||
            return True
 | 
			
		||||
            if "pk" in view.kwargs.keys():
 | 
			
		||||
                return _has_perm(r, "can_list_alerts") and _has_perm_on_alert(
 | 
			
		||||
                    r.user, view.kwargs["pk"]
 | 
			
		||||
                )
 | 
			
		||||
            else:
 | 
			
		||||
                return _has_perm(r, "can_list_alerts")
 | 
			
		||||
        else:
 | 
			
		||||
            if "pk" in view.kwargs.keys():
 | 
			
		||||
                return _has_perm(r, "can_manage_alerts") and _has_perm_on_alert(
 | 
			
		||||
                    r.user, view.kwargs["pk"]
 | 
			
		||||
                )
 | 
			
		||||
            else:
 | 
			
		||||
                return _has_perm(r, "can_manage_alerts")
 | 
			
		||||
 | 
			
		||||
        return _has_perm(r, "can_manage_alerts")
 | 
			
		||||
 | 
			
		||||
class AlertTemplatePerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        if r.method == "GET":
 | 
			
		||||
            return _has_perm(r, "can_list_alerttemplates")
 | 
			
		||||
        else:
 | 
			
		||||
            return _has_perm(r, "can_manage_alerttemplates")
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@ from rest_framework.fields import SerializerMethodField
 | 
			
		||||
from rest_framework.serializers import ModelSerializer, ReadOnlyField
 | 
			
		||||
 | 
			
		||||
from automation.serializers import PolicySerializer
 | 
			
		||||
from clients.serializers import ClientSerializer, SiteSerializer
 | 
			
		||||
from clients.serializers import ClientMinimumSerializer, SiteMinimumSerializer
 | 
			
		||||
from tacticalrmm.utils import get_default_timezone
 | 
			
		||||
 | 
			
		||||
from .models import Alert, AlertTemplate
 | 
			
		||||
@@ -113,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
 | 
			
		||||
@@ -1398,3 +1402,188 @@ class TestAlertTasks(TacticalTestCase):
 | 
			
		||||
            ["-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()),
 | 
			
		||||
]
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,7 @@ from rest_framework.views import APIView
 | 
			
		||||
from tacticalrmm.utils import notify_error
 | 
			
		||||
 | 
			
		||||
from .models import Alert, AlertTemplate
 | 
			
		||||
from .permissions import ManageAlertsPerms
 | 
			
		||||
from .permissions import AlertPerms, AlertTemplatePerms
 | 
			
		||||
from .serializers import (
 | 
			
		||||
    AlertSerializer,
 | 
			
		||||
    AlertTemplateRelationSerializer,
 | 
			
		||||
@@ -20,7 +20,7 @@ from .tasks import cache_agents_alert_template
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetAddAlerts(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, ManageAlertsPerms]
 | 
			
		||||
    permission_classes = [IsAuthenticated, AlertPerms]
 | 
			
		||||
 | 
			
		||||
    def patch(self, request):
 | 
			
		||||
 | 
			
		||||
@@ -92,7 +92,8 @@ class GetAddAlerts(APIView):
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
            alerts = (
 | 
			
		||||
                Alert.objects.filter(clientFilter)
 | 
			
		||||
                Alert.objects.filter_by_role(request.user)
 | 
			
		||||
                .filter(clientFilter)
 | 
			
		||||
                .filter(severityFilter)
 | 
			
		||||
                .filter(resolvedFilter)
 | 
			
		||||
                .filter(snoozedFilter)
 | 
			
		||||
@@ -101,7 +102,7 @@ class GetAddAlerts(APIView):
 | 
			
		||||
            return Response(AlertSerializer(alerts, many=True).data)
 | 
			
		||||
 | 
			
		||||
        else:
 | 
			
		||||
            alerts = Alert.objects.all()
 | 
			
		||||
            alerts = Alert.objects.filter_by_role(request.user)
 | 
			
		||||
            return Response(AlertSerializer(alerts, many=True).data)
 | 
			
		||||
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
@@ -113,11 +114,10 @@ class GetAddAlerts(APIView):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetUpdateDeleteAlert(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, ManageAlertsPerms]
 | 
			
		||||
    permission_classes = [IsAuthenticated, AlertPerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request, pk):
 | 
			
		||||
        alert = get_object_or_404(Alert, pk=pk)
 | 
			
		||||
 | 
			
		||||
        return Response(AlertSerializer(alert).data)
 | 
			
		||||
 | 
			
		||||
    def put(self, request, pk):
 | 
			
		||||
@@ -169,7 +169,7 @@ class GetUpdateDeleteAlert(APIView):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BulkAlerts(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, ManageAlertsPerms]
 | 
			
		||||
    permission_classes = [IsAuthenticated, AlertPerms]
 | 
			
		||||
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
        if request.data["bulk_action"] == "resolve":
 | 
			
		||||
@@ -193,11 +193,10 @@ class BulkAlerts(APIView):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetAddAlertTemplates(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, ManageAlertsPerms]
 | 
			
		||||
    permission_classes = [IsAuthenticated, AlertTemplatePerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request):
 | 
			
		||||
        alert_templates = AlertTemplate.objects.all()
 | 
			
		||||
 | 
			
		||||
        return Response(AlertTemplateSerializer(alert_templates, many=True).data)
 | 
			
		||||
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
@@ -212,7 +211,7 @@ class GetAddAlertTemplates(APIView):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetUpdateDeleteAlertTemplate(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, ManageAlertsPerms]
 | 
			
		||||
    permission_classes = [IsAuthenticated, AlertTemplatePerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request, pk):
 | 
			
		||||
        alert_template = get_object_or_404(AlertTemplate, pk=pk)
 | 
			
		||||
@@ -243,6 +242,8 @@ class GetUpdateDeleteAlertTemplate(APIView):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RelatedAlertTemplate(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, AlertTemplatePerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request, pk):
 | 
			
		||||
        alert_template = get_object_or_404(AlertTemplate, pk=pk)
 | 
			
		||||
        return Response(AlertTemplateRelationSerializer(alert_template).data)
 | 
			
		||||
 
 | 
			
		||||
@@ -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")
 | 
			
		||||
@@ -350,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)
 | 
			
		||||
@@ -371,38 +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
 | 
			
		||||
                    if task.collector_all_output
 | 
			
		||||
                    else 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:
 | 
			
		||||
@@ -419,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")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -518,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(
 | 
			
		||||
@@ -622,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),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -33,7 +33,7 @@ class Policy(BaseAuditModel):
 | 
			
		||||
 | 
			
		||||
        # 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):
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,6 @@ from tacticalrmm.permissions import _has_perm
 | 
			
		||||
class AutomationPolicyPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        if r.method == "GET":
 | 
			
		||||
            return True
 | 
			
		||||
 | 
			
		||||
        return _has_perm(r, "can_manage_automation_policies")
 | 
			
		||||
            return _has_perm(r, "can_list_automation_policies")
 | 
			
		||||
        else:
 | 
			
		||||
            return _has_perm(r, "can_manage_automation_policies")
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ from agents.serializers import AgentHostnameSerializer
 | 
			
		||||
from autotasks.models import AutomatedTask
 | 
			
		||||
from checks.models import Check
 | 
			
		||||
from clients.models import Client
 | 
			
		||||
from clients.serializers import ClientSerializer, SiteSerializer
 | 
			
		||||
from clients.serializers import ClientMinimumSerializer, SiteMinimumSerializer
 | 
			
		||||
from winupdate.serializers import WinUpdatePolicySerializer
 | 
			
		||||
 | 
			
		||||
from .models import Policy
 | 
			
		||||
@@ -21,25 +21,70 @@ class PolicySerializer(ModelSerializer):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PolicyTableSerializer(ModelSerializer):
 | 
			
		||||
 | 
			
		||||
    default_server_policy = ReadOnlyField(source="is_default_server_policy")
 | 
			
		||||
    default_workstation_policy = ReadOnlyField(source="is_default_workstation_policy")
 | 
			
		||||
    agents_count = SerializerMethodField(read_only=True)
 | 
			
		||||
    winupdatepolicy = WinUpdatePolicySerializer(many=True, read_only=True)
 | 
			
		||||
    alert_template = ReadOnlyField(source="alert_template.id")
 | 
			
		||||
    excluded_clients = ClientSerializer(many=True)
 | 
			
		||||
    excluded_sites = SiteSerializer(many=True)
 | 
			
		||||
    excluded_agents = AgentHostnameSerializer(many=True)
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Policy
 | 
			
		||||
        fields = "__all__"
 | 
			
		||||
        depth = 1
 | 
			
		||||
 | 
			
		||||
    def get_agents_count(self, policy):
 | 
			
		||||
        return policy.related_agents().count()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PolicyRelatedSerializer(ModelSerializer):
 | 
			
		||||
    workstation_clients = SerializerMethodField()
 | 
			
		||||
    server_clients = SerializerMethodField()
 | 
			
		||||
    workstation_sites = SerializerMethodField()
 | 
			
		||||
    server_sites = SerializerMethodField()
 | 
			
		||||
    agents = SerializerMethodField()
 | 
			
		||||
 | 
			
		||||
    def get_agents(self, policy):
 | 
			
		||||
        return AgentHostnameSerializer(
 | 
			
		||||
            policy.agents.filter_by_role(self.context["user"]).only(
 | 
			
		||||
                "agent_id", "hostname"
 | 
			
		||||
            ),
 | 
			
		||||
            many=True,
 | 
			
		||||
        ).data
 | 
			
		||||
 | 
			
		||||
    def get_workstation_clients(self, policy):
 | 
			
		||||
        return ClientMinimumSerializer(
 | 
			
		||||
            policy.workstation_clients.filter_by_role(self.context["user"]), many=True
 | 
			
		||||
        ).data
 | 
			
		||||
 | 
			
		||||
    def get_server_clients(self, policy):
 | 
			
		||||
        return ClientMinimumSerializer(
 | 
			
		||||
            policy.server_clients.filter_by_role(self.context["user"]), many=True
 | 
			
		||||
        ).data
 | 
			
		||||
 | 
			
		||||
    def get_workstation_sites(self, policy):
 | 
			
		||||
        return SiteMinimumSerializer(
 | 
			
		||||
            policy.workstation_sites.filter_by_role(self.context["user"]), many=True
 | 
			
		||||
        ).data
 | 
			
		||||
 | 
			
		||||
    def get_server_sites(self, policy):
 | 
			
		||||
        return SiteMinimumSerializer(
 | 
			
		||||
            policy.server_sites.filter_by_role(self.context["user"]), many=True
 | 
			
		||||
        ).data
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Policy
 | 
			
		||||
        fields = (
 | 
			
		||||
            "pk",
 | 
			
		||||
            "name",
 | 
			
		||||
            "workstation_clients",
 | 
			
		||||
            "workstation_sites",
 | 
			
		||||
            "server_clients",
 | 
			
		||||
            "server_sites",
 | 
			
		||||
            "agents",
 | 
			
		||||
            "is_default_server_policy",
 | 
			
		||||
            "is_default_workstation_policy",
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PolicyOverviewSerializer(ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Client
 | 
			
		||||
@@ -48,7 +93,6 @@ class PolicyOverviewSerializer(ModelSerializer):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PolicyCheckStatusSerializer(ModelSerializer):
 | 
			
		||||
 | 
			
		||||
    hostname = ReadOnlyField(source="agent.hostname")
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
@@ -57,7 +101,6 @@ class PolicyCheckStatusSerializer(ModelSerializer):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PolicyTaskStatusSerializer(ModelSerializer):
 | 
			
		||||
 | 
			
		||||
    hostname = ReadOnlyField(source="agent.hostname")
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
@@ -65,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
 | 
			
		||||
 
 | 
			
		||||
@@ -8,12 +8,9 @@ from tacticalrmm.test import TacticalTestCase
 | 
			
		||||
from winupdate.models import WinUpdatePolicy
 | 
			
		||||
 | 
			
		||||
from .serializers import (
 | 
			
		||||
    AutoTasksFieldSerializer,
 | 
			
		||||
    PolicyCheckSerializer,
 | 
			
		||||
    PolicyCheckStatusSerializer,
 | 
			
		||||
    PolicyOverviewSerializer,
 | 
			
		||||
    PolicySerializer,
 | 
			
		||||
    PolicyTableSerializer,
 | 
			
		||||
    PolicyTaskStatusSerializer,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -26,12 +23,10 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
    def test_get_all_policies(self):
 | 
			
		||||
        url = "/automation/policies/"
 | 
			
		||||
 | 
			
		||||
        policies = baker.make("automation.Policy", _quantity=3)
 | 
			
		||||
        baker.make("automation.Policy", _quantity=3)
 | 
			
		||||
        resp = self.client.get(url, format="json")
 | 
			
		||||
        serializer = PolicyTableSerializer(policies, many=True)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertEqual(resp.data, serializer.data)  # type: ignore
 | 
			
		||||
        self.assertEqual(len(resp.data), 3)
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
@@ -181,38 +176,6 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("delete", url)
 | 
			
		||||
 | 
			
		||||
    def test_get_all_policy_tasks(self):
 | 
			
		||||
        # create policy with tasks
 | 
			
		||||
        policy = baker.make("automation.Policy")
 | 
			
		||||
        tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3)
 | 
			
		||||
        url = f"/automation/{policy.pk}/policyautomatedtasks/"  # type: ignore
 | 
			
		||||
 | 
			
		||||
        resp = self.client.get(url, format="json")
 | 
			
		||||
        serializer = AutoTasksFieldSerializer(tasks, many=True)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertEqual(resp.data, serializer.data)  # type: ignore
 | 
			
		||||
        self.assertEqual(len(resp.data), 3)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
    def test_get_all_policy_checks(self):
 | 
			
		||||
 | 
			
		||||
        # setup data
 | 
			
		||||
        policy = baker.make("automation.Policy")
 | 
			
		||||
        checks = self.create_checks(policy=policy)
 | 
			
		||||
 | 
			
		||||
        url = f"/automation/{policy.pk}/policychecks/"  # type: ignore
 | 
			
		||||
 | 
			
		||||
        resp = self.client.get(url, format="json")
 | 
			
		||||
        serializer = PolicyCheckSerializer(checks, many=True)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertEqual(resp.data, serializer.data)  # type: ignore
 | 
			
		||||
        self.assertEqual(len(resp.data), 7)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
    def test_get_policy_check_status(self):
 | 
			
		||||
        # setup data
 | 
			
		||||
        site = baker.make("clients.Site")
 | 
			
		||||
@@ -225,14 +188,14 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
            managed_by_policy=True,
 | 
			
		||||
            parent_check=policy_diskcheck.pk,
 | 
			
		||||
        )
 | 
			
		||||
        url = f"/automation/policycheckstatus/{policy_diskcheck.pk}/check/"
 | 
			
		||||
        url = f"/automation/checks/{policy_diskcheck.pk}/status/"
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, format="json")
 | 
			
		||||
        resp = self.client.get(url, format="json")
 | 
			
		||||
        serializer = PolicyCheckStatusSerializer([managed_check], many=True)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertEqual(resp.data, serializer.data)  # type: ignore
 | 
			
		||||
        self.check_not_authenticated("patch", url)
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
    def test_policy_overview(self):
 | 
			
		||||
        from clients.models import Client
 | 
			
		||||
@@ -292,15 +255,15 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
            "autotasks.AutomatedTask", parent_task=task.id, _quantity=5  # type: ignore
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        url = f"/automation/policyautomatedtaskstatus/{task.id}/task/"  # type: ignore
 | 
			
		||||
        url = f"/automation/tasks/{task.id}/status/"  # type: ignore
 | 
			
		||||
 | 
			
		||||
        serializer = PolicyTaskStatusSerializer(policy_tasks, many=True)
 | 
			
		||||
        resp = self.client.patch(url, format="json")
 | 
			
		||||
        resp = self.client.get(url, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertEqual(resp.data, serializer.data)  # type: ignore
 | 
			
		||||
        self.assertEqual(len(resp.data), 5)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("patch", url)
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
    @patch("automation.tasks.run_win_policy_autotasks_task.delay")
 | 
			
		||||
    def test_run_win_task(self, mock_task):
 | 
			
		||||
@@ -313,16 +276,16 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
            _quantity=6,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        url = "/automation/runwintask/1/"
 | 
			
		||||
        resp = self.client.put(url, format="json")
 | 
			
		||||
        url = "/automation/tasks/1/run/"
 | 
			
		||||
        resp = self.client.post(url, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        mock_task.assert_called()  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("put", url)
 | 
			
		||||
        self.check_not_authenticated("post", url)
 | 
			
		||||
 | 
			
		||||
    def test_create_new_patch_policy(self):
 | 
			
		||||
        url = "/automation/winupdatepolicy/"
 | 
			
		||||
        url = "/automation/patchpolicy/"
 | 
			
		||||
 | 
			
		||||
        # test policy doesn't exist
 | 
			
		||||
        data = {"policy": 500}
 | 
			
		||||
@@ -353,15 +316,14 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
    def test_update_patch_policy(self):
 | 
			
		||||
 | 
			
		||||
        # test policy doesn't exist
 | 
			
		||||
        resp = self.client.put("/automation/winupdatepolicy/500/", format="json")
 | 
			
		||||
        resp = self.client.put("/automation/patchpolicy/500/", format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 404)
 | 
			
		||||
 | 
			
		||||
        policy = baker.make("automation.Policy")
 | 
			
		||||
        patch_policy = baker.make("winupdate.WinUpdatePolicy", policy=policy)
 | 
			
		||||
        url = f"/automation/winupdatepolicy/{patch_policy.pk}/"  # type: ignore
 | 
			
		||||
        url = f"/automation/patchpolicy/{patch_policy.pk}/"  # type: ignore
 | 
			
		||||
 | 
			
		||||
        data = {
 | 
			
		||||
            "id": patch_policy.pk,  # type: ignore
 | 
			
		||||
            "policy": policy.pk,  # type: ignore
 | 
			
		||||
            "critical": "approve",
 | 
			
		||||
            "important": "approve",
 | 
			
		||||
@@ -377,7 +339,7 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
        self.check_not_authenticated("put", url)
 | 
			
		||||
 | 
			
		||||
    def test_reset_patch_policy(self):
 | 
			
		||||
        url = "/automation/winupdatepolicy/reset/"
 | 
			
		||||
        url = "/automation/patchpolicy/reset/"
 | 
			
		||||
 | 
			
		||||
        inherit_fields = {
 | 
			
		||||
            "critical": "inherit",
 | 
			
		||||
@@ -406,7 +368,7 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
        # test reset agents in site
 | 
			
		||||
        data = {"site": sites[0].id}  # type: ignore
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        resp = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        agents = Agent.objects.filter(site=sites[0])  # type: ignore
 | 
			
		||||
@@ -418,7 +380,7 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
        # test reset agents in client
 | 
			
		||||
        data = {"client": clients[1].id}  # type: ignore
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        resp = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        agents = Agent.objects.filter(site__client=clients[1])  # type: ignore
 | 
			
		||||
@@ -430,7 +392,7 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
        # test reset all agents
 | 
			
		||||
        data = {}
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        resp = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        agents = Agent.objects.all()
 | 
			
		||||
@@ -438,17 +400,17 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
            for k, v in inherit_fields.items():
 | 
			
		||||
                self.assertEqual(getattr(agent.winupdatepolicy.get(), k), v)
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("patch", url)
 | 
			
		||||
        self.check_not_authenticated("post", url)
 | 
			
		||||
 | 
			
		||||
    def test_delete_patch_policy(self):
 | 
			
		||||
        # test patch policy doesn't exist
 | 
			
		||||
        resp = self.client.delete("/automation/winupdatepolicy/500/", format="json")
 | 
			
		||||
        resp = self.client.delete("/automation/patchpolicy/500/", format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 404)
 | 
			
		||||
 | 
			
		||||
        winupdate_policy = baker.make_recipe(
 | 
			
		||||
            "winupdate.winupdate_policy", policy__name="Test Policy"
 | 
			
		||||
        )
 | 
			
		||||
        url = f"/automation/winupdatepolicy/{winupdate_policy.pk}/"
 | 
			
		||||
        url = f"/automation/patchpolicy/{winupdate_policy.pk}/"
 | 
			
		||||
 | 
			
		||||
        resp = self.client.delete(url, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
@@ -503,7 +465,7 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        # Add Client to Policy
 | 
			
		||||
        policy.server_clients.add(server_agents[13].client)  # type: ignore
 | 
			
		||||
        policy.workstation_clients.add(workstation_agents[15].client)  # type: ignore
 | 
			
		||||
        policy.workstation_clients.add(workstation_agents[13].client)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        resp = self.client.get(
 | 
			
		||||
            f"/automation/policies/{policy.pk}/related/", format="json"  # type: ignore
 | 
			
		||||
@@ -511,22 +473,28 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertEquals(len(resp.data["server_clients"]), 1)  # type: ignore
 | 
			
		||||
        self.assertEquals(len(resp.data["server_sites"]), 5)  # type: ignore
 | 
			
		||||
        self.assertEquals(len(resp.data["server_sites"]), 0)  # type: ignore
 | 
			
		||||
        self.assertEquals(len(resp.data["workstation_clients"]), 1)  # type: ignore
 | 
			
		||||
        self.assertEquals(len(resp.data["workstation_sites"]), 5)  # type: ignore
 | 
			
		||||
        self.assertEquals(len(resp.data["agents"]), 10)  # type: ignore
 | 
			
		||||
        self.assertEquals(len(resp.data["workstation_sites"]), 0)  # type: ignore
 | 
			
		||||
        self.assertEquals(len(resp.data["agents"]), 0)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # Add Site to Policy and the agents and sites length shouldn't change
 | 
			
		||||
        policy.server_sites.add(server_agents[13].site)  # type: ignore
 | 
			
		||||
        policy.workstation_sites.add(workstation_agents[15].site)  # type: ignore
 | 
			
		||||
        self.assertEquals(len(resp.data["server_sites"]), 5)  # type: ignore
 | 
			
		||||
        self.assertEquals(len(resp.data["workstation_sites"]), 5)  # type: ignore
 | 
			
		||||
        self.assertEquals(len(resp.data["agents"]), 10)  # type: ignore
 | 
			
		||||
        # Add Site to Policy
 | 
			
		||||
        policy.server_sites.add(server_agents[10].site)  # type: ignore
 | 
			
		||||
        policy.workstation_sites.add(workstation_agents[10].site)  # type: ignore
 | 
			
		||||
        resp = self.client.get(
 | 
			
		||||
            f"/automation/policies/{policy.pk}/related/", format="json"  # type: ignore
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEquals(len(resp.data["server_sites"]), 1)  # type: ignore
 | 
			
		||||
        self.assertEquals(len(resp.data["workstation_sites"]), 1)  # type: ignore
 | 
			
		||||
        self.assertEquals(len(resp.data["agents"]), 0)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # Add Agent to Policy and the agents length shouldn't change
 | 
			
		||||
        policy.agents.add(server_agents[13])  # type: ignore
 | 
			
		||||
        policy.agents.add(workstation_agents[15])  # type: ignore
 | 
			
		||||
        self.assertEquals(len(resp.data["agents"]), 10)  # type: ignore
 | 
			
		||||
        # Add Agent to Policy
 | 
			
		||||
        policy.agents.add(server_agents[2])  # type: ignore
 | 
			
		||||
        policy.agents.add(workstation_agents[2])  # type: ignore
 | 
			
		||||
        resp = self.client.get(
 | 
			
		||||
            f"/automation/policies/{policy.pk}/related/", format="json"  # type: ignore
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEquals(len(resp.data["agents"]), 2)  # type: ignore
 | 
			
		||||
 | 
			
		||||
    def test_generating_agent_policy_checks(self):
 | 
			
		||||
        from .tasks import generate_agent_checks_task
 | 
			
		||||
@@ -918,11 +886,13 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
    @patch("autotasks.models.AutomatedTask.create_task_on_agent")
 | 
			
		||||
    @patch("autotasks.models.AutomatedTask.delete_task_on_agent")
 | 
			
		||||
    def test_delete_policy_tasks(self, delete_task_on_agent, create_task):
 | 
			
		||||
        from .tasks import delete_policy_autotasks_task
 | 
			
		||||
        from .tasks import delete_policy_autotasks_task, generate_agent_checks_task
 | 
			
		||||
 | 
			
		||||
        policy = baker.make("automation.Policy", active=True)
 | 
			
		||||
        tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3)
 | 
			
		||||
        baker.make_recipe("agents.server_agent", policy=policy)
 | 
			
		||||
        agent = baker.make_recipe("agents.server_agent", policy=policy)
 | 
			
		||||
 | 
			
		||||
        generate_agent_checks_task(agents=[agent.pk], create_tasks=True)
 | 
			
		||||
 | 
			
		||||
        delete_policy_autotasks_task(task=tasks[0].id)  # type: ignore
 | 
			
		||||
 | 
			
		||||
@@ -931,11 +901,13 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
    @patch("autotasks.models.AutomatedTask.create_task_on_agent")
 | 
			
		||||
    @patch("autotasks.models.AutomatedTask.run_win_task")
 | 
			
		||||
    def test_run_policy_task(self, run_win_task, create_task):
 | 
			
		||||
        from .tasks import run_win_policy_autotasks_task
 | 
			
		||||
        from .tasks import run_win_policy_autotasks_task, generate_agent_checks_task
 | 
			
		||||
 | 
			
		||||
        policy = baker.make("automation.Policy", active=True)
 | 
			
		||||
        tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3)
 | 
			
		||||
        baker.make_recipe("agents.server_agent", policy=policy)
 | 
			
		||||
        agent = baker.make_recipe("agents.server_agent", policy=policy)
 | 
			
		||||
 | 
			
		||||
        generate_agent_checks_task(agents=[agent.pk], create_tasks=True)
 | 
			
		||||
 | 
			
		||||
        run_win_policy_autotasks_task(task=tasks[0].id)  # type: ignore
 | 
			
		||||
 | 
			
		||||
@@ -944,7 +916,10 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
    @patch("autotasks.models.AutomatedTask.create_task_on_agent")
 | 
			
		||||
    @patch("autotasks.models.AutomatedTask.modify_task_on_agent")
 | 
			
		||||
    def test_update_policy_tasks(self, modify_task_on_agent, create_task):
 | 
			
		||||
        from .tasks import update_policy_autotasks_fields_task
 | 
			
		||||
        from .tasks import (
 | 
			
		||||
            update_policy_autotasks_fields_task,
 | 
			
		||||
            generate_agent_checks_task,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # setup data
 | 
			
		||||
        policy = baker.make("automation.Policy", active=True)
 | 
			
		||||
@@ -956,6 +931,8 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
        )
 | 
			
		||||
        agent = baker.make_recipe("agents.server_agent", policy=policy)
 | 
			
		||||
 | 
			
		||||
        generate_agent_checks_task(agents=[agent.pk], create_tasks=True)
 | 
			
		||||
 | 
			
		||||
        tasks[0].enabled = False  # type: ignore
 | 
			
		||||
        tasks[0].save()  # type: ignore
 | 
			
		||||
 | 
			
		||||
@@ -995,6 +972,8 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
    @patch("autotasks.models.AutomatedTask.create_task_on_agent")
 | 
			
		||||
    def test_policy_exclusions(self, create_task):
 | 
			
		||||
        from .tasks import generate_agent_checks_task
 | 
			
		||||
 | 
			
		||||
        # setup data
 | 
			
		||||
        policy = baker.make("automation.Policy", active=True)
 | 
			
		||||
        baker.make_recipe("checks.memory_check", policy=policy)
 | 
			
		||||
@@ -1003,6 +982,8 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
            "agents.agent", policy=policy, monitoring_type="server"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        generate_agent_checks_task(agents=[agent.pk], create_tasks=True)
 | 
			
		||||
 | 
			
		||||
        # make sure related agents on policy returns correctly
 | 
			
		||||
        self.assertEqual(policy.related_agents().count(), 1)  # type: ignore
 | 
			
		||||
        self.assertEqual(agent.agentchecks.count(), 1)  # type: ignore
 | 
			
		||||
@@ -1164,3 +1145,9 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
        # should get policies from agent policy
 | 
			
		||||
        self.assertTrue(agent.autotasks.all())
 | 
			
		||||
        self.assertTrue(agent.agentchecks.all())
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestAutomationPermission(TacticalTestCase):
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        self.client_setup()
 | 
			
		||||
        self.setup_coresettings()
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,8 @@
 | 
			
		||||
from django.urls import path
 | 
			
		||||
 | 
			
		||||
from . import views
 | 
			
		||||
from checks.views import GetAddChecks
 | 
			
		||||
from autotasks.views import GetAddAutoTasks
 | 
			
		||||
 | 
			
		||||
urlpatterns = [
 | 
			
		||||
    path("policies/", views.GetAddPolicies.as_view()),
 | 
			
		||||
@@ -8,12 +10,14 @@ urlpatterns = [
 | 
			
		||||
    path("policies/overview/", views.OverviewPolicy.as_view()),
 | 
			
		||||
    path("policies/<int:pk>/", views.GetUpdateDeletePolicy.as_view()),
 | 
			
		||||
    path("sync/", views.PolicySync.as_view()),
 | 
			
		||||
    path("<int:pk>/policychecks/", views.PolicyCheck.as_view()),
 | 
			
		||||
    path("<int:pk>/policyautomatedtasks/", views.PolicyAutoTask.as_view()),
 | 
			
		||||
    path("policycheckstatus/<int:check>/check/", views.PolicyCheck.as_view()),
 | 
			
		||||
    path("policyautomatedtaskstatus/<int:task>/task/", views.PolicyAutoTask.as_view()),
 | 
			
		||||
    path("runwintask/<int:task>/", views.PolicyAutoTask.as_view()),
 | 
			
		||||
    path("winupdatepolicy/", views.UpdatePatchPolicy.as_view()),
 | 
			
		||||
    path("winupdatepolicy/<int:patchpolicy>/", views.UpdatePatchPolicy.as_view()),
 | 
			
		||||
    path("winupdatepolicy/reset/", views.UpdatePatchPolicy.as_view()),
 | 
			
		||||
    # alias to get policy checks
 | 
			
		||||
    path("policies/<int:policy>/checks/", GetAddChecks.as_view()),
 | 
			
		||||
    # alias to get policy tasks
 | 
			
		||||
    path("policies/<int:policy>/tasks/", GetAddAutoTasks.as_view()),
 | 
			
		||||
    path("checks/<int:check>/status/", views.PolicyCheck.as_view()),
 | 
			
		||||
    path("tasks/<int:task>/status/", views.PolicyAutoTask.as_view()),
 | 
			
		||||
    path("tasks/<int:task>/run/", views.PolicyAutoTask.as_view()),
 | 
			
		||||
    path("patchpolicy/", views.UpdatePatchPolicy.as_view()),
 | 
			
		||||
    path("patchpolicy/<int:pk>/", views.UpdatePatchPolicy.as_view()),
 | 
			
		||||
    path("patchpolicy/reset/", views.ResetPatchPolicy.as_view()),
 | 
			
		||||
]
 | 
			
		||||
 
 | 
			
		||||
@@ -1,23 +1,22 @@
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from agents.serializers import AgentHostnameSerializer
 | 
			
		||||
from autotasks.models import AutomatedTask
 | 
			
		||||
from checks.models import Check
 | 
			
		||||
from clients.models import Client
 | 
			
		||||
from clients.serializers import ClientSerializer, SiteSerializer
 | 
			
		||||
from django.shortcuts import get_object_or_404
 | 
			
		||||
from rest_framework.permissions import IsAuthenticated
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
from rest_framework.views import APIView
 | 
			
		||||
from rest_framework.exceptions import PermissionDenied
 | 
			
		||||
from tacticalrmm.utils import notify_error
 | 
			
		||||
from tacticalrmm.permissions import _has_perm_on_client, _has_perm_on_site
 | 
			
		||||
from winupdate.models import WinUpdatePolicy
 | 
			
		||||
from winupdate.serializers import WinUpdatePolicySerializer
 | 
			
		||||
 | 
			
		||||
from .models import Policy
 | 
			
		||||
from .permissions import AutomationPolicyPerms
 | 
			
		||||
from .serializers import (
 | 
			
		||||
    AutoTasksFieldSerializer,
 | 
			
		||||
    PolicyCheckSerializer,
 | 
			
		||||
    PolicyCheckStatusSerializer,
 | 
			
		||||
    PolicyRelatedSerializer,
 | 
			
		||||
    PolicyOverviewSerializer,
 | 
			
		||||
    PolicySerializer,
 | 
			
		||||
    PolicyTableSerializer,
 | 
			
		||||
@@ -31,7 +30,11 @@ class GetAddPolicies(APIView):
 | 
			
		||||
    def get(self, request):
 | 
			
		||||
        policies = Policy.objects.all()
 | 
			
		||||
 | 
			
		||||
        return Response(PolicyTableSerializer(policies, many=True).data)
 | 
			
		||||
        return Response(
 | 
			
		||||
            PolicyTableSerializer(
 | 
			
		||||
                policies, context={"user": request.user}, many=True
 | 
			
		||||
            ).data
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
        serializer = PolicySerializer(data=request.data, partial=True)
 | 
			
		||||
@@ -102,19 +105,14 @@ class PolicySync(APIView):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PolicyAutoTask(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, AutomationPolicyPerms]
 | 
			
		||||
    # tasks associated with policy
 | 
			
		||||
    def get(self, request, pk):
 | 
			
		||||
        tasks = AutomatedTask.objects.filter(policy=pk)
 | 
			
		||||
        return Response(AutoTasksFieldSerializer(tasks, many=True).data)
 | 
			
		||||
 | 
			
		||||
    # get status of all tasks
 | 
			
		||||
    def patch(self, request, task):
 | 
			
		||||
    def get(self, request, task):
 | 
			
		||||
        tasks = AutomatedTask.objects.filter(parent_task=task)
 | 
			
		||||
        return Response(PolicyTaskStatusSerializer(tasks, many=True).data)
 | 
			
		||||
 | 
			
		||||
    # bulk run win tasks associated with policy
 | 
			
		||||
    def put(self, request, task):
 | 
			
		||||
    def post(self, request, task):
 | 
			
		||||
        from .tasks import run_win_policy_autotasks_task
 | 
			
		||||
 | 
			
		||||
        run_win_policy_autotasks_task.delay(task=task)
 | 
			
		||||
@@ -124,11 +122,7 @@ class PolicyAutoTask(APIView):
 | 
			
		||||
class PolicyCheck(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, AutomationPolicyPerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request, pk):
 | 
			
		||||
        checks = Check.objects.filter(policy__pk=pk, agent=None)
 | 
			
		||||
        return Response(PolicyCheckSerializer(checks, many=True).data)
 | 
			
		||||
 | 
			
		||||
    def patch(self, request, check):
 | 
			
		||||
    def get(self, request, check):
 | 
			
		||||
        checks = Check.objects.filter(parent_check=check)
 | 
			
		||||
        return Response(PolicyCheckStatusSerializer(checks, many=True).data)
 | 
			
		||||
 | 
			
		||||
@@ -143,8 +137,6 @@ class OverviewPolicy(APIView):
 | 
			
		||||
class GetRelated(APIView):
 | 
			
		||||
    def get(self, request, pk):
 | 
			
		||||
 | 
			
		||||
        response = {}
 | 
			
		||||
 | 
			
		||||
        policy = (
 | 
			
		||||
            Policy.objects.filter(pk=pk)
 | 
			
		||||
            .prefetch_related(
 | 
			
		||||
@@ -156,43 +148,9 @@ class GetRelated(APIView):
 | 
			
		||||
            .first()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        response["default_server_policy"] = policy.is_default_server_policy
 | 
			
		||||
        response["default_workstation_policy"] = policy.is_default_workstation_policy
 | 
			
		||||
 | 
			
		||||
        response["server_clients"] = ClientSerializer(
 | 
			
		||||
            policy.server_clients.all(), many=True
 | 
			
		||||
        ).data
 | 
			
		||||
        response["workstation_clients"] = ClientSerializer(
 | 
			
		||||
            policy.workstation_clients.all(), many=True
 | 
			
		||||
        ).data
 | 
			
		||||
 | 
			
		||||
        filtered_server_sites = list()
 | 
			
		||||
        filtered_workstation_sites = list()
 | 
			
		||||
 | 
			
		||||
        for client in policy.server_clients.all():
 | 
			
		||||
            for site in client.sites.all():
 | 
			
		||||
                if site not in policy.server_sites.all():
 | 
			
		||||
                    filtered_server_sites.append(site)
 | 
			
		||||
 | 
			
		||||
        response["server_sites"] = SiteSerializer(
 | 
			
		||||
            filtered_server_sites + list(policy.server_sites.all()), many=True
 | 
			
		||||
        ).data
 | 
			
		||||
 | 
			
		||||
        for client in policy.workstation_clients.all():
 | 
			
		||||
            for site in client.sites.all():
 | 
			
		||||
                if site not in policy.workstation_sites.all():
 | 
			
		||||
                    filtered_workstation_sites.append(site)
 | 
			
		||||
 | 
			
		||||
        response["workstation_sites"] = SiteSerializer(
 | 
			
		||||
            filtered_workstation_sites + list(policy.workstation_sites.all()), many=True
 | 
			
		||||
        ).data
 | 
			
		||||
 | 
			
		||||
        response["agents"] = AgentHostnameSerializer(
 | 
			
		||||
            policy.related_agents().only("pk", "hostname"),
 | 
			
		||||
            many=True,
 | 
			
		||||
        ).data
 | 
			
		||||
 | 
			
		||||
        return Response(response)
 | 
			
		||||
        return Response(
 | 
			
		||||
            PolicyRelatedSerializer(policy, context={"user": request.user}).data
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UpdatePatchPolicy(APIView):
 | 
			
		||||
@@ -209,8 +167,8 @@ class UpdatePatchPolicy(APIView):
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
    # update patch policy
 | 
			
		||||
    def put(self, request, patchpolicy):
 | 
			
		||||
        policy = get_object_or_404(WinUpdatePolicy, pk=patchpolicy)
 | 
			
		||||
    def put(self, request, pk):
 | 
			
		||||
        policy = get_object_or_404(WinUpdatePolicy, pk=pk)
 | 
			
		||||
 | 
			
		||||
        serializer = WinUpdatePolicySerializer(
 | 
			
		||||
            instance=policy, data=request.data, partial=True
 | 
			
		||||
@@ -220,20 +178,41 @@ class UpdatePatchPolicy(APIView):
 | 
			
		||||
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
    # bulk reset agent patch policy
 | 
			
		||||
    def patch(self, request):
 | 
			
		||||
    # delete patch policy
 | 
			
		||||
    def delete(self, request, pk):
 | 
			
		||||
        get_object_or_404(WinUpdatePolicy, pk=pk).delete()
 | 
			
		||||
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ResetPatchPolicy(APIView):
 | 
			
		||||
    # bulk reset agent patch policy
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
 | 
			
		||||
        agents = None
 | 
			
		||||
        if "client" in request.data:
 | 
			
		||||
            agents = Agent.objects.prefetch_related("winupdatepolicy").filter(
 | 
			
		||||
                site__client_id=request.data["client"]
 | 
			
		||||
            if not _has_perm_on_client(request.user, request.data["client"]):
 | 
			
		||||
                raise PermissionDenied()
 | 
			
		||||
 | 
			
		||||
            agents = (
 | 
			
		||||
                Agent.objects.filter_by_role(request.user)
 | 
			
		||||
                .prefetch_related("winupdatepolicy")
 | 
			
		||||
                .filter(site__client_id=request.data["client"])
 | 
			
		||||
            )
 | 
			
		||||
        elif "site" in request.data:
 | 
			
		||||
            agents = Agent.objects.prefetch_related("winupdatepolicy").filter(
 | 
			
		||||
                site_id=request.data["site"]
 | 
			
		||||
            if not _has_perm_on_site(request.user, request.data["site"]):
 | 
			
		||||
                raise PermissionDenied()
 | 
			
		||||
 | 
			
		||||
            agents = (
 | 
			
		||||
                Agent.objects.filter_by_role(request.user)
 | 
			
		||||
                .prefetch_related("winupdatepolicy")
 | 
			
		||||
                .filter(site_id=request.data["site"])
 | 
			
		||||
            )
 | 
			
		||||
        else:
 | 
			
		||||
            agents = Agent.objects.prefetch_related("winupdatepolicy").only("pk")
 | 
			
		||||
            agents = (
 | 
			
		||||
                Agent.objects.filter_by_role(request.user)
 | 
			
		||||
                .prefetch_related("winupdatepolicy")
 | 
			
		||||
                .only("pk")
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        for agent in agents:
 | 
			
		||||
            winupdatepolicy = agent.winupdatepolicy.get()
 | 
			
		||||
@@ -258,10 +237,4 @@ class UpdatePatchPolicy(APIView):
 | 
			
		||||
                ]
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
    # delete patch policy
 | 
			
		||||
    def delete(self, request, patchpolicy):
 | 
			
		||||
        get_object_or_404(WinUpdatePolicy, pk=patchpolicy).delete()
 | 
			
		||||
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
        return Response("The patch policy on the affected agents has been reset.")
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,23 @@
 | 
			
		||||
# Generated by Django 3.2.6 on 2021-09-17 19:54
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("autotasks", "0022_automatedtask_collector_all_output"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="automatedtask",
 | 
			
		||||
            name="created_by",
 | 
			
		||||
            field=models.CharField(blank=True, max_length=255, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="automatedtask",
 | 
			
		||||
            name="modified_by",
 | 
			
		||||
            field=models.CharField(blank=True, max_length=255, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -6,19 +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"),
 | 
			
		||||
@@ -51,6 +48,8 @@ TASK_STATUS_CHOICES = [
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AutomatedTask(BaseAuditModel):
 | 
			
		||||
    objects = PermissionQuerySet.as_manager()
 | 
			
		||||
 | 
			
		||||
    agent = models.ForeignKey(
 | 
			
		||||
        "agents.Agent",
 | 
			
		||||
        related_name="autotasks",
 | 
			
		||||
@@ -136,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":
 | 
			
		||||
@@ -195,12 +219,20 @@ 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, 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
 | 
			
		||||
@@ -254,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
 | 
			
		||||
            )
 | 
			
		||||
@@ -280,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
 | 
			
		||||
@@ -301,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"
 | 
			
		||||
 | 
			
		||||
@@ -333,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"
 | 
			
		||||
 | 
			
		||||
@@ -362,7 +406,7 @@ 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"
 | 
			
		||||
@@ -372,13 +416,19 @@ class AutomatedTask(BaseAuditModel):
 | 
			
		||||
            except DatabaseError:
 | 
			
		||||
                pass
 | 
			
		||||
 | 
			
		||||
            logger.warning(
 | 
			
		||||
                f"{agent.hostname} task {self.name} will be deleted on next checkin"
 | 
			
		||||
            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"
 | 
			
		||||
 | 
			
		||||
@@ -391,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
 | 
			
		||||
@@ -413,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"
 | 
			
		||||
 | 
			
		||||
@@ -424,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"
 | 
			
		||||
 | 
			
		||||
@@ -442,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
 | 
			
		||||
@@ -454,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
 | 
			
		||||
@@ -465,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
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,19 @@
 | 
			
		||||
from rest_framework import permissions
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.permissions import _has_perm
 | 
			
		||||
from tacticalrmm.permissions import _has_perm, _has_perm_on_agent
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ManageAutoTaskPerms(permissions.BasePermission):
 | 
			
		||||
class AutoTaskPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        if r.method == "GET":
 | 
			
		||||
            return True
 | 
			
		||||
 | 
			
		||||
        return _has_perm(r, "can_manage_autotasks")
 | 
			
		||||
            if "agent_id" in view.kwargs.keys():
 | 
			
		||||
                return _has_perm(r, "can_list_autotasks") and _has_perm_on_agent(
 | 
			
		||||
                    r.user, view.kwargs["agent_id"]
 | 
			
		||||
                )
 | 
			
		||||
            else:
 | 
			
		||||
                return _has_perm(r, "can_list_autotasks")
 | 
			
		||||
        else:
 | 
			
		||||
            return _has_perm(r, "can_manage_autotasks")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RunAutoTaskPerms(permissions.BasePermission):
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,7 @@ from .models import AutomatedTask
 | 
			
		||||
 | 
			
		||||
class TaskSerializer(serializers.ModelSerializer):
 | 
			
		||||
 | 
			
		||||
    assigned_check = CheckSerializer(read_only=True)
 | 
			
		||||
    check_name = serializers.ReadOnlyField(source="assigned_check.readable_desc")
 | 
			
		||||
    schedule = serializers.ReadOnlyField()
 | 
			
		||||
    last_run = serializers.ReadOnlyField(source="last_run_as_timezone")
 | 
			
		||||
    alert_template = serializers.SerializerMethodField()
 | 
			
		||||
@@ -37,19 +37,6 @@ class TaskSerializer(serializers.ModelSerializer):
 | 
			
		||||
        fields = "__all__"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AutoTaskSerializer(serializers.ModelSerializer):
 | 
			
		||||
 | 
			
		||||
    autotasks = TaskSerializer(many=True, read_only=True)
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Agent
 | 
			
		||||
        fields = (
 | 
			
		||||
            "pk",
 | 
			
		||||
            "hostname",
 | 
			
		||||
            "autotasks",
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# below is for the windows agent
 | 
			
		||||
class TaskRunnerScriptField(serializers.ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
@@ -84,3 +71,9 @@ class TaskRunnerPatchSerializer(serializers.ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = AutomatedTask
 | 
			
		||||
        fields = "__all__"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TaskAuditSerializer(serializers.ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = AutomatedTask
 | 
			
		||||
        fields = "__all__"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,18 +1,16 @@
 | 
			
		||||
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 autotasks.models import AutomatedTask
 | 
			
		||||
from logs.models import DebugLog
 | 
			
		||||
from tacticalrmm.celery import app
 | 
			
		||||
 | 
			
		||||
logger.configure(**settings.LOG_CONFIG)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
def create_win_task_schedule(pk):
 | 
			
		||||
@@ -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,55 +1,59 @@
 | 
			
		||||
from django.shortcuts import get_object_or_404
 | 
			
		||||
from rest_framework.decorators import api_view, permission_classes
 | 
			
		||||
from rest_framework.permissions import IsAuthenticated
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
from rest_framework.views import APIView
 | 
			
		||||
from rest_framework.exceptions import PermissionDenied
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from checks.models import Check
 | 
			
		||||
from scripts.models import Script
 | 
			
		||||
from tacticalrmm.utils import get_bit_days, get_default_timezone, notify_error
 | 
			
		||||
from automation.models import Policy
 | 
			
		||||
from tacticalrmm.utils import get_bit_days
 | 
			
		||||
from tacticalrmm.permissions import _has_perm_on_agent
 | 
			
		||||
 | 
			
		||||
from .models import AutomatedTask
 | 
			
		||||
from .permissions import ManageAutoTaskPerms, RunAutoTaskPerms
 | 
			
		||||
from .serializers import AutoTaskSerializer, TaskSerializer
 | 
			
		||||
from .permissions import AutoTaskPerms, RunAutoTaskPerms
 | 
			
		||||
from .serializers import TaskSerializer
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AddAutoTask(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, ManageAutoTaskPerms]
 | 
			
		||||
class GetAddAutoTasks(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, AutoTaskPerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request, agent_id=None, policy=None):
 | 
			
		||||
 | 
			
		||||
        if agent_id:
 | 
			
		||||
            agent = get_object_or_404(Agent, agent_id=agent_id)
 | 
			
		||||
            tasks = AutomatedTask.objects.filter(agent=agent)
 | 
			
		||||
        elif policy:
 | 
			
		||||
            policy = get_object_or_404(Policy, id=policy)
 | 
			
		||||
            tasks = AutomatedTask.objects.filter(policy=policy)
 | 
			
		||||
        else:
 | 
			
		||||
            tasks = AutomatedTask.objects.filter_by_role(request.user)
 | 
			
		||||
        return Response(TaskSerializer(tasks, many=True).data)
 | 
			
		||||
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
        from automation.models import Policy
 | 
			
		||||
        from automation.tasks import generate_agent_autotasks_task
 | 
			
		||||
        from autotasks.tasks import create_win_task_schedule
 | 
			
		||||
 | 
			
		||||
        data = request.data
 | 
			
		||||
        script = get_object_or_404(Script, pk=data["autotask"]["script"])
 | 
			
		||||
        data = request.data.copy()
 | 
			
		||||
 | 
			
		||||
        # Determine if adding check to Policy or Agent
 | 
			
		||||
        if "policy" in data:
 | 
			
		||||
            policy = get_object_or_404(Policy, id=data["policy"])
 | 
			
		||||
            # Object used for filter and save
 | 
			
		||||
            parent = {"policy": policy}
 | 
			
		||||
        else:
 | 
			
		||||
            agent = get_object_or_404(Agent, pk=data["agent"])
 | 
			
		||||
            parent = {"agent": agent}
 | 
			
		||||
        # Determine if adding to an agent and replace agent_id with pk
 | 
			
		||||
        if "agent" in data.keys():
 | 
			
		||||
            agent = get_object_or_404(Agent, agent_id=data["agent"])
 | 
			
		||||
 | 
			
		||||
        check = None
 | 
			
		||||
        if data["autotask"]["assigned_check"]:
 | 
			
		||||
            check = get_object_or_404(Check, pk=data["autotask"]["assigned_check"])
 | 
			
		||||
            if not _has_perm_on_agent(request.user, agent.agent_id):
 | 
			
		||||
                raise PermissionDenied()
 | 
			
		||||
 | 
			
		||||
            data["agent"] = agent.pk
 | 
			
		||||
 | 
			
		||||
        bit_weekdays = None
 | 
			
		||||
        if data["autotask"]["run_time_days"]:
 | 
			
		||||
            bit_weekdays = get_bit_days(data["autotask"]["run_time_days"])
 | 
			
		||||
        if "run_time_days" in data.keys():
 | 
			
		||||
            if data["run_time_days"]:
 | 
			
		||||
                bit_weekdays = get_bit_days(data["run_time_days"])
 | 
			
		||||
            data.pop("run_time_days")
 | 
			
		||||
 | 
			
		||||
        del data["autotask"]["run_time_days"]
 | 
			
		||||
        serializer = TaskSerializer(data=data["autotask"], partial=True, context=parent)
 | 
			
		||||
        serializer = TaskSerializer(data=data)
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        task = serializer.save(
 | 
			
		||||
            **parent,
 | 
			
		||||
            script=script,
 | 
			
		||||
            win_task_name=AutomatedTask.generate_task_name(),
 | 
			
		||||
            assigned_check=check,
 | 
			
		||||
            run_time_bit_weekdays=bit_weekdays,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@@ -59,58 +63,35 @@ class AddAutoTask(APIView):
 | 
			
		||||
        elif task.policy:
 | 
			
		||||
            generate_agent_autotasks_task.delay(policy=task.policy.pk)
 | 
			
		||||
 | 
			
		||||
        return Response("Task will be created shortly!")
 | 
			
		||||
        return Response(
 | 
			
		||||
            "The task has been created. It will show up on the agent on next checkin"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AutoTask(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, ManageAutoTaskPerms]
 | 
			
		||||
class GetEditDeleteAutoTask(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, AutoTaskPerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request, pk):
 | 
			
		||||
 | 
			
		||||
        agent = get_object_or_404(Agent, pk=pk)
 | 
			
		||||
        ctx = {
 | 
			
		||||
            "default_tz": get_default_timezone(),
 | 
			
		||||
            "agent_tz": agent.time_zone,
 | 
			
		||||
        }
 | 
			
		||||
        return Response(AutoTaskSerializer(agent, context=ctx).data)
 | 
			
		||||
        task = get_object_or_404(AutomatedTask, pk=pk)
 | 
			
		||||
 | 
			
		||||
        if task.agent and not _has_perm_on_agent(request.user, task.agent.agent_id):
 | 
			
		||||
            raise PermissionDenied()
 | 
			
		||||
 | 
			
		||||
        return Response(TaskSerializer(task).data)
 | 
			
		||||
 | 
			
		||||
    def put(self, request, pk):
 | 
			
		||||
        from automation.tasks import update_policy_autotasks_fields_task
 | 
			
		||||
 | 
			
		||||
        task = get_object_or_404(AutomatedTask, pk=pk)
 | 
			
		||||
 | 
			
		||||
        if task.agent and not _has_perm_on_agent(request.user, task.agent.agent_id):
 | 
			
		||||
            raise PermissionDenied()
 | 
			
		||||
 | 
			
		||||
        serializer = TaskSerializer(instance=task, data=request.data, partial=True)
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        serializer.save()
 | 
			
		||||
 | 
			
		||||
        if task.policy:
 | 
			
		||||
            update_policy_autotasks_fields_task.delay(task=task.pk)
 | 
			
		||||
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
    def patch(self, request, pk):
 | 
			
		||||
        from automation.tasks import update_policy_autotasks_fields_task
 | 
			
		||||
        from autotasks.tasks import enable_or_disable_win_task
 | 
			
		||||
 | 
			
		||||
        task = get_object_or_404(AutomatedTask, pk=pk)
 | 
			
		||||
 | 
			
		||||
        if "enableordisable" in request.data:
 | 
			
		||||
            action = request.data["enableordisable"]
 | 
			
		||||
            task.enabled = action
 | 
			
		||||
            task.save(update_fields=["enabled"])
 | 
			
		||||
            action = "enabled" if action else "disabled"
 | 
			
		||||
 | 
			
		||||
            if task.policy:
 | 
			
		||||
                update_policy_autotasks_fields_task.delay(
 | 
			
		||||
                    task=task.pk, update_agent=True
 | 
			
		||||
                )
 | 
			
		||||
            elif task.agent:
 | 
			
		||||
                enable_or_disable_win_task.delay(pk=task.pk)
 | 
			
		||||
 | 
			
		||||
            return Response(f"Task will be {action} shortly")
 | 
			
		||||
 | 
			
		||||
        else:
 | 
			
		||||
            return notify_error("The request was invalid")
 | 
			
		||||
        return Response("The task was updated")
 | 
			
		||||
 | 
			
		||||
    def delete(self, request, pk):
 | 
			
		||||
        from automation.tasks import delete_policy_autotasks_task
 | 
			
		||||
@@ -118,6 +99,9 @@ class AutoTask(APIView):
 | 
			
		||||
 | 
			
		||||
        task = get_object_or_404(AutomatedTask, pk=pk)
 | 
			
		||||
 | 
			
		||||
        if task.agent and not _has_perm_on_agent(request.user, task.agent.agent_id):
 | 
			
		||||
            raise PermissionDenied()
 | 
			
		||||
 | 
			
		||||
        if task.agent:
 | 
			
		||||
            delete_win_task_schedule.delay(pk=task.pk)
 | 
			
		||||
        elif task.policy:
 | 
			
		||||
@@ -127,11 +111,16 @@ class AutoTask(APIView):
 | 
			
		||||
        return Response(f"{task.name} will be deleted shortly")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view()
 | 
			
		||||
@permission_classes([IsAuthenticated, RunAutoTaskPerms])
 | 
			
		||||
def run_task(request, pk):
 | 
			
		||||
    from autotasks.tasks import run_win_task
 | 
			
		||||
class RunAutoTask(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, RunAutoTaskPerms]
 | 
			
		||||
 | 
			
		||||
    task = get_object_or_404(AutomatedTask, pk=pk)
 | 
			
		||||
    run_win_task.delay(pk=pk)
 | 
			
		||||
    return Response(f"{task.name} will now be run on {task.agent.hostname}")
 | 
			
		||||
    def post(self, request, pk):
 | 
			
		||||
        from autotasks.tasks import run_win_task
 | 
			
		||||
 | 
			
		||||
        task = get_object_or_404(AutomatedTask, pk=pk)
 | 
			
		||||
 | 
			
		||||
        if task.agent and not _has_perm_on_agent(request.user, task.agent.agent_id):
 | 
			
		||||
            raise PermissionDenied()
 | 
			
		||||
 | 
			
		||||
        run_win_task.delay(pk=pk)
 | 
			
		||||
        return Response(f"{task.name} will now be run on {task.agent.hostname}")
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										23
									
								
								api/tacticalrmm/checks/migrations/0025_auto_20210917_1954.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								api/tacticalrmm/checks/migrations/0025_auto_20210917_1954.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
# Generated by Django 3.2.6 on 2021-09-17 19:54
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("checks", "0024_auto_20210606_1632"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="check",
 | 
			
		||||
            name="created_by",
 | 
			
		||||
            field=models.CharField(blank=True, max_length=255, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="check",
 | 
			
		||||
            name="modified_by",
 | 
			
		||||
            field=models.CharField(blank=True, max_length=255, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -12,10 +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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
logger.configure(**settings.LOG_CONFIG)
 | 
			
		||||
from tacticalrmm.models import PermissionQuerySet
 | 
			
		||||
 | 
			
		||||
CHECK_TYPE_CHOICES = [
 | 
			
		||||
    ("diskspace", "Disk Space Check"),
 | 
			
		||||
@@ -54,6 +51,7 @@ EVT_LOG_FAIL_WHEN_CHOICES = [
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Check(BaseAuditModel):
 | 
			
		||||
    objects = PermissionQuerySet.as_manager()
 | 
			
		||||
 | 
			
		||||
    # common fields
 | 
			
		||||
 | 
			
		||||
@@ -234,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",
 | 
			
		||||
@@ -461,7 +459,7 @@ class Check(BaseAuditModel):
 | 
			
		||||
 | 
			
		||||
        elif self.status == "passing":
 | 
			
		||||
            self.fail_count = 0
 | 
			
		||||
            self.save(update_fields=["status", "fail_count", "alert_severity"])
 | 
			
		||||
            self.save()
 | 
			
		||||
            if Alert.objects.filter(assigned_check=self, resolved=False).exists():
 | 
			
		||||
                Alert.handle_alert_resolve(self)
 | 
			
		||||
 | 
			
		||||
@@ -475,24 +473,9 @@ class Check(BaseAuditModel):
 | 
			
		||||
    @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):
 | 
			
		||||
 | 
			
		||||
@@ -688,6 +671,8 @@ class Check(BaseAuditModel):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CheckHistory(models.Model):
 | 
			
		||||
    objects = PermissionQuerySet.as_manager()
 | 
			
		||||
 | 
			
		||||
    check_id = models.PositiveIntegerField(default=0)
 | 
			
		||||
    x = models.DateTimeField(auto_now_add=True)
 | 
			
		||||
    y = models.PositiveIntegerField(null=True, blank=True, default=None)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,16 +1,23 @@
 | 
			
		||||
from rest_framework import permissions
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.permissions import _has_perm
 | 
			
		||||
from tacticalrmm.permissions import _has_perm, _has_perm_on_agent
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ManageChecksPerms(permissions.BasePermission):
 | 
			
		||||
class ChecksPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        if r.method == "GET":
 | 
			
		||||
            return True
 | 
			
		||||
 | 
			
		||||
        return _has_perm(r, "can_manage_checks")
 | 
			
		||||
        if r.method == "GET" or r.method == "PATCH":
 | 
			
		||||
            if "agent_id" in view.kwargs.keys():
 | 
			
		||||
                return _has_perm(r, "can_list_checks") and _has_perm_on_agent(
 | 
			
		||||
                    r.user, view.kwargs["agent_id"]
 | 
			
		||||
                )
 | 
			
		||||
            else:
 | 
			
		||||
                return _has_perm(r, "can_list_checks")
 | 
			
		||||
        else:
 | 
			
		||||
            return _has_perm(r, "can_manage_checks")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RunChecksPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        return _has_perm(r, "can_run_checks")
 | 
			
		||||
        return _has_perm(r, "can_run_checks") and _has_perm_on_agent(
 | 
			
		||||
            r.user, view.kwargs["agent_id"]
 | 
			
		||||
        )
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@ import validators as _v
 | 
			
		||||
from rest_framework import serializers
 | 
			
		||||
 | 
			
		||||
from autotasks.models import AutomatedTask
 | 
			
		||||
from scripts.serializers import ScriptCheckSerializer, ScriptSerializer
 | 
			
		||||
from scripts.serializers import ScriptCheckSerializer
 | 
			
		||||
 | 
			
		||||
from .models import Check, CheckHistory
 | 
			
		||||
from scripts.models import Script
 | 
			
		||||
@@ -18,7 +18,6 @@ class AssignedTaskField(serializers.ModelSerializer):
 | 
			
		||||
class CheckSerializer(serializers.ModelSerializer):
 | 
			
		||||
 | 
			
		||||
    readable_desc = serializers.ReadOnlyField()
 | 
			
		||||
    script = ScriptSerializer(read_only=True)
 | 
			
		||||
    assigned_task = serializers.SerializerMethodField()
 | 
			
		||||
    last_run = serializers.ReadOnlyField(source="last_run_as_timezone")
 | 
			
		||||
    history_info = serializers.ReadOnlyField()
 | 
			
		||||
@@ -57,6 +56,11 @@ class CheckSerializer(serializers.ModelSerializer):
 | 
			
		||||
    def validate(self, val):
 | 
			
		||||
        try:
 | 
			
		||||
            check_type = val["check_type"]
 | 
			
		||||
            filter = (
 | 
			
		||||
                {"agent": val["agent"]}
 | 
			
		||||
                if "agent" in val.keys()
 | 
			
		||||
                else {"policy": val["policy"]}
 | 
			
		||||
            )
 | 
			
		||||
        except KeyError:
 | 
			
		||||
            return val
 | 
			
		||||
 | 
			
		||||
@@ -65,7 +69,7 @@ class CheckSerializer(serializers.ModelSerializer):
 | 
			
		||||
        if check_type == "diskspace":
 | 
			
		||||
            if not self.instance:  # only on create
 | 
			
		||||
                checks = (
 | 
			
		||||
                    Check.objects.filter(**self.context)
 | 
			
		||||
                    Check.objects.filter(**filter)
 | 
			
		||||
                    .filter(check_type="diskspace")
 | 
			
		||||
                    .exclude(managed_by_policy=True)
 | 
			
		||||
                )
 | 
			
		||||
@@ -102,7 +106,7 @@ class CheckSerializer(serializers.ModelSerializer):
 | 
			
		||||
 | 
			
		||||
        if check_type == "cpuload" and not self.instance:
 | 
			
		||||
            if (
 | 
			
		||||
                Check.objects.filter(**self.context, check_type="cpuload")
 | 
			
		||||
                Check.objects.filter(**filter, check_type="cpuload")
 | 
			
		||||
                .exclude(managed_by_policy=True)
 | 
			
		||||
                .exists()
 | 
			
		||||
            ):
 | 
			
		||||
@@ -126,7 +130,7 @@ class CheckSerializer(serializers.ModelSerializer):
 | 
			
		||||
 | 
			
		||||
        if check_type == "memory" and not self.instance:
 | 
			
		||||
            if (
 | 
			
		||||
                Check.objects.filter(**self.context, check_type="memory")
 | 
			
		||||
                Check.objects.filter(**filter, check_type="memory")
 | 
			
		||||
                .exclude(managed_by_policy=True)
 | 
			
		||||
                .exists()
 | 
			
		||||
            ):
 | 
			
		||||
@@ -220,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__"
 | 
			
		||||
 
 | 
			
		||||
@@ -8,21 +8,46 @@ from tacticalrmm.test import TacticalTestCase
 | 
			
		||||
 | 
			
		||||
from .serializers import CheckSerializer
 | 
			
		||||
 | 
			
		||||
base_url = "/checks"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestCheckViews(TacticalTestCase):
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
        self.setup_coresettings()
 | 
			
		||||
 | 
			
		||||
    def test_get_checks(self):
 | 
			
		||||
        url = f"{base_url}/"
 | 
			
		||||
        agent = baker.make_recipe("agents.agent")
 | 
			
		||||
        baker.make("checks.Check", agent=agent, _quantity=4)
 | 
			
		||||
        baker.make("checks.Check", _quantity=4)
 | 
			
		||||
 | 
			
		||||
        resp = self.client.get(url, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertEqual(len(resp.data), 8)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # test checks agent url
 | 
			
		||||
        url = f"/agents/{agent.agent_id}/checks/"
 | 
			
		||||
        resp = self.client.get(url, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertEqual(len(resp.data), 4)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # test agent doesn't exist
 | 
			
		||||
        url = f"/agents/jh3498uf8fkh4ro8hfd8df98/checks/"
 | 
			
		||||
        resp = self.client.get(url, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 404)
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
    def test_delete_agent_check(self):
 | 
			
		||||
        # setup data
 | 
			
		||||
        agent = baker.make_recipe("agents.agent")
 | 
			
		||||
        check = baker.make_recipe("checks.diskspace_check", agent=agent)
 | 
			
		||||
 | 
			
		||||
        resp = self.client.delete("/checks/500/check/", format="json")
 | 
			
		||||
        resp = self.client.delete(f"{base_url}/500/", format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 404)
 | 
			
		||||
 | 
			
		||||
        url = f"/checks/{check.pk}/check/"
 | 
			
		||||
        url = f"{base_url}/{check.pk}/"
 | 
			
		||||
 | 
			
		||||
        resp = self.client.delete(url, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
@@ -30,11 +55,11 @@ class TestCheckViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("delete", url)
 | 
			
		||||
 | 
			
		||||
    def test_get_disk_check(self):
 | 
			
		||||
    def test_get_check(self):
 | 
			
		||||
        # setup data
 | 
			
		||||
        disk_check = baker.make_recipe("checks.diskspace_check")
 | 
			
		||||
 | 
			
		||||
        url = f"/checks/{disk_check.pk}/check/"
 | 
			
		||||
        url = f"{base_url}/{disk_check.pk}/"
 | 
			
		||||
 | 
			
		||||
        resp = self.client.get(url, format="json")
 | 
			
		||||
        serializer = CheckSerializer(disk_check)
 | 
			
		||||
@@ -46,296 +71,161 @@ class TestCheckViews(TacticalTestCase):
 | 
			
		||||
    def test_add_disk_check(self):
 | 
			
		||||
        # setup data
 | 
			
		||||
        agent = baker.make_recipe("agents.agent")
 | 
			
		||||
        policy = baker.make("automation.Policy")
 | 
			
		||||
 | 
			
		||||
        url = "/checks/checks/"
 | 
			
		||||
        url = f"{base_url}/"
 | 
			
		||||
 | 
			
		||||
        valid_payload = {
 | 
			
		||||
            "pk": agent.pk,
 | 
			
		||||
            "check": {
 | 
			
		||||
                "check_type": "diskspace",
 | 
			
		||||
                "disk": "C:",
 | 
			
		||||
                "error_threshold": 55,
 | 
			
		||||
                "warning_threshold": 0,
 | 
			
		||||
                "fails_b4_alert": 3,
 | 
			
		||||
            },
 | 
			
		||||
        agent_payload = {
 | 
			
		||||
            "agent": agent.agent_id,
 | 
			
		||||
            "check_type": "diskspace",
 | 
			
		||||
            "disk": "C:",
 | 
			
		||||
            "error_threshold": 55,
 | 
			
		||||
            "warning_threshold": 0,
 | 
			
		||||
            "fails_b4_alert": 3,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        resp = self.client.post(url, valid_payload, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        # this should fail because we already have a check for drive C: in setup
 | 
			
		||||
        invalid_payload = {
 | 
			
		||||
            "pk": agent.pk,
 | 
			
		||||
            "check": {
 | 
			
		||||
                "check_type": "diskspace",
 | 
			
		||||
                "disk": "C:",
 | 
			
		||||
                "error_threshold": 55,
 | 
			
		||||
                "warning_threshold": 0,
 | 
			
		||||
                "fails_b4_alert": 3,
 | 
			
		||||
            },
 | 
			
		||||
        policy_payload = {
 | 
			
		||||
            "policy": policy.id,
 | 
			
		||||
            "check_type": "diskspace",
 | 
			
		||||
            "disk": "C:",
 | 
			
		||||
            "error_threshold": 55,
 | 
			
		||||
            "warning_threshold": 0,
 | 
			
		||||
            "fails_b4_alert": 3,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        resp = self.client.post(url, invalid_payload, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 400)
 | 
			
		||||
        for payload in [agent_payload, policy_payload]:
 | 
			
		||||
 | 
			
		||||
        # this should fail because both error and warning threshold are 0
 | 
			
		||||
        invalid_payload = {
 | 
			
		||||
            "pk": agent.pk,
 | 
			
		||||
            "check": {
 | 
			
		||||
                "check_type": "diskspace",
 | 
			
		||||
                "disk": "C:",
 | 
			
		||||
                "error_threshold": 0,
 | 
			
		||||
                "warning_threshold": 0,
 | 
			
		||||
                "fails_b4_alert": 3,
 | 
			
		||||
            },
 | 
			
		||||
        }
 | 
			
		||||
            # add valid check
 | 
			
		||||
            resp = self.client.post(url, payload, format="json")
 | 
			
		||||
            self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        resp = self.client.post(url, invalid_payload, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 400)
 | 
			
		||||
            # this should fail since we just added it
 | 
			
		||||
            resp = self.client.post(url, payload, format="json")
 | 
			
		||||
            self.assertEqual(resp.status_code, 400)
 | 
			
		||||
 | 
			
		||||
        # this should fail because both error is greater than warning threshold
 | 
			
		||||
        invalid_payload = {
 | 
			
		||||
            "pk": agent.pk,
 | 
			
		||||
            "check": {
 | 
			
		||||
                "check_type": "diskspace",
 | 
			
		||||
                "disk": "C:",
 | 
			
		||||
                "error_threshold": 50,
 | 
			
		||||
                "warning_threshold": 30,
 | 
			
		||||
                "fails_b4_alert": 3,
 | 
			
		||||
            },
 | 
			
		||||
        }
 | 
			
		||||
            # this should fail because both error and warning threshold are 0
 | 
			
		||||
            payload["error_threshold"] = 0
 | 
			
		||||
            payload["warning_threshold"] = 0
 | 
			
		||||
 | 
			
		||||
        resp = self.client.post(url, invalid_payload, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 400)
 | 
			
		||||
            resp = self.client.post(url, payload, format="json")
 | 
			
		||||
            self.assertEqual(resp.status_code, 400)
 | 
			
		||||
 | 
			
		||||
            # this should fail because error threshold is greater than warning threshold
 | 
			
		||||
            payload["error_threshold"] = 50
 | 
			
		||||
            payload["warning_threshold"] = 30
 | 
			
		||||
 | 
			
		||||
            resp = self.client.post(url, payload, format="json")
 | 
			
		||||
            self.assertEqual(resp.status_code, 400)
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("post", url)
 | 
			
		||||
 | 
			
		||||
    def test_add_cpuload_check(self):
 | 
			
		||||
        url = "/checks/checks/"
 | 
			
		||||
        url = f"{base_url}/"
 | 
			
		||||
        agent = baker.make_recipe("agents.agent")
 | 
			
		||||
        payload = {
 | 
			
		||||
            "pk": agent.pk,
 | 
			
		||||
            "check": {
 | 
			
		||||
                "check_type": "cpuload",
 | 
			
		||||
                "error_threshold": 66,
 | 
			
		||||
                "warning_threshold": 0,
 | 
			
		||||
                "fails_b4_alert": 9,
 | 
			
		||||
            },
 | 
			
		||||
        policy = baker.make("automation.Policy")
 | 
			
		||||
 | 
			
		||||
        agent_payload = {
 | 
			
		||||
            "agent": agent.agent_id,
 | 
			
		||||
            "check_type": "cpuload",
 | 
			
		||||
            "error_threshold": 66,
 | 
			
		||||
            "warning_threshold": 0,
 | 
			
		||||
            "fails_b4_alert": 9,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        resp = self.client.post(url, payload, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        payload["error_threshold"] = 87
 | 
			
		||||
        resp = self.client.post(url, payload, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 400)
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            resp.json()["non_field_errors"][0],
 | 
			
		||||
            "A cpuload check for this agent already exists",
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # should fail because both error and warning thresholds are 0
 | 
			
		||||
        invalid_payload = {
 | 
			
		||||
            "pk": agent.pk,
 | 
			
		||||
            "check": {
 | 
			
		||||
                "check_type": "cpuload",
 | 
			
		||||
                "error_threshold": 0,
 | 
			
		||||
                "warning_threshold": 0,
 | 
			
		||||
                "fails_b4_alert": 9,
 | 
			
		||||
            },
 | 
			
		||||
        policy_payload = {
 | 
			
		||||
            "policy": policy.id,
 | 
			
		||||
            "check_type": "cpuload",
 | 
			
		||||
            "error_threshold": 66,
 | 
			
		||||
            "warning_threshold": 0,
 | 
			
		||||
            "fails_b4_alert": 9,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        resp = self.client.post(url, invalid_payload, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 400)
 | 
			
		||||
        for payload in [agent_payload, policy_payload]:
 | 
			
		||||
 | 
			
		||||
        # should fail because error is less than warning
 | 
			
		||||
        invalid_payload = {
 | 
			
		||||
            "pk": agent.pk,
 | 
			
		||||
            "check": {
 | 
			
		||||
                "check_type": "cpuload",
 | 
			
		||||
                "error_threshold": 10,
 | 
			
		||||
                "warning_threshold": 50,
 | 
			
		||||
                "fails_b4_alert": 9,
 | 
			
		||||
            },
 | 
			
		||||
        }
 | 
			
		||||
            # add cpu check
 | 
			
		||||
            resp = self.client.post(url, payload, format="json")
 | 
			
		||||
            self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        resp = self.client.post(url, invalid_payload, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 400)
 | 
			
		||||
            # should fail since cpu check already exists
 | 
			
		||||
            resp = self.client.post(url, payload, format="json")
 | 
			
		||||
            self.assertEqual(resp.status_code, 400)
 | 
			
		||||
 | 
			
		||||
            # this should fail because both error and warning threshold are 0
 | 
			
		||||
            payload["error_threshold"] = 0
 | 
			
		||||
            payload["warning_threshold"] = 0
 | 
			
		||||
 | 
			
		||||
            resp = self.client.post(url, payload, format="json")
 | 
			
		||||
            self.assertEqual(resp.status_code, 400)
 | 
			
		||||
 | 
			
		||||
            # this should fail because error threshold is less than warning threshold
 | 
			
		||||
            payload["error_threshold"] = 20
 | 
			
		||||
            payload["warning_threshold"] = 30
 | 
			
		||||
 | 
			
		||||
            resp = self.client.post(url, payload, format="json")
 | 
			
		||||
            self.assertEqual(resp.status_code, 400)
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("post", url)
 | 
			
		||||
 | 
			
		||||
    def test_add_memory_check(self):
 | 
			
		||||
        url = "/checks/checks/"
 | 
			
		||||
        url = f"{base_url}/"
 | 
			
		||||
        agent = baker.make_recipe("agents.agent")
 | 
			
		||||
        payload = {
 | 
			
		||||
            "pk": agent.pk,
 | 
			
		||||
            "check": {
 | 
			
		||||
                "check_type": "memory",
 | 
			
		||||
                "error_threshold": 78,
 | 
			
		||||
                "warning_threshold": 0,
 | 
			
		||||
                "fails_b4_alert": 1,
 | 
			
		||||
            },
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        resp = self.client.post(url, payload, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        payload["error_threshold"] = 55
 | 
			
		||||
        resp = self.client.post(url, payload, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 400)
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            resp.json()["non_field_errors"][0],
 | 
			
		||||
            "A memory check for this agent already exists",
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # should fail because both error and warning thresholds are 0
 | 
			
		||||
        invalid_payload = {
 | 
			
		||||
            "pk": agent.pk,
 | 
			
		||||
            "check": {
 | 
			
		||||
                "check_type": "memory",
 | 
			
		||||
                "error_threshold": 0,
 | 
			
		||||
                "warning_threshold": 0,
 | 
			
		||||
                "fails_b4_alert": 9,
 | 
			
		||||
            },
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        resp = self.client.post(url, invalid_payload, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 400)
 | 
			
		||||
 | 
			
		||||
        # should fail because error is less than warning
 | 
			
		||||
        invalid_payload = {
 | 
			
		||||
            "pk": agent.pk,
 | 
			
		||||
            "check": {
 | 
			
		||||
                "check_type": "memory",
 | 
			
		||||
                "error_threshold": 10,
 | 
			
		||||
                "warning_threshold": 50,
 | 
			
		||||
                "fails_b4_alert": 9,
 | 
			
		||||
            },
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        resp = self.client.post(url, invalid_payload, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 400)
 | 
			
		||||
 | 
			
		||||
    def test_get_policy_disk_check(self):
 | 
			
		||||
        # setup data
 | 
			
		||||
        policy = baker.make("automation.Policy")
 | 
			
		||||
        disk_check = baker.make_recipe("checks.diskspace_check", policy=policy)
 | 
			
		||||
 | 
			
		||||
        url = f"/checks/{disk_check.pk}/check/"
 | 
			
		||||
        agent_payload = {
 | 
			
		||||
            "agent": agent.agent_id,
 | 
			
		||||
            "check_type": "memory",
 | 
			
		||||
            "error_threshold": 78,
 | 
			
		||||
            "warning_threshold": 0,
 | 
			
		||||
            "fails_b4_alert": 1,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        resp = self.client.get(url, format="json")
 | 
			
		||||
        serializer = CheckSerializer(disk_check)
 | 
			
		||||
        policy_payload = {
 | 
			
		||||
            "policy": policy.id,
 | 
			
		||||
            "check_type": "memory",
 | 
			
		||||
            "error_threshold": 78,
 | 
			
		||||
            "warning_threshold": 0,
 | 
			
		||||
            "fails_b4_alert": 1,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        for payload in [agent_payload, policy_payload]:
 | 
			
		||||
 | 
			
		||||
            # add memory check
 | 
			
		||||
            resp = self.client.post(url, payload, format="json")
 | 
			
		||||
            self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
            # should fail since cpu check already exists
 | 
			
		||||
            resp = self.client.post(url, payload, format="json")
 | 
			
		||||
            self.assertEqual(resp.status_code, 400)
 | 
			
		||||
 | 
			
		||||
            # this should fail because both error and warning threshold are 0
 | 
			
		||||
            payload["error_threshold"] = 0
 | 
			
		||||
            payload["warning_threshold"] = 0
 | 
			
		||||
 | 
			
		||||
            resp = self.client.post(url, payload, format="json")
 | 
			
		||||
            self.assertEqual(resp.status_code, 400)
 | 
			
		||||
 | 
			
		||||
            # this should fail because error threshold is less than warning threshold
 | 
			
		||||
            payload["error_threshold"] = 20
 | 
			
		||||
            payload["warning_threshold"] = 30
 | 
			
		||||
 | 
			
		||||
            resp = self.client.post(url, payload, format="json")
 | 
			
		||||
            self.assertEqual(resp.status_code, 400)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertEqual(resp.data, serializer.data)  # type: ignore
 | 
			
		||||
        self.check_not_authenticated("post", url)
 | 
			
		||||
 | 
			
		||||
    def test_add_policy_disk_check(self):
 | 
			
		||||
        # setup data
 | 
			
		||||
        policy = baker.make("automation.Policy")
 | 
			
		||||
 | 
			
		||||
        url = "/checks/checks/"
 | 
			
		||||
 | 
			
		||||
        valid_payload = {
 | 
			
		||||
            "policy": policy.pk,  # type: ignore
 | 
			
		||||
            "check": {
 | 
			
		||||
                "check_type": "diskspace",
 | 
			
		||||
                "disk": "M:",
 | 
			
		||||
                "error_threshold": 86,
 | 
			
		||||
                "warning_threshold": 0,
 | 
			
		||||
                "fails_b4_alert": 2,
 | 
			
		||||
            },
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        # should fail because both error and warning thresholds are 0
 | 
			
		||||
        invalid_payload = {
 | 
			
		||||
            "policy": policy.pk,  # type: ignore
 | 
			
		||||
            "check": {
 | 
			
		||||
                "check_type": "diskspace",
 | 
			
		||||
                "error_threshold": 0,
 | 
			
		||||
                "warning_threshold": 0,
 | 
			
		||||
                "fails_b4_alert": 9,
 | 
			
		||||
            },
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        resp = self.client.post(url, invalid_payload, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 400)
 | 
			
		||||
 | 
			
		||||
        # should fail because warning is less than error
 | 
			
		||||
        invalid_payload = {
 | 
			
		||||
            "policy": policy.pk,  # type: ignore
 | 
			
		||||
            "check": {
 | 
			
		||||
                "check_type": "diskspace",
 | 
			
		||||
                "error_threshold": 80,
 | 
			
		||||
                "warning_threshold": 50,
 | 
			
		||||
                "fails_b4_alert": 9,
 | 
			
		||||
            },
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        resp = self.client.post(url, valid_payload, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        # this should fail because we already have a check for drive M: in setup
 | 
			
		||||
        invalid_payload = {
 | 
			
		||||
            "policy": policy.pk,  # type: ignore
 | 
			
		||||
            "check": {
 | 
			
		||||
                "check_type": "diskspace",
 | 
			
		||||
                "disk": "M:",
 | 
			
		||||
                "error_threshold": 34,
 | 
			
		||||
                "warning_threshold": 0,
 | 
			
		||||
                "fails_b4_alert": 9,
 | 
			
		||||
            },
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        resp = self.client.post(url, invalid_payload, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 400)
 | 
			
		||||
 | 
			
		||||
    def test_get_disks_for_policies(self):
 | 
			
		||||
        url = "/checks/getalldisks/"
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
        self.assertIsInstance(r.data, list)  # type: ignore
 | 
			
		||||
        self.assertEqual(26, len(r.data))  # type: ignore
 | 
			
		||||
 | 
			
		||||
    def test_edit_check_alert(self):
 | 
			
		||||
        # setup data
 | 
			
		||||
        policy = baker.make("automation.Policy")
 | 
			
		||||
        agent = baker.make_recipe("agents.agent")
 | 
			
		||||
 | 
			
		||||
        policy_disk_check = baker.make_recipe("checks.diskspace_check", policy=policy)
 | 
			
		||||
        agent_disk_check = baker.make_recipe("checks.diskspace_check", agent=agent)
 | 
			
		||||
        url_a = f"/checks/{agent_disk_check.pk}/check/"
 | 
			
		||||
        url_p = f"/checks/{policy_disk_check.pk}/check/"
 | 
			
		||||
 | 
			
		||||
        valid_payload = {"email_alert": False, "check_alert": True}
 | 
			
		||||
        invalid_payload = {"email_alert": False}
 | 
			
		||||
 | 
			
		||||
        with self.assertRaises(KeyError) as err:
 | 
			
		||||
            resp = self.client.patch(url_a, invalid_payload, format="json")
 | 
			
		||||
 | 
			
		||||
        with self.assertRaises(KeyError) as err:
 | 
			
		||||
            resp = self.client.patch(url_p, invalid_payload, format="json")
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url_a, valid_payload, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url_p, valid_payload, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("patch", url_a)
 | 
			
		||||
 | 
			
		||||
    @patch("agents.models.Agent.nats_cmd")
 | 
			
		||||
    def test_run_checks(self, nats_cmd):
 | 
			
		||||
        agent = baker.make_recipe("agents.agent", version="1.4.1")
 | 
			
		||||
        agent_b4_141 = baker.make_recipe("agents.agent", version="1.4.0")
 | 
			
		||||
 | 
			
		||||
        url = f"/checks/runchecks/{agent_b4_141.pk}/"
 | 
			
		||||
        url = f"{base_url}/{agent_b4_141.agent_id}/run/"
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        nats_cmd.assert_called_with({"func": "runchecks"}, wait=False)
 | 
			
		||||
 | 
			
		||||
        nats_cmd.reset_mock()
 | 
			
		||||
        nats_cmd.return_value = "busy"
 | 
			
		||||
        url = f"/checks/runchecks/{agent.pk}/"
 | 
			
		||||
        url = f"{base_url}/{agent.agent_id}/run/"
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
        self.assertEqual(r.status_code, 400)
 | 
			
		||||
        nats_cmd.assert_called_with({"func": "runchecks"}, timeout=15)
 | 
			
		||||
@@ -343,7 +233,7 @@ class TestCheckViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        nats_cmd.reset_mock()
 | 
			
		||||
        nats_cmd.return_value = "ok"
 | 
			
		||||
        url = f"/checks/runchecks/{agent.pk}/"
 | 
			
		||||
        url = f"{base_url}/{agent.agent_id}/run/"
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        nats_cmd.assert_called_with({"func": "runchecks"}, timeout=15)
 | 
			
		||||
@@ -351,7 +241,7 @@ class TestCheckViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        nats_cmd.reset_mock()
 | 
			
		||||
        nats_cmd.return_value = "timeout"
 | 
			
		||||
        url = f"/checks/runchecks/{agent.pk}/"
 | 
			
		||||
        url = f"{base_url}/{agent.agent_id}/run/"
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
        self.assertEqual(r.status_code, 400)
 | 
			
		||||
        nats_cmd.assert_called_with({"func": "runchecks"}, timeout=15)
 | 
			
		||||
@@ -379,7 +269,7 @@ class TestCheckViews(TacticalTestCase):
 | 
			
		||||
        resp = self.client.patch("/checks/history/500/", format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 404)
 | 
			
		||||
 | 
			
		||||
        url = f"/checks/history/{check.id}/"
 | 
			
		||||
        url = f"/checks/{check.id}/history/"
 | 
			
		||||
 | 
			
		||||
        # test with timeFilter last 30 days
 | 
			
		||||
        data = {"timeFilter": 30}
 | 
			
		||||
@@ -873,74 +763,7 @@ class TestCheckTasks(TacticalTestCase):
 | 
			
		||||
        self.assertEqual(new_check.status, "failing")
 | 
			
		||||
        self.assertEqual(new_check.alert_severity, "info")
 | 
			
		||||
 | 
			
		||||
        """ # test failing and attempt start
 | 
			
		||||
        winsvc.restart_if_stopped = True
 | 
			
		||||
        winsvc.alert_severity = "warning"
 | 
			
		||||
        winsvc.save()
 | 
			
		||||
 | 
			
		||||
        nats_cmd.return_value = "timeout"
 | 
			
		||||
 | 
			
		||||
        data = {"id": winsvc.id, "exists": True, "status": "not running"}
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=winsvc.id)
 | 
			
		||||
        self.assertEqual(new_check.status, "failing")
 | 
			
		||||
        self.assertEqual(new_check.alert_severity, "warning")
 | 
			
		||||
        nats_cmd.assert_called()
 | 
			
		||||
        nats_cmd.reset_mock()
 | 
			
		||||
 | 
			
		||||
        # test failing and attempt start
 | 
			
		||||
        winsvc.alert_severity = "error"
 | 
			
		||||
        winsvc.save()
 | 
			
		||||
        nats_cmd.return_value = {"success": False, "errormsg": "Some Error"}
 | 
			
		||||
 | 
			
		||||
        data = {"id": winsvc.id, "exists": True, "status": "not running"}
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=winsvc.id)
 | 
			
		||||
        self.assertEqual(new_check.status, "failing")
 | 
			
		||||
        self.assertEqual(new_check.alert_severity, "error")
 | 
			
		||||
        nats_cmd.assert_called()
 | 
			
		||||
        nats_cmd.reset_mock()
 | 
			
		||||
 | 
			
		||||
        # test success and attempt start
 | 
			
		||||
        nats_cmd.return_value = {"success": True}
 | 
			
		||||
 | 
			
		||||
        data = {"id": winsvc.id, "exists": True, "status": "not running"}
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=winsvc.id)
 | 
			
		||||
        self.assertEqual(new_check.status, "passing")
 | 
			
		||||
        nats_cmd.assert_called()
 | 
			
		||||
        nats_cmd.reset_mock()
 | 
			
		||||
 | 
			
		||||
        # test failing and service not exist
 | 
			
		||||
        data = {"id": winsvc.id, "exists": False, "status": ""}
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=winsvc.id)
 | 
			
		||||
        self.assertEqual(new_check.status, "failing")
 | 
			
		||||
 | 
			
		||||
        # test success and service not exist
 | 
			
		||||
        winsvc.pass_if_svc_not_exist = True
 | 
			
		||||
        winsvc.save()
 | 
			
		||||
        data = {"id": winsvc.id, "exists": False, "status": ""}
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=winsvc.id)
 | 
			
		||||
        self.assertEqual(new_check.status, "passing") """
 | 
			
		||||
 | 
			
		||||
    """ def test_handle_eventlog_check(self):
 | 
			
		||||
    def test_handle_eventlog_check(self):
 | 
			
		||||
        from checks.models import Check
 | 
			
		||||
 | 
			
		||||
        url = "/api/v3/checkrunner/"
 | 
			
		||||
@@ -984,6 +807,8 @@ class TestCheckTasks(TacticalTestCase):
 | 
			
		||||
            ],
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        no_logs_data = {"id": eventlog.id, "log": []}
 | 
			
		||||
 | 
			
		||||
        # test failing when contains
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
@@ -993,11 +818,8 @@ class TestCheckTasks(TacticalTestCase):
 | 
			
		||||
        self.assertEquals(new_check.alert_severity, "warning")
 | 
			
		||||
        self.assertEquals(new_check.status, "failing")
 | 
			
		||||
 | 
			
		||||
        # test passing when not contains and message
 | 
			
		||||
        eventlog.event_message = "doesnt exist"
 | 
			
		||||
        eventlog.save()
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        # test passing when contains
 | 
			
		||||
        resp = self.client.patch(url, no_logs_data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=eventlog.id)
 | 
			
		||||
@@ -1007,11 +829,9 @@ class TestCheckTasks(TacticalTestCase):
 | 
			
		||||
        # test failing when not contains and message and source
 | 
			
		||||
        eventlog.fail_when = "not_contains"
 | 
			
		||||
        eventlog.alert_severity = "error"
 | 
			
		||||
        eventlog.event_message = "doesnt exist"
 | 
			
		||||
        eventlog.event_source = "doesnt exist"
 | 
			
		||||
        eventlog.save()
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        resp = self.client.patch(url, no_logs_data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=eventlog.id)
 | 
			
		||||
@@ -1020,10 +840,6 @@ class TestCheckTasks(TacticalTestCase):
 | 
			
		||||
        self.assertEquals(new_check.alert_severity, "error")
 | 
			
		||||
 | 
			
		||||
        # test passing when contains with source and message
 | 
			
		||||
        eventlog.event_message = "test"
 | 
			
		||||
        eventlog.event_source = "source"
 | 
			
		||||
        eventlog.save()
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
@@ -1031,115 +847,252 @@ class TestCheckTasks(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        self.assertEquals(new_check.status, "passing")
 | 
			
		||||
 | 
			
		||||
        # test failing with wildcard not contains and source
 | 
			
		||||
        eventlog.event_id_is_wildcard = True
 | 
			
		||||
        eventlog.event_source = "doesn't exist"
 | 
			
		||||
        eventlog.event_message = ""
 | 
			
		||||
        eventlog.event_id = 0
 | 
			
		||||
        eventlog.save()
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
class TestCheckPermissions(TacticalTestCase):
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        self.setup_coresettings()
 | 
			
		||||
        self.client_setup()
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=eventlog.id)
 | 
			
		||||
    def test_get_checks_permissions(self):
 | 
			
		||||
        agent = baker.make_recipe("agents.agent")
 | 
			
		||||
        policy = baker.make("automation.Policy")
 | 
			
		||||
        unauthorized_agent = baker.make_recipe("agents.agent")
 | 
			
		||||
        check = baker.make("checks.Check", agent=agent, _quantity=5)
 | 
			
		||||
        unauthorized_check = baker.make(
 | 
			
		||||
            "checks.Check", agent=unauthorized_agent, _quantity=7
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.assertEquals(new_check.status, "failing")
 | 
			
		||||
        self.assertEquals(new_check.alert_severity, "error")
 | 
			
		||||
        policy_checks = baker.make("checks.Check", policy=policy, _quantity=2)
 | 
			
		||||
 | 
			
		||||
        # test passing with wildcard contains
 | 
			
		||||
        eventlog.event_source = ""
 | 
			
		||||
        eventlog.event_message = ""
 | 
			
		||||
        eventlog.save()
 | 
			
		||||
        # test super user access
 | 
			
		||||
        self.check_authorized_superuser("get", f"{base_url}/")
 | 
			
		||||
        self.check_authorized_superuser("get", f"/agents/{agent.agent_id}/checks/")
 | 
			
		||||
        self.check_authorized_superuser(
 | 
			
		||||
            "get", f"/agents/{unauthorized_agent.agent_id}/checks/"
 | 
			
		||||
        )
 | 
			
		||||
        self.check_authorized_superuser(
 | 
			
		||||
            "get", f"/automation/policies/{policy.id}/checks/"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        user = self.create_user_with_roles([])
 | 
			
		||||
        self.client.force_authenticate(user=user)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=eventlog.id)
 | 
			
		||||
        self.check_not_authorized("get", f"{base_url}/")
 | 
			
		||||
        self.check_not_authorized("get", f"/agents/{agent.agent_id}/checks/")
 | 
			
		||||
        self.check_not_authorized(
 | 
			
		||||
            "get", f"/agents/{unauthorized_agent.agent_id}/checks/"
 | 
			
		||||
        )
 | 
			
		||||
        self.check_not_authorized("get", f"/automation/policies/{policy.id}/checks/")
 | 
			
		||||
 | 
			
		||||
        self.assertEquals(new_check.status, "passing")
 | 
			
		||||
        # add list software role to user
 | 
			
		||||
        user.role.can_list_checks = True
 | 
			
		||||
        user.role.save()
 | 
			
		||||
 | 
			
		||||
        # test failing with wildcard contains and message
 | 
			
		||||
        eventlog.fail_when = "contains"
 | 
			
		||||
        eventlog.event_type = "error"
 | 
			
		||||
        eventlog.alert_severity = "info"
 | 
			
		||||
        eventlog.event_message = "test"
 | 
			
		||||
        eventlog.event_source = ""
 | 
			
		||||
        eventlog.save()
 | 
			
		||||
        r = self.check_authorized("get", f"{base_url}/")
 | 
			
		||||
        self.assertEqual(len(r.data), 14)  # type: ignore
 | 
			
		||||
        r = self.check_authorized("get", f"/agents/{agent.agent_id}/checks/")
 | 
			
		||||
        self.assertEqual(len(r.data), 5)  # type: ignore
 | 
			
		||||
        r = self.check_authorized(
 | 
			
		||||
            "get", f"/agents/{unauthorized_agent.agent_id}/checks/"
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(len(r.data), 7)  # type: ignore
 | 
			
		||||
        r = self.check_authorized("get", f"/automation/policies/{policy.id}/checks/")
 | 
			
		||||
        self.assertEqual(len(r.data), 2)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        # test limiting to client
 | 
			
		||||
        user.role.can_view_clients.set([agent.client])
 | 
			
		||||
        self.check_not_authorized(
 | 
			
		||||
            "get", f"/agents/{unauthorized_agent.agent_id}/checks/"
 | 
			
		||||
        )
 | 
			
		||||
        self.check_authorized("get", f"/agents/{agent.agent_id}/checks/")
 | 
			
		||||
        self.check_authorized("get", f"/automation/policies/{policy.id}/checks/")
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=eventlog.id)
 | 
			
		||||
        # make sure queryset is limited too
 | 
			
		||||
        r = self.client.get(f"{base_url}/")
 | 
			
		||||
        self.assertEqual(len(r.data), 7)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.assertEquals(new_check.status, "failing")
 | 
			
		||||
        self.assertEquals(new_check.alert_severity, "info")
 | 
			
		||||
    def test_add_check_permissions(self):
 | 
			
		||||
        agent = baker.make_recipe("agents.agent")
 | 
			
		||||
        unauthorized_agent = baker.make_recipe("agents.agent")
 | 
			
		||||
        policy = baker.make("automation.Policy")
 | 
			
		||||
 | 
			
		||||
        # test passing with wildcard not contains message and source
 | 
			
		||||
        eventlog.event_message = "doesnt exist"
 | 
			
		||||
        eventlog.event_source = "doesnt exist"
 | 
			
		||||
        eventlog.save()
 | 
			
		||||
        policy_data = {
 | 
			
		||||
            "policy": policy.id,
 | 
			
		||||
            "check_type": "diskspace",
 | 
			
		||||
            "disk": "C:",
 | 
			
		||||
            "error_threshold": 55,
 | 
			
		||||
            "warning_threshold": 0,
 | 
			
		||||
            "fails_b4_alert": 3,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        agent_data = {
 | 
			
		||||
            "agent": agent.agent_id,
 | 
			
		||||
            "check_type": "diskspace",
 | 
			
		||||
            "disk": "C:",
 | 
			
		||||
            "error_threshold": 55,
 | 
			
		||||
            "warning_threshold": 0,
 | 
			
		||||
            "fails_b4_alert": 3,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=eventlog.id)
 | 
			
		||||
        unauthorized_agent_data = {
 | 
			
		||||
            "agent": unauthorized_agent.agent_id,
 | 
			
		||||
            "check_type": "diskspace",
 | 
			
		||||
            "disk": "C:",
 | 
			
		||||
            "error_threshold": 55,
 | 
			
		||||
            "warning_threshold": 0,
 | 
			
		||||
            "fails_b4_alert": 3,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        self.assertEquals(new_check.status, "passing")
 | 
			
		||||
        url = f"{base_url}/"
 | 
			
		||||
 | 
			
		||||
        # test multiple events found and contains
 | 
			
		||||
        # this should pass since only two events are found
 | 
			
		||||
        eventlog.number_of_events_b4_alert = 3
 | 
			
		||||
        eventlog.event_id_is_wildcard = False
 | 
			
		||||
        eventlog.event_source = None
 | 
			
		||||
        eventlog.event_message = None
 | 
			
		||||
        eventlog.event_id = 123
 | 
			
		||||
        eventlog.event_type = "error"
 | 
			
		||||
        eventlog.fail_when = "contains"
 | 
			
		||||
        eventlog.save()
 | 
			
		||||
        for data in [policy_data, agent_data]:
 | 
			
		||||
            # test superuser access
 | 
			
		||||
            self.check_authorized_superuser("post", url, data)
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
            user = self.create_user_with_roles([])
 | 
			
		||||
            self.client.force_authenticate(user=user)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=eventlog.id)
 | 
			
		||||
            # test user without role
 | 
			
		||||
            self.check_not_authorized("post", url, data)
 | 
			
		||||
 | 
			
		||||
        self.assertEquals(new_check.status, "passing")
 | 
			
		||||
            # add user to role and test
 | 
			
		||||
            setattr(user.role, "can_manage_checks", True)
 | 
			
		||||
            user.role.save()
 | 
			
		||||
 | 
			
		||||
        # this should pass since there are two events returned
 | 
			
		||||
        eventlog.number_of_events_b4_alert = 2
 | 
			
		||||
        eventlog.save()
 | 
			
		||||
            self.check_authorized("post", url, data)
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
            # limit user to client
 | 
			
		||||
            user.role.can_view_clients.set([agent.client])
 | 
			
		||||
            if "agent" in data.keys():
 | 
			
		||||
                self.check_authorized("post", url, data)
 | 
			
		||||
                self.check_not_authorized("post", url, unauthorized_agent_data)
 | 
			
		||||
            else:
 | 
			
		||||
                self.check_authorized("post", url, data)
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=eventlog.id)
 | 
			
		||||
    # mock the check delete method so it actually isn't deleted
 | 
			
		||||
    @patch("checks.models.Check.delete")
 | 
			
		||||
    def test_check_get_edit_delete_permissions(self, delete_check):
 | 
			
		||||
        agent = baker.make_recipe("agents.agent")
 | 
			
		||||
        unauthorized_agent = baker.make_recipe("agents.agent")
 | 
			
		||||
        policy = baker.make("automation.Policy")
 | 
			
		||||
        check = baker.make("checks.Check", agent=agent)
 | 
			
		||||
        unauthorized_check = baker.make("checks.Check", agent=unauthorized_agent)
 | 
			
		||||
        policy_check = baker.make("checks.Check", policy=policy)
 | 
			
		||||
 | 
			
		||||
        self.assertEquals(new_check.status, "failing")
 | 
			
		||||
        for method in ["get", "put", "delete"]:
 | 
			
		||||
 | 
			
		||||
        # test not contains
 | 
			
		||||
        # this should fail since only two events are found
 | 
			
		||||
        eventlog.number_of_events_b4_alert = 3
 | 
			
		||||
        eventlog.event_id_is_wildcard = False
 | 
			
		||||
        eventlog.event_source = None
 | 
			
		||||
        eventlog.event_message = None
 | 
			
		||||
        eventlog.event_id = 123
 | 
			
		||||
        eventlog.event_type = "error"
 | 
			
		||||
        eventlog.fail_when = "not_contains"
 | 
			
		||||
        eventlog.save()
 | 
			
		||||
            url = f"{base_url}/{check.id}/"
 | 
			
		||||
            unauthorized_url = f"{base_url}/{unauthorized_check.id}/"
 | 
			
		||||
            policy_url = f"{base_url}/{policy_check.id}/"
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
            # test superuser access
 | 
			
		||||
            self.check_authorized_superuser(method, url)
 | 
			
		||||
            self.check_authorized_superuser(method, unauthorized_url)
 | 
			
		||||
            self.check_authorized_superuser(method, policy_url)
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=eventlog.id)
 | 
			
		||||
            user = self.create_user_with_roles([])
 | 
			
		||||
            self.client.force_authenticate(user=user)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.assertEquals(new_check.status, "failing")
 | 
			
		||||
            # test user without role
 | 
			
		||||
            self.check_not_authorized(method, url)
 | 
			
		||||
            self.check_not_authorized(method, unauthorized_url)
 | 
			
		||||
            self.check_not_authorized(method, policy_url)
 | 
			
		||||
 | 
			
		||||
        # this should pass since there are two events returned
 | 
			
		||||
        eventlog.number_of_events_b4_alert = 2
 | 
			
		||||
        eventlog.save()
 | 
			
		||||
            # add user to role and test
 | 
			
		||||
            setattr(
 | 
			
		||||
                user.role,
 | 
			
		||||
                "can_list_checks" if method == "get" else "can_manage_checks",
 | 
			
		||||
                True,
 | 
			
		||||
            )
 | 
			
		||||
            user.role.save()
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
            self.check_authorized(method, url)
 | 
			
		||||
            self.check_authorized(method, unauthorized_url)
 | 
			
		||||
            self.check_authorized(method, policy_url)
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=eventlog.id)
 | 
			
		||||
            # limit user to client if agent check
 | 
			
		||||
            user.role.can_view_clients.set([agent.client])
 | 
			
		||||
 | 
			
		||||
        self.assertEquals(new_check.status, "passing") """
 | 
			
		||||
            self.check_authorized(method, url)
 | 
			
		||||
            self.check_not_authorized(method, unauthorized_url)
 | 
			
		||||
            self.check_authorized(method, policy_url)
 | 
			
		||||
 | 
			
		||||
    def test_check_action_permissions(self):
 | 
			
		||||
 | 
			
		||||
        agent = baker.make_recipe("agents.agent")
 | 
			
		||||
        unauthorized_agent = baker.make_recipe("agents.agent")
 | 
			
		||||
        check = baker.make("checks.Check", agent=agent)
 | 
			
		||||
        unauthorized_check = baker.make("checks.Check", agent=unauthorized_agent)
 | 
			
		||||
 | 
			
		||||
        for action in ["reset", "run"]:
 | 
			
		||||
            if action == "reset":
 | 
			
		||||
                url = f"{base_url}/{check.id}/{action}/"
 | 
			
		||||
                unauthorized_url = f"{base_url}/{unauthorized_check.id}/{action}/"
 | 
			
		||||
            else:
 | 
			
		||||
                url = f"{base_url}/{agent.agent_id}/{action}/"
 | 
			
		||||
                unauthorized_url = f"{base_url}/{unauthorized_agent.agent_id}/{action}/"
 | 
			
		||||
 | 
			
		||||
            # test superuser access
 | 
			
		||||
            self.check_authorized_superuser("post", url)
 | 
			
		||||
            self.check_authorized_superuser("post", unauthorized_url)
 | 
			
		||||
 | 
			
		||||
            user = self.create_user_with_roles([])
 | 
			
		||||
            self.client.force_authenticate(user=user)  # type: ignore
 | 
			
		||||
 | 
			
		||||
            # test user without role
 | 
			
		||||
            self.check_not_authorized("post", url)
 | 
			
		||||
            self.check_not_authorized("post", unauthorized_url)
 | 
			
		||||
 | 
			
		||||
            # add user to role and test
 | 
			
		||||
            setattr(
 | 
			
		||||
                user.role,
 | 
			
		||||
                "can_manage_checks" if action == "reset" else "can_run_checks",
 | 
			
		||||
                True,
 | 
			
		||||
            )
 | 
			
		||||
            user.role.save()
 | 
			
		||||
 | 
			
		||||
            self.check_authorized("post", url)
 | 
			
		||||
            self.check_authorized("post", unauthorized_url)
 | 
			
		||||
 | 
			
		||||
            # limit user to client if agent check
 | 
			
		||||
            user.role.can_view_sites.set([agent.site])
 | 
			
		||||
 | 
			
		||||
            self.check_authorized("post", url)
 | 
			
		||||
            self.check_not_authorized("post", unauthorized_url)
 | 
			
		||||
 | 
			
		||||
    def test_check_history_permissions(self):
 | 
			
		||||
        agent = baker.make_recipe("agents.agent")
 | 
			
		||||
        unauthorized_agent = baker.make_recipe("agents.agent")
 | 
			
		||||
        check = baker.make("checks.Check", agent=agent)
 | 
			
		||||
        unauthorized_check = baker.make("checks.Check", agent=unauthorized_agent)
 | 
			
		||||
 | 
			
		||||
        url = f"{base_url}/{check.id}/history/"
 | 
			
		||||
        unauthorized_url = f"{base_url}/{unauthorized_check.id}/history/"
 | 
			
		||||
 | 
			
		||||
        # test superuser access
 | 
			
		||||
        self.check_authorized_superuser("patch", url)
 | 
			
		||||
        self.check_authorized_superuser("patch", unauthorized_url)
 | 
			
		||||
 | 
			
		||||
        user = self.create_user_with_roles([])
 | 
			
		||||
        self.client.force_authenticate(user=user)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # test user without role
 | 
			
		||||
        self.check_not_authorized("patch", url)
 | 
			
		||||
        self.check_not_authorized("patch", unauthorized_url)
 | 
			
		||||
 | 
			
		||||
        # add user to role and test
 | 
			
		||||
        setattr(
 | 
			
		||||
            user.role,
 | 
			
		||||
            "can_list_checks",
 | 
			
		||||
            True,
 | 
			
		||||
        )
 | 
			
		||||
        user.role.save()
 | 
			
		||||
 | 
			
		||||
        self.check_authorized("patch", url)
 | 
			
		||||
        self.check_authorized("patch", unauthorized_url)
 | 
			
		||||
 | 
			
		||||
        # limit user to client if agent check
 | 
			
		||||
        user.role.can_view_sites.set([agent.site])
 | 
			
		||||
 | 
			
		||||
        self.check_authorized("patch", url)
 | 
			
		||||
        self.check_not_authorized("patch", unauthorized_url)
 | 
			
		||||
 
 | 
			
		||||
@@ -3,10 +3,9 @@ from django.urls import path
 | 
			
		||||
from . import views
 | 
			
		||||
 | 
			
		||||
urlpatterns = [
 | 
			
		||||
    path("checks/", views.AddCheck.as_view()),
 | 
			
		||||
    path("<int:pk>/check/", views.GetUpdateDeleteCheck.as_view()),
 | 
			
		||||
    path("<pk>/loadchecks/", views.load_checks),
 | 
			
		||||
    path("getalldisks/", views.get_disks_for_policies),
 | 
			
		||||
    path("runchecks/<pk>/", views.run_checks),
 | 
			
		||||
    path("history/<int:checkpk>/", views.GetCheckHistory.as_view()),
 | 
			
		||||
    path("", views.GetAddChecks.as_view()),
 | 
			
		||||
    path("<int:pk>/", views.GetUpdateDeleteCheck.as_view()),
 | 
			
		||||
    path("<int:pk>/reset/", views.ResetCheck.as_view()),
 | 
			
		||||
    path("<agent:agent_id>/run/", views.run_checks),
 | 
			
		||||
    path("<int:pk>/history/", views.GetCheckHistory.as_view()),
 | 
			
		||||
]
 | 
			
		||||
 
 | 
			
		||||
@@ -9,57 +9,57 @@ from rest_framework.decorators import api_view, permission_classes
 | 
			
		||||
from rest_framework.permissions import IsAuthenticated
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
from rest_framework.views import APIView
 | 
			
		||||
from rest_framework.exceptions import PermissionDenied
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from automation.models import Policy
 | 
			
		||||
from scripts.models import Script
 | 
			
		||||
from tacticalrmm.utils import notify_error
 | 
			
		||||
from tacticalrmm.permissions import _has_perm_on_agent
 | 
			
		||||
 | 
			
		||||
from .models import Check, CheckHistory
 | 
			
		||||
from .permissions import ManageChecksPerms, RunChecksPerms
 | 
			
		||||
from .permissions import ChecksPerms, RunChecksPerms
 | 
			
		||||
from .serializers import CheckHistorySerializer, CheckSerializer
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AddCheck(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, ManageChecksPerms]
 | 
			
		||||
class GetAddChecks(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, ChecksPerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request, agent_id=None, policy=None):
 | 
			
		||||
        if agent_id:
 | 
			
		||||
            agent = get_object_or_404(Agent, agent_id=agent_id)
 | 
			
		||||
            checks = Check.objects.filter(agent=agent)
 | 
			
		||||
        elif policy:
 | 
			
		||||
            policy = get_object_or_404(Policy, id=policy)
 | 
			
		||||
            checks = Check.objects.filter(policy=policy)
 | 
			
		||||
        else:
 | 
			
		||||
            checks = Check.objects.filter_by_role(request.user)
 | 
			
		||||
        return Response(CheckSerializer(checks, many=True).data)
 | 
			
		||||
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
        from automation.tasks import generate_agent_checks_task
 | 
			
		||||
 | 
			
		||||
        policy = None
 | 
			
		||||
        agent = None
 | 
			
		||||
        data = request.data.copy()
 | 
			
		||||
        # Determine if adding check to Agent and replace agent_id with pk
 | 
			
		||||
        if "agent" in data.keys():
 | 
			
		||||
            agent = get_object_or_404(Agent, agent_id=data["agent"])
 | 
			
		||||
            if not _has_perm_on_agent(request.user, agent.agent_id):
 | 
			
		||||
                raise PermissionDenied()
 | 
			
		||||
 | 
			
		||||
        # Determine if adding check to Policy or Agent
 | 
			
		||||
        if "policy" in request.data:
 | 
			
		||||
            policy = get_object_or_404(Policy, id=request.data["policy"])
 | 
			
		||||
            # Object used for filter and save
 | 
			
		||||
            parent = {"policy": policy}
 | 
			
		||||
        else:
 | 
			
		||||
            agent = get_object_or_404(Agent, pk=request.data["pk"])
 | 
			
		||||
            parent = {"agent": agent}
 | 
			
		||||
 | 
			
		||||
        script = None
 | 
			
		||||
        if "script" in request.data["check"]:
 | 
			
		||||
            script = get_object_or_404(Script, pk=request.data["check"]["script"])
 | 
			
		||||
            data["agent"] = agent.pk
 | 
			
		||||
 | 
			
		||||
        # set event id to 0 if wildcard because it needs to be an integer field for db
 | 
			
		||||
        # will be ignored anyway by the agent when doing wildcard check
 | 
			
		||||
        if (
 | 
			
		||||
            request.data["check"]["check_type"] == "eventlog"
 | 
			
		||||
            and request.data["check"]["event_id_is_wildcard"]
 | 
			
		||||
        ):
 | 
			
		||||
            request.data["check"]["event_id"] = 0
 | 
			
		||||
        if data["check_type"] == "eventlog" and data["event_id_is_wildcard"]:
 | 
			
		||||
            data["event_id"] = 0
 | 
			
		||||
 | 
			
		||||
        serializer = CheckSerializer(
 | 
			
		||||
            data=request.data["check"], partial=True, context=parent
 | 
			
		||||
        )
 | 
			
		||||
        serializer = CheckSerializer(data=data, partial=True)
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        new_check = serializer.save(**parent, script=script)
 | 
			
		||||
        new_check = serializer.save()
 | 
			
		||||
 | 
			
		||||
        # Generate policy Checks
 | 
			
		||||
        if policy:
 | 
			
		||||
            generate_agent_checks_task.delay(policy=policy.pk)
 | 
			
		||||
        elif agent:
 | 
			
		||||
        if "policy" in data.keys():
 | 
			
		||||
            generate_agent_checks_task.delay(policy=data["policy"])
 | 
			
		||||
        elif "agent" in data.keys():
 | 
			
		||||
            checks = agent.agentchecks.filter(  # type: ignore
 | 
			
		||||
                check_type=new_check.check_type, managed_by_policy=True
 | 
			
		||||
            )
 | 
			
		||||
@@ -81,44 +81,43 @@ class AddCheck(APIView):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetUpdateDeleteCheck(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, ManageChecksPerms]
 | 
			
		||||
    permission_classes = [IsAuthenticated, ChecksPerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request, pk):
 | 
			
		||||
        check = get_object_or_404(Check, pk=pk)
 | 
			
		||||
        if check.agent and not _has_perm_on_agent(request.user, check.agent.agent_id):
 | 
			
		||||
            raise PermissionDenied()
 | 
			
		||||
 | 
			
		||||
        return Response(CheckSerializer(check).data)
 | 
			
		||||
 | 
			
		||||
    def patch(self, request, pk):
 | 
			
		||||
    def put(self, request, pk):
 | 
			
		||||
        from automation.tasks import update_policy_check_fields_task
 | 
			
		||||
 | 
			
		||||
        check = get_object_or_404(Check, pk=pk)
 | 
			
		||||
 | 
			
		||||
        data = request.data.copy()
 | 
			
		||||
 | 
			
		||||
        if check.agent and not _has_perm_on_agent(request.user, check.agent.agent_id):
 | 
			
		||||
            raise PermissionDenied()
 | 
			
		||||
 | 
			
		||||
        # remove fields that should not be changed when editing a check from the frontend
 | 
			
		||||
        if (
 | 
			
		||||
            "check_alert" not in request.data.keys()
 | 
			
		||||
            and "check_reset" not in request.data.keys()
 | 
			
		||||
        ):
 | 
			
		||||
            [request.data.pop(i) for i in check.non_editable_fields]
 | 
			
		||||
        [data.pop(i) for i in Check.non_editable_fields() if i in data.keys()]
 | 
			
		||||
 | 
			
		||||
        # set event id to 0 if wildcard because it needs to be an integer field for db
 | 
			
		||||
        # will be ignored anyway by the agent when doing wildcard check
 | 
			
		||||
        if check.check_type == "eventlog":
 | 
			
		||||
            try:
 | 
			
		||||
                request.data["event_id_is_wildcard"]
 | 
			
		||||
                data["event_id_is_wildcard"]
 | 
			
		||||
            except KeyError:
 | 
			
		||||
                pass
 | 
			
		||||
            else:
 | 
			
		||||
                if request.data["event_id_is_wildcard"]:
 | 
			
		||||
                    request.data["event_id"] = 0
 | 
			
		||||
                if data["event_id_is_wildcard"]:
 | 
			
		||||
                    data["event_id"] = 0
 | 
			
		||||
 | 
			
		||||
        serializer = CheckSerializer(instance=check, data=request.data, partial=True)
 | 
			
		||||
        serializer = CheckSerializer(instance=check, data=data, partial=True)
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        check = serializer.save()
 | 
			
		||||
 | 
			
		||||
        # resolve any alerts that are open
 | 
			
		||||
        if "check_reset" in request.data.keys():
 | 
			
		||||
            if check.alert.filter(resolved=False).exists():
 | 
			
		||||
                check.alert.get(resolved=False).resolve()
 | 
			
		||||
 | 
			
		||||
        if check.policy:
 | 
			
		||||
            update_policy_check_fields_task.delay(check=check.pk)
 | 
			
		||||
 | 
			
		||||
@@ -129,6 +128,9 @@ class GetUpdateDeleteCheck(APIView):
 | 
			
		||||
 | 
			
		||||
        check = get_object_or_404(Check, pk=pk)
 | 
			
		||||
 | 
			
		||||
        if check.agent and not _has_perm_on_agent(request.user, check.agent.agent_id):
 | 
			
		||||
            raise PermissionDenied()
 | 
			
		||||
 | 
			
		||||
        check.delete()
 | 
			
		||||
 | 
			
		||||
        # Policy check deleted
 | 
			
		||||
@@ -137,18 +139,42 @@ class GetUpdateDeleteCheck(APIView):
 | 
			
		||||
 | 
			
		||||
            # Re-evaluate agent checks is policy was enforced
 | 
			
		||||
            if check.policy.enforced:
 | 
			
		||||
                generate_agent_checks_task.delay(policy=check.policy)
 | 
			
		||||
                generate_agent_checks_task.delay(policy=check.policy.pk)
 | 
			
		||||
 | 
			
		||||
        # Agent check deleted
 | 
			
		||||
        elif check.agent:
 | 
			
		||||
            check.agent.generate_checks_from_policies()
 | 
			
		||||
            generate_agent_checks_task.delay(agents=[check.agent.pk])
 | 
			
		||||
 | 
			
		||||
        return Response(f"{check.readable_desc} was deleted!")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ResetCheck(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, ChecksPerms]
 | 
			
		||||
 | 
			
		||||
    def post(self, request, pk):
 | 
			
		||||
        check = get_object_or_404(Check, pk=pk)
 | 
			
		||||
 | 
			
		||||
        if check.agent and not _has_perm_on_agent(request.user, check.agent.agent_id):
 | 
			
		||||
            raise PermissionDenied()
 | 
			
		||||
 | 
			
		||||
        check.status = "passing"
 | 
			
		||||
        check.save()
 | 
			
		||||
 | 
			
		||||
        # resolve any alerts that are open
 | 
			
		||||
        if check.alert.filter(resolved=False).exists():
 | 
			
		||||
            check.alert.get(resolved=False).resolve()
 | 
			
		||||
 | 
			
		||||
        return Response("The check status was reset")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetCheckHistory(APIView):
 | 
			
		||||
    def patch(self, request, checkpk):
 | 
			
		||||
        check = get_object_or_404(Check, pk=checkpk)
 | 
			
		||||
    permission_classes = [IsAuthenticated, ChecksPerms]
 | 
			
		||||
 | 
			
		||||
    def patch(self, request, pk):
 | 
			
		||||
        check = get_object_or_404(Check, pk=pk)
 | 
			
		||||
 | 
			
		||||
        if check.agent and not _has_perm_on_agent(request.user, check.agent.agent_id):
 | 
			
		||||
            raise PermissionDenied()
 | 
			
		||||
 | 
			
		||||
        timeFilter = Q()
 | 
			
		||||
 | 
			
		||||
@@ -160,7 +186,7 @@ class GetCheckHistory(APIView):
 | 
			
		||||
                    - djangotime.timedelta(days=request.data["timeFilter"]),
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
        check_history = CheckHistory.objects.filter(check_id=checkpk).filter(timeFilter).order_by("-x")  # type: ignore
 | 
			
		||||
        check_history = CheckHistory.objects.filter(check_id=pk).filter(timeFilter).order_by("-x")  # type: ignore
 | 
			
		||||
 | 
			
		||||
        return Response(
 | 
			
		||||
            CheckHistorySerializer(
 | 
			
		||||
@@ -171,8 +197,8 @@ class GetCheckHistory(APIView):
 | 
			
		||||
 | 
			
		||||
@api_view()
 | 
			
		||||
@permission_classes([IsAuthenticated, RunChecksPerms])
 | 
			
		||||
def run_checks(request, pk):
 | 
			
		||||
    agent = get_object_or_404(Agent, pk=pk)
 | 
			
		||||
def run_checks(request, agent_id):
 | 
			
		||||
    agent = get_object_or_404(Agent, agent_id=agent_id)
 | 
			
		||||
 | 
			
		||||
    if pyver.parse(agent.version) >= pyver.parse("1.4.1"):
 | 
			
		||||
        r = asyncio.run(agent.nats_cmd({"func": "runchecks"}, timeout=15))
 | 
			
		||||
@@ -185,14 +211,3 @@ def run_checks(request, pk):
 | 
			
		||||
    else:
 | 
			
		||||
        asyncio.run(agent.nats_cmd({"func": "runchecks"}, wait=False))
 | 
			
		||||
        return Response(f"Checks will now be re-run on {agent.hostname}")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view()
 | 
			
		||||
def load_checks(request, pk):
 | 
			
		||||
    checks = Check.objects.filter(agent__pk=pk)
 | 
			
		||||
    return Response(CheckSerializer(checks, many=True).data)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view()
 | 
			
		||||
def get_disks_for_policies(request):
 | 
			
		||||
    return Response(Check.all_disks())
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,33 @@
 | 
			
		||||
# Generated by Django 3.2.6 on 2021-10-10 02:49
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('clients', '0017_auto_20210417_0125'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='client',
 | 
			
		||||
            name='created_by',
 | 
			
		||||
            field=models.CharField(blank=True, max_length=255, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='client',
 | 
			
		||||
            name='modified_by',
 | 
			
		||||
            field=models.CharField(blank=True, max_length=255, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='site',
 | 
			
		||||
            name='created_by',
 | 
			
		||||
            field=models.CharField(blank=True, max_length=255, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='site',
 | 
			
		||||
            name='modified_by',
 | 
			
		||||
            field=models.CharField(blank=True, max_length=255, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -0,0 +1,17 @@
 | 
			
		||||
# Generated by Django 3.2.6 on 2021-10-28 00:12
 | 
			
		||||
 | 
			
		||||
from django.db import migrations
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('clients', '0018_auto_20211010_0249'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='deployment',
 | 
			
		||||
            name='client',
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -5,9 +5,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()
 | 
			
		||||
 
 | 
			
		||||
@@ -1,27 +1,45 @@
 | 
			
		||||
from rest_framework import permissions
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.permissions import _has_perm
 | 
			
		||||
from tacticalrmm.permissions import _has_perm, _has_perm_on_client, _has_perm_on_site
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ManageClientsPerms(permissions.BasePermission):
 | 
			
		||||
class ClientsPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        if r.method == "GET":
 | 
			
		||||
            return True
 | 
			
		||||
 | 
			
		||||
        return _has_perm(r, "can_manage_clients")
 | 
			
		||||
            if "pk" in view.kwargs.keys():
 | 
			
		||||
                return _has_perm(r, "can_list_clients") and _has_perm_on_client(
 | 
			
		||||
                    r.user, view.kwargs["pk"]
 | 
			
		||||
                )
 | 
			
		||||
            else:
 | 
			
		||||
                return _has_perm(r, "can_list_clients")
 | 
			
		||||
        elif r.method == "PUT" or r.method == "DELETE":
 | 
			
		||||
            return _has_perm(r, "can_manage_clients") and _has_perm_on_client(
 | 
			
		||||
                r.user, view.kwargs["pk"]
 | 
			
		||||
            )
 | 
			
		||||
        else:
 | 
			
		||||
            return _has_perm(r, "can_manage_clients")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ManageSitesPerms(permissions.BasePermission):
 | 
			
		||||
class SitesPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        if r.method == "GET":
 | 
			
		||||
            return True
 | 
			
		||||
 | 
			
		||||
        return _has_perm(r, "can_manage_sites")
 | 
			
		||||
            if "pk" in view.kwargs.keys():
 | 
			
		||||
                return _has_perm(r, "can_list_sites") and _has_perm_on_site(
 | 
			
		||||
                    r.user, view.kwargs["pk"]
 | 
			
		||||
                )
 | 
			
		||||
            else:
 | 
			
		||||
                return _has_perm(r, "can_list_sites")
 | 
			
		||||
        elif r.method == "PUT" or r.method == "DELETE":
 | 
			
		||||
            return _has_perm(r, "can_manage_sites") and _has_perm_on_site(
 | 
			
		||||
                r.user, view.kwargs["pk"]
 | 
			
		||||
            )
 | 
			
		||||
        else:
 | 
			
		||||
            return _has_perm(r, "can_manage_sites")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ManageDeploymentPerms(permissions.BasePermission):
 | 
			
		||||
class DeploymentPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        if r.method == "GET":
 | 
			
		||||
            return True
 | 
			
		||||
 | 
			
		||||
        return _has_perm(r, "can_manage_deployments")
 | 
			
		||||
            return _has_perm(r, "can_list_deployments")
 | 
			
		||||
        else:
 | 
			
		||||
            return _has_perm(r, "can_manage_deployments")
 | 
			
		||||
 
 | 
			
		||||
@@ -1,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,397 @@ 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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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,38 +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, IsAuthenticated
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
from rest_framework.views import APIView
 | 
			
		||||
from rest_framework.exceptions import PermissionDenied
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from core.models import CoreSettings
 | 
			
		||||
from tacticalrmm.utils import notify_error
 | 
			
		||||
from tacticalrmm.permissions import _has_perm_on_client, _has_perm_on_site
 | 
			
		||||
 | 
			
		||||
from .models import Client, ClientCustomField, Deployment, Site, SiteCustomField
 | 
			
		||||
from .permissions import ManageClientsPerms, ManageDeploymentPerms, ManageSitesPerms
 | 
			
		||||
from .permissions import (
 | 
			
		||||
    ClientsPerms,
 | 
			
		||||
    DeploymentPerms,
 | 
			
		||||
    SitesPerms,
 | 
			
		||||
)
 | 
			
		||||
from .serializers import (
 | 
			
		||||
    ClientCustomFieldSerializer,
 | 
			
		||||
    ClientSerializer,
 | 
			
		||||
    ClientTreeSerializer,
 | 
			
		||||
    DeploymentSerializer,
 | 
			
		||||
    SiteCustomFieldSerializer,
 | 
			
		||||
    SiteSerializer,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
logger.configure(**settings.LOG_CONFIG)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetAddClients(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, ManageClientsPerms]
 | 
			
		||||
    permission_classes = [IsAuthenticated, ClientsPerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request):
 | 
			
		||||
        clients = Client.objects.all()
 | 
			
		||||
        return Response(ClientSerializer(clients, many=True).data)
 | 
			
		||||
        clients = Client.objects.select_related(
 | 
			
		||||
            "workstation_policy", "server_policy", "alert_template"
 | 
			
		||||
        ).filter_by_role(request.user)
 | 
			
		||||
        return Response(
 | 
			
		||||
            ClientSerializer(clients, context={"user": request.user}, many=True).data
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
        # create client
 | 
			
		||||
@@ -71,15 +76,15 @@ class GetAddClients(APIView):
 | 
			
		||||
                serializer.is_valid(raise_exception=True)
 | 
			
		||||
                serializer.save()
 | 
			
		||||
 | 
			
		||||
        return Response(f"{client} was added!")
 | 
			
		||||
        return Response(f"{client.name} was added")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetUpdateClient(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, ManageClientsPerms]
 | 
			
		||||
class GetUpdateDeleteClient(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, ClientsPerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request, pk):
 | 
			
		||||
        client = get_object_or_404(Client, pk=pk)
 | 
			
		||||
        return Response(ClientSerializer(client).data)
 | 
			
		||||
        return Response(ClientSerializer(client, context={"user": request.user}).data)
 | 
			
		||||
 | 
			
		||||
    def put(self, request, pk):
 | 
			
		||||
        client = get_object_or_404(Client, pk=pk)
 | 
			
		||||
@@ -111,46 +116,41 @@ class GetUpdateClient(APIView):
 | 
			
		||||
                    serializer.is_valid(raise_exception=True)
 | 
			
		||||
                    serializer.save()
 | 
			
		||||
 | 
			
		||||
        return Response("The Client was updated")
 | 
			
		||||
        return Response("{client} was updated")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DeleteClient(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, ManageClientsPerms]
 | 
			
		||||
 | 
			
		||||
    def delete(self, request, pk, sitepk):
 | 
			
		||||
    def delete(self, request, pk):
 | 
			
		||||
        from automation.tasks import generate_agent_checks_task
 | 
			
		||||
 | 
			
		||||
        client = get_object_or_404(Client, pk=pk)
 | 
			
		||||
        agents = Agent.objects.filter(site__client=client)
 | 
			
		||||
 | 
			
		||||
        if not sitepk:
 | 
			
		||||
        # only run tasks if it affects clients
 | 
			
		||||
        if client.agent_count > 0 and "move_to_site" in request.query_params.keys():
 | 
			
		||||
            agents = Agent.objects.filter(site__client=client)
 | 
			
		||||
            site = get_object_or_404(Site, pk=request.query_params["move_to_site"])
 | 
			
		||||
            agents.update(site=site)
 | 
			
		||||
            generate_agent_checks_task.delay(all=True, create_tasks=True)
 | 
			
		||||
 | 
			
		||||
        elif client.agent_count > 0:
 | 
			
		||||
            return notify_error(
 | 
			
		||||
                "There needs to be a site specified to move existing agents to"
 | 
			
		||||
                "Agents exist under this client. There needs to be a site specified to move existing agents to"
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        site = get_object_or_404(Site, pk=sitepk)
 | 
			
		||||
        agents.update(site=site)
 | 
			
		||||
 | 
			
		||||
        generate_agent_checks_task.delay(all=True, create_tasks=True)
 | 
			
		||||
 | 
			
		||||
        client.delete()
 | 
			
		||||
        return Response(f"{client.name} was deleted!")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetClientTree(APIView):
 | 
			
		||||
    def get(self, request):
 | 
			
		||||
        clients = Client.objects.all()
 | 
			
		||||
        return Response(ClientTreeSerializer(clients, many=True).data)
 | 
			
		||||
        return Response(f"{client.name} was deleted")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetAddSites(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, ManageSitesPerms]
 | 
			
		||||
    permission_classes = [IsAuthenticated, SitesPerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request):
 | 
			
		||||
        sites = Site.objects.all()
 | 
			
		||||
        sites = Site.objects.filter_by_role(request.user)
 | 
			
		||||
        return Response(SiteSerializer(sites, many=True).data)
 | 
			
		||||
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
 | 
			
		||||
        if not _has_perm_on_client(request.user, request.data["site"]["client"]):
 | 
			
		||||
            raise PermissionDenied()
 | 
			
		||||
 | 
			
		||||
        serializer = SiteSerializer(data=request.data["site"])
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        site = serializer.save()
 | 
			
		||||
@@ -170,8 +170,8 @@ class GetAddSites(APIView):
 | 
			
		||||
        return Response(f"Site {site.name} was added!")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetUpdateSite(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, ManageSitesPerms]
 | 
			
		||||
class GetUpdateDeleteSite(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, SitesPerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request, pk):
 | 
			
		||||
        site = get_object_or_404(Site, pk=pk)
 | 
			
		||||
@@ -212,55 +212,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):
 | 
			
		||||
    permission_classes = [IsAuthenticated, ManageSitesPerms]
 | 
			
		||||
 | 
			
		||||
    def delete(self, request, pk, sitepk):
 | 
			
		||||
    def delete(self, request, pk):
 | 
			
		||||
        from automation.tasks import generate_agent_checks_task
 | 
			
		||||
 | 
			
		||||
        site = get_object_or_404(Site, pk=pk)
 | 
			
		||||
        if site.client.sites.count() == 1:
 | 
			
		||||
            return notify_error("A client must have at least 1 site.")
 | 
			
		||||
 | 
			
		||||
        agents = Agent.objects.filter(site=site)
 | 
			
		||||
        # only run tasks if it affects clients
 | 
			
		||||
        if site.agent_count > 0 and "move_to_site" in request.query_params.keys():
 | 
			
		||||
            agents = Agent.objects.filter(site=site)
 | 
			
		||||
            new_site = get_object_or_404(Site, pk=request.query_params["move_to_site"])
 | 
			
		||||
            agents.update(site=new_site)
 | 
			
		||||
            generate_agent_checks_task.delay(all=True, create_tasks=True)
 | 
			
		||||
 | 
			
		||||
        if not sitepk:
 | 
			
		||||
        elif site.agent_count > 0:
 | 
			
		||||
            return notify_error(
 | 
			
		||||
                "There needs to be a site specified to move the agents to"
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        agent_site = get_object_or_404(Site, pk=sitepk)
 | 
			
		||||
 | 
			
		||||
        agents.update(site=agent_site)
 | 
			
		||||
 | 
			
		||||
        generate_agent_checks_task.delay(all=True, create_tasks=True)
 | 
			
		||||
 | 
			
		||||
        site.delete()
 | 
			
		||||
        return Response(f"{site.name} was deleted!")
 | 
			
		||||
        return Response(f"{site.name} was deleted")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AgentDeployment(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, ManageDeploymentPerms]
 | 
			
		||||
    permission_classes = [IsAuthenticated, DeploymentPerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request):
 | 
			
		||||
        deps = Deployment.objects.all()
 | 
			
		||||
        deps = Deployment.objects.filter_by_role(request.user)
 | 
			
		||||
        return Response(DeploymentSerializer(deps, many=True).data)
 | 
			
		||||
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
        from knox.models import AuthToken
 | 
			
		||||
        from accounts.models import User
 | 
			
		||||
 | 
			
		||||
        client = get_object_or_404(Client, pk=request.data["client"])
 | 
			
		||||
        site = get_object_or_404(Site, pk=request.data["site"])
 | 
			
		||||
 | 
			
		||||
        if not _has_perm_on_site(request.user, site.pk):
 | 
			
		||||
            raise PermissionDenied()
 | 
			
		||||
 | 
			
		||||
        installer_user = User.objects.filter(is_installer_user=True).first()
 | 
			
		||||
 | 
			
		||||
        expires = dt.datetime.strptime(
 | 
			
		||||
            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"],
 | 
			
		||||
@@ -269,7 +269,6 @@ class AgentDeployment(APIView):
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Deployment(
 | 
			
		||||
            client=client,
 | 
			
		||||
            site=site,
 | 
			
		||||
            expiry=expires,
 | 
			
		||||
            mon_type=request.data["agenttype"],
 | 
			
		||||
@@ -278,17 +277,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
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@ from django.core.management.base import BaseCommand
 | 
			
		||||
 | 
			
		||||
from logs.models import PendingAction
 | 
			
		||||
from scripts.models import Script
 | 
			
		||||
from accounts.models import User
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
@@ -13,3 +14,9 @@ class Command(BaseCommand):
 | 
			
		||||
 | 
			
		||||
        # load community scripts into the db
 | 
			
		||||
        Script.load_community_scripts()
 | 
			
		||||
 | 
			
		||||
        # make sure installer user is set to block_dashboard_logins
 | 
			
		||||
        if User.objects.filter(is_installer_user=True).exists():
 | 
			
		||||
            for user in User.objects.filter(is_installer_user=True):
 | 
			
		||||
                user.block_dashboard_login = True
 | 
			
		||||
                user.save()
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										28
									
								
								api/tacticalrmm/core/migrations/0025_auto_20210707_1835.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								api/tacticalrmm/core/migrations/0025_auto_20210707_1835.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,28 @@
 | 
			
		||||
# Generated by Django 3.2.1 on 2021-07-07 18:35
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('core', '0024_auto_20210707_1828'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='coresettings',
 | 
			
		||||
            name='agent_debug_level',
 | 
			
		||||
            field=models.CharField(choices=[('info', 'Info'), ('warning', 'Warning'), ('error', 'Error'), ('critical', 'Critical')], default='info', max_length=20),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='coresettings',
 | 
			
		||||
            name='debug_log_prune_days',
 | 
			
		||||
            field=models.PositiveIntegerField(default=30),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='coresettings',
 | 
			
		||||
            name='agent_history_prune_days',
 | 
			
		||||
            field=models.PositiveIntegerField(default=60),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -0,0 +1,18 @@
 | 
			
		||||
# Generated by Django 3.2.1 on 2021-07-21 17:40
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('core', '0025_auto_20210707_1835'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='coresettings',
 | 
			
		||||
            name='audit_log_prune_days',
 | 
			
		||||
            field=models.PositiveIntegerField(default=0),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										73
									
								
								api/tacticalrmm/core/migrations/0027_auto_20210905_1606.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								api/tacticalrmm/core/migrations/0027_auto_20210905_1606.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,73 @@
 | 
			
		||||
# Generated by Django 3.2.6 on 2021-09-05 16:06
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('core', '0026_coresettings_audit_log_prune_days'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='customfield',
 | 
			
		||||
            name='created_by',
 | 
			
		||||
            field=models.CharField(blank=True, max_length=100, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='customfield',
 | 
			
		||||
            name='created_time',
 | 
			
		||||
            field=models.DateTimeField(auto_now_add=True, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='customfield',
 | 
			
		||||
            name='modified_by',
 | 
			
		||||
            field=models.CharField(blank=True, max_length=100, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='customfield',
 | 
			
		||||
            name='modified_time',
 | 
			
		||||
            field=models.DateTimeField(auto_now=True, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='globalkvstore',
 | 
			
		||||
            name='created_by',
 | 
			
		||||
            field=models.CharField(blank=True, max_length=100, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='globalkvstore',
 | 
			
		||||
            name='created_time',
 | 
			
		||||
            field=models.DateTimeField(auto_now_add=True, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='globalkvstore',
 | 
			
		||||
            name='modified_by',
 | 
			
		||||
            field=models.CharField(blank=True, max_length=100, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='globalkvstore',
 | 
			
		||||
            name='modified_time',
 | 
			
		||||
            field=models.DateTimeField(auto_now=True, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='urlaction',
 | 
			
		||||
            name='created_by',
 | 
			
		||||
            field=models.CharField(blank=True, max_length=100, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='urlaction',
 | 
			
		||||
            name='created_time',
 | 
			
		||||
            field=models.DateTimeField(auto_now_add=True, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='urlaction',
 | 
			
		||||
            name='modified_by',
 | 
			
		||||
            field=models.CharField(blank=True, max_length=100, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='urlaction',
 | 
			
		||||
            name='modified_time',
 | 
			
		||||
            field=models.DateTimeField(auto_now=True, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										53
									
								
								api/tacticalrmm/core/migrations/0028_auto_20210917_1954.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								api/tacticalrmm/core/migrations/0028_auto_20210917_1954.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,53 @@
 | 
			
		||||
# Generated by Django 3.2.6 on 2021-09-17 19:54
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("core", "0027_auto_20210905_1606"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="coresettings",
 | 
			
		||||
            name="created_by",
 | 
			
		||||
            field=models.CharField(blank=True, max_length=255, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="coresettings",
 | 
			
		||||
            name="modified_by",
 | 
			
		||||
            field=models.CharField(blank=True, max_length=255, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="customfield",
 | 
			
		||||
            name="created_by",
 | 
			
		||||
            field=models.CharField(blank=True, max_length=255, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="customfield",
 | 
			
		||||
            name="modified_by",
 | 
			
		||||
            field=models.CharField(blank=True, max_length=255, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="globalkvstore",
 | 
			
		||||
            name="created_by",
 | 
			
		||||
            field=models.CharField(blank=True, max_length=255, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="globalkvstore",
 | 
			
		||||
            name="modified_by",
 | 
			
		||||
            field=models.CharField(blank=True, max_length=255, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="urlaction",
 | 
			
		||||
            name="created_by",
 | 
			
		||||
            field=models.CharField(blank=True, max_length=255, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="urlaction",
 | 
			
		||||
            name="modified_by",
 | 
			
		||||
            field=models.CharField(blank=True, max_length=255, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -1,3 +1,4 @@
 | 
			
		||||
import requests
 | 
			
		||||
import smtplib
 | 
			
		||||
from email.message import EmailMessage
 | 
			
		||||
 | 
			
		||||
@@ -6,12 +7,10 @@ from django.conf import settings
 | 
			
		||||
from django.contrib.postgres.fields import ArrayField
 | 
			
		||||
from django.core.exceptions import ValidationError
 | 
			
		||||
from django.db import models
 | 
			
		||||
from loguru import logger
 | 
			
		||||
from twilio.rest import Client as TwClient
 | 
			
		||||
from twilio.base.exceptions import TwilioRestException
 | 
			
		||||
 | 
			
		||||
from logs.models import BaseAuditModel
 | 
			
		||||
 | 
			
		||||
logger.configure(**settings.LOG_CONFIG)
 | 
			
		||||
from logs.models import BaseAuditModel, DebugLog, LOG_LEVEL_CHOICES
 | 
			
		||||
 | 
			
		||||
TZ_CHOICES = [(_, _) for _ in pytz.all_timezones]
 | 
			
		||||
 | 
			
		||||
@@ -51,6 +50,13 @@ class CoreSettings(BaseAuditModel):
 | 
			
		||||
    )
 | 
			
		||||
    # removes check history older than days
 | 
			
		||||
    check_history_prune_days = models.PositiveIntegerField(default=30)
 | 
			
		||||
    resolved_alerts_prune_days = models.PositiveIntegerField(default=0)
 | 
			
		||||
    agent_history_prune_days = models.PositiveIntegerField(default=60)
 | 
			
		||||
    debug_log_prune_days = models.PositiveIntegerField(default=30)
 | 
			
		||||
    audit_log_prune_days = models.PositiveIntegerField(default=0)
 | 
			
		||||
    agent_debug_level = models.CharField(
 | 
			
		||||
        max_length=20, choices=LOG_LEVEL_CHOICES, default="info"
 | 
			
		||||
    )
 | 
			
		||||
    clear_faults_days = models.IntegerField(default=0)
 | 
			
		||||
    mesh_token = models.CharField(max_length=255, null=True, blank=True, default="")
 | 
			
		||||
    mesh_username = models.CharField(max_length=255, null=True, blank=True, default="")
 | 
			
		||||
@@ -147,7 +153,7 @@ class CoreSettings(BaseAuditModel):
 | 
			
		||||
 | 
			
		||||
    def send_mail(self, subject, body, alert_template=None, test=False):
 | 
			
		||||
 | 
			
		||||
        if not alert_template and not self.email_is_configured:
 | 
			
		||||
        if not alert_template or not self.email_is_configured:
 | 
			
		||||
            if test:
 | 
			
		||||
                return "Missing required fields (need at least 1 recipient)"
 | 
			
		||||
            return False
 | 
			
		||||
@@ -184,28 +190,35 @@ class CoreSettings(BaseAuditModel):
 | 
			
		||||
                    server.quit()
 | 
			
		||||
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            logger.error(f"Sending email failed with error: {e}")
 | 
			
		||||
            DebugLog.error(message=f"Sending email failed with error: {e}")
 | 
			
		||||
            if test:
 | 
			
		||||
                return str(e)
 | 
			
		||||
        else:
 | 
			
		||||
            return True
 | 
			
		||||
 | 
			
		||||
    def send_sms(self, body, alert_template=None):
 | 
			
		||||
        if not alert_template and not self.sms_is_configured:
 | 
			
		||||
            return
 | 
			
		||||
    def send_sms(self, body, alert_template=None, test=False):
 | 
			
		||||
        if not alert_template or not self.sms_is_configured:
 | 
			
		||||
            return "Sms alerting is not setup correctly."
 | 
			
		||||
 | 
			
		||||
        # override email recipients if alert_template is passed and is set
 | 
			
		||||
        if alert_template and alert_template.text_recipients:
 | 
			
		||||
            text_recipients = alert_template.email_recipients
 | 
			
		||||
            text_recipients = alert_template.text_recipients
 | 
			
		||||
        else:
 | 
			
		||||
            text_recipients = self.sms_alert_recipients
 | 
			
		||||
 | 
			
		||||
        if not text_recipients:
 | 
			
		||||
            return "No sms recipients found"
 | 
			
		||||
 | 
			
		||||
        tw_client = TwClient(self.twilio_account_sid, self.twilio_auth_token)
 | 
			
		||||
        for num in text_recipients:
 | 
			
		||||
            try:
 | 
			
		||||
                tw_client.messages.create(body=body, to=num, from_=self.twilio_number)
 | 
			
		||||
            except Exception as e:
 | 
			
		||||
                logger.error(f"SMS failed to send: {e}")
 | 
			
		||||
            except TwilioRestException as e:
 | 
			
		||||
                DebugLog.error(message=f"SMS failed to send: {e}")
 | 
			
		||||
                if test:
 | 
			
		||||
                    return str(e)
 | 
			
		||||
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def serialize(core):
 | 
			
		||||
@@ -227,7 +240,7 @@ FIELD_TYPE_CHOICES = (
 | 
			
		||||
MODEL_CHOICES = (("client", "Client"), ("site", "Site"), ("agent", "Agent"))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CustomField(models.Model):
 | 
			
		||||
class CustomField(BaseAuditModel):
 | 
			
		||||
 | 
			
		||||
    order = models.PositiveIntegerField(default=0)
 | 
			
		||||
    model = models.CharField(max_length=25, choices=MODEL_CHOICES)
 | 
			
		||||
@@ -256,6 +269,12 @@ class CustomField(models.Model):
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return self.name
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def serialize(field):
 | 
			
		||||
        from .serializers import CustomFieldSerializer
 | 
			
		||||
 | 
			
		||||
        return CustomFieldSerializer(field).data
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def default_value(self):
 | 
			
		||||
        if self.type == "multiple":
 | 
			
		||||
@@ -265,6 +284,26 @@ class CustomField(models.Model):
 | 
			
		||||
        else:
 | 
			
		||||
            return self.default_value_string
 | 
			
		||||
 | 
			
		||||
    def get_or_create_field_value(self, instance):
 | 
			
		||||
        from agents.models import Agent, AgentCustomField
 | 
			
		||||
        from clients.models import Client, ClientCustomField, Site, SiteCustomField
 | 
			
		||||
 | 
			
		||||
        if isinstance(instance, Agent):
 | 
			
		||||
            if AgentCustomField.objects.filter(field=self, agent=instance).exists():
 | 
			
		||||
                return AgentCustomField.objects.get(field=self, agent=instance)
 | 
			
		||||
            else:
 | 
			
		||||
                return AgentCustomField.objects.create(field=self, agent=instance)
 | 
			
		||||
        elif isinstance(instance, Client):
 | 
			
		||||
            if ClientCustomField.objects.filter(field=self, client=instance).exists():
 | 
			
		||||
                return ClientCustomField.objects.get(field=self, client=instance)
 | 
			
		||||
            else:
 | 
			
		||||
                return ClientCustomField.objects.create(field=self, client=instance)
 | 
			
		||||
        elif isinstance(instance, Site):
 | 
			
		||||
            if SiteCustomField.objects.filter(field=self, site=instance).exists():
 | 
			
		||||
                return SiteCustomField.objects.get(field=self, site=instance)
 | 
			
		||||
            else:
 | 
			
		||||
                return SiteCustomField.objects.create(field=self, site=instance)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CodeSignToken(models.Model):
 | 
			
		||||
    token = models.CharField(max_length=255, null=True, blank=True)
 | 
			
		||||
@@ -275,23 +314,63 @@ class CodeSignToken(models.Model):
 | 
			
		||||
 | 
			
		||||
        super(CodeSignToken, self).save(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def is_valid(self) -> bool:
 | 
			
		||||
        if not self.token:
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
        errors = []
 | 
			
		||||
        for url in settings.EXE_GEN_URLS:
 | 
			
		||||
            try:
 | 
			
		||||
                r = requests.post(
 | 
			
		||||
                    f"{url}/api/v1/checktoken",
 | 
			
		||||
                    json={"token": self.token},
 | 
			
		||||
                    headers={"Content-type": "application/json"},
 | 
			
		||||
                    timeout=15,
 | 
			
		||||
                )
 | 
			
		||||
            except Exception as e:
 | 
			
		||||
                errors.append(str(e))
 | 
			
		||||
            else:
 | 
			
		||||
                errors = []
 | 
			
		||||
                break
 | 
			
		||||
 | 
			
		||||
        if errors:
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
        return r.status_code == 200
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return "Code signing token"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GlobalKVStore(models.Model):
 | 
			
		||||
class GlobalKVStore(BaseAuditModel):
 | 
			
		||||
    name = models.CharField(max_length=25)
 | 
			
		||||
    value = models.TextField()
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return self.name
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def serialize(store):
 | 
			
		||||
        from .serializers import KeyStoreSerializer
 | 
			
		||||
 | 
			
		||||
class URLAction(models.Model):
 | 
			
		||||
        return KeyStoreSerializer(store).data
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class URLAction(BaseAuditModel):
 | 
			
		||||
    name = models.CharField(max_length=25)
 | 
			
		||||
    desc = models.CharField(max_length=100, null=True, blank=True)
 | 
			
		||||
    pattern = models.TextField()
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return self.name
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def serialize(action):
 | 
			
		||||
        from .serializers import URLActionSerializer
 | 
			
		||||
 | 
			
		||||
        return URLActionSerializer(action).data
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
RUN_ON_CHOICES = (
 | 
			
		||||
    ("client", "Client"),
 | 
			
		||||
 
 | 
			
		||||
@@ -3,9 +3,17 @@ from rest_framework import permissions
 | 
			
		||||
from tacticalrmm.permissions import _has_perm
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class EditCoreSettingsPerms(permissions.BasePermission):
 | 
			
		||||
class CoreSettingsPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        return _has_perm(r, "can_edit_core_settings")
 | 
			
		||||
        if r.method == "GET":
 | 
			
		||||
            return _has_perm(r, "can_view_core_settings")
 | 
			
		||||
        else:
 | 
			
		||||
            return _has_perm(r, "can_edit_core_settings")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class URLActionPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        return _has_perm(r, "can_run_urlactions")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ServerMaintPerms(permissions.BasePermission):
 | 
			
		||||
@@ -16,3 +24,11 @@ class ServerMaintPerms(permissions.BasePermission):
 | 
			
		||||
class CodeSignPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        return _has_perm(r, "can_code_sign")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CustomFieldPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        if r.method == "GET":
 | 
			
		||||
            return _has_perm(r, "can_view_customfields")
 | 
			
		||||
        else:
 | 
			
		||||
            return _has_perm(r, "can_manage_customfields")
 | 
			
		||||
 
 | 
			
		||||
@@ -1,17 +1,15 @@
 | 
			
		||||
import pytz
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from loguru import logger
 | 
			
		||||
 | 
			
		||||
from autotasks.models import AutomatedTask
 | 
			
		||||
from autotasks.tasks import delete_win_task_schedule
 | 
			
		||||
from checks.tasks import prune_check_history
 | 
			
		||||
from agents.tasks import clear_faults_task
 | 
			
		||||
from agents.tasks import clear_faults_task, prune_agent_history
 | 
			
		||||
from alerts.tasks import prune_resolved_alerts
 | 
			
		||||
from core.models import CoreSettings
 | 
			
		||||
from logs.tasks import prune_debug_log, prune_audit_log
 | 
			
		||||
from tacticalrmm.celery import app
 | 
			
		||||
 | 
			
		||||
logger.configure(**settings.LOG_CONFIG)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
def core_maintenance_tasks():
 | 
			
		||||
@@ -32,8 +30,41 @@ def core_maintenance_tasks():
 | 
			
		||||
    core = CoreSettings.objects.first()
 | 
			
		||||
 | 
			
		||||
    # remove old CheckHistory data
 | 
			
		||||
    if core.check_history_prune_days > 0:
 | 
			
		||||
        prune_check_history.delay(core.check_history_prune_days)
 | 
			
		||||
    if core.check_history_prune_days > 0:  # type: ignore
 | 
			
		||||
        prune_check_history.delay(core.check_history_prune_days)  # type: ignore
 | 
			
		||||
 | 
			
		||||
    # remove old resolved alerts
 | 
			
		||||
    if core.resolved_alerts_prune_days > 0:  # type: ignore
 | 
			
		||||
        prune_resolved_alerts.delay(core.resolved_alerts_prune_days)  # type: ignore
 | 
			
		||||
 | 
			
		||||
    # remove old agent history
 | 
			
		||||
    if core.agent_history_prune_days > 0:  # type: ignore
 | 
			
		||||
        prune_agent_history.delay(core.agent_history_prune_days)  # type: ignore
 | 
			
		||||
 | 
			
		||||
    # remove old debug logs
 | 
			
		||||
    if core.debug_log_prune_days > 0:  # type: ignore
 | 
			
		||||
        prune_debug_log.delay(core.debug_log_prune_days)  # type: ignore
 | 
			
		||||
 | 
			
		||||
    # remove old audit logs
 | 
			
		||||
    if core.audit_log_prune_days > 0:  # type: ignore
 | 
			
		||||
        prune_audit_log.delay(core.audit_log_prune_days)  # type: ignore
 | 
			
		||||
 | 
			
		||||
    # clear faults
 | 
			
		||||
    if core.clear_faults_days > 0:
 | 
			
		||||
        clear_faults_task.delay(core.clear_faults_days)
 | 
			
		||||
    if core.clear_faults_days > 0:  # type: ignore
 | 
			
		||||
        clear_faults_task.delay(core.clear_faults_days)  # type: ignore
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
def cache_db_fields_task():
 | 
			
		||||
    from agents.models import Agent
 | 
			
		||||
 | 
			
		||||
    for agent in Agent.objects.prefetch_related("winupdates", "pendingactions").only(
 | 
			
		||||
        "pending_actions_count", "has_patches_pending", "pk"
 | 
			
		||||
    ):
 | 
			
		||||
        agent.pending_actions_count = agent.pendingactions.filter(
 | 
			
		||||
            status="pending"
 | 
			
		||||
        ).count()
 | 
			
		||||
        agent.has_patches_pending = (
 | 
			
		||||
            agent.winupdates.filter(action="approve").filter(installed=False).exists()
 | 
			
		||||
        )
 | 
			
		||||
        agent.save(update_fields=["pending_actions_count", "has_patches_pending"])
 | 
			
		||||
 
 | 
			
		||||
@@ -82,7 +82,7 @@ class TestCoreTasks(TacticalTestCase):
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
    def test_get_core_settings(self):
 | 
			
		||||
        url = "/core/getcoresettings/"
 | 
			
		||||
        url = "/core/settings/"
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 | 
			
		||||
@@ -90,7 +90,7 @@ class TestCoreTasks(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
    @patch("automation.tasks.generate_agent_checks_task.delay")
 | 
			
		||||
    def test_edit_coresettings(self, generate_agent_checks_task):
 | 
			
		||||
        url = "/core/editsettings/"
 | 
			
		||||
        url = "/core/settings/"
 | 
			
		||||
 | 
			
		||||
        # setup
 | 
			
		||||
        policies = baker.make("automation.Policy", _quantity=2)
 | 
			
		||||
@@ -99,7 +99,7 @@ class TestCoreTasks(TacticalTestCase):
 | 
			
		||||
            "smtp_from_email": "newexample@example.com",
 | 
			
		||||
            "mesh_token": "New_Mesh_Token",
 | 
			
		||||
        }
 | 
			
		||||
        r = self.client.patch(url, data)
 | 
			
		||||
        r = self.client.put(url, data)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            CoreSettings.objects.first().smtp_from_email, data["smtp_from_email"]
 | 
			
		||||
@@ -113,7 +113,7 @@ class TestCoreTasks(TacticalTestCase):
 | 
			
		||||
            "workstation_policy": policies[0].id,  # type: ignore
 | 
			
		||||
            "server_policy": policies[1].id,  # type: ignore
 | 
			
		||||
        }
 | 
			
		||||
        r = self.client.patch(url, data)
 | 
			
		||||
        r = self.client.put(url, data)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(CoreSettings.objects.first().server_policy.id, policies[1].id)  # type: ignore
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
@@ -128,13 +128,13 @@ class TestCoreTasks(TacticalTestCase):
 | 
			
		||||
        data = {
 | 
			
		||||
            "workstation_policy": "",
 | 
			
		||||
        }
 | 
			
		||||
        r = self.client.patch(url, data)
 | 
			
		||||
        r = self.client.put(url, data)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(CoreSettings.objects.first().workstation_policy, None)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(generate_agent_checks_task.call_count, 1)
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("patch", url)
 | 
			
		||||
        self.check_not_authenticated("put", url)
 | 
			
		||||
 | 
			
		||||
    @patch("tacticalrmm.utils.reload_nats")
 | 
			
		||||
    @patch("autotasks.tasks.remove_orphaned_win_tasks.delay")
 | 
			
		||||
@@ -404,10 +404,10 @@ class TestCoreTasks(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        url = "/core/urlaction/run/"
 | 
			
		||||
        # test not found
 | 
			
		||||
        r = self.client.patch(url, {"agent": 500, "action": 500})
 | 
			
		||||
        r = self.client.patch(url, {"agent_id": 500, "action": 500})
 | 
			
		||||
        self.assertEqual(r.status_code, 404)
 | 
			
		||||
 | 
			
		||||
        data = {"agent": agent.id, "action": action.id}  # type: ignore
 | 
			
		||||
        data = {"agent_id": agent.agent_id, "action": action.id}  # type: ignore
 | 
			
		||||
        r = self.client.patch(url, data)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 | 
			
		||||
@@ -417,3 +417,9 @@ class TestCoreTasks(TacticalTestCase):
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("patch", url)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestCorePermissions(TacticalTestCase):
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        self.client_setup()
 | 
			
		||||
        self.setup_coresettings()
 | 
			
		||||
 
 | 
			
		||||
@@ -4,8 +4,7 @@ from . import views
 | 
			
		||||
 | 
			
		||||
urlpatterns = [
 | 
			
		||||
    path("uploadmesh/", views.UploadMeshAgent.as_view()),
 | 
			
		||||
    path("getcoresettings/", views.get_core_settings),
 | 
			
		||||
    path("editsettings/", views.edit_settings),
 | 
			
		||||
    path("settings/", views.GetEditCoreSettings.as_view()),
 | 
			
		||||
    path("version/", views.version),
 | 
			
		||||
    path("emailtest/", views.email_test),
 | 
			
		||||
    path("dashinfo/", views.dashboard_info),
 | 
			
		||||
@@ -18,4 +17,5 @@ urlpatterns = [
 | 
			
		||||
    path("urlaction/", views.GetAddURLAction.as_view()),
 | 
			
		||||
    path("urlaction/<int:pk>/", views.UpdateDeleteURLAction.as_view()),
 | 
			
		||||
    path("urlaction/run/", views.RunURLAction.as_view()),
 | 
			
		||||
    path("smstest/", views.TwilioSMSTest.as_view()),
 | 
			
		||||
]
 | 
			
		||||
 
 | 
			
		||||
@@ -3,19 +3,30 @@ import re
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.shortcuts import get_object_or_404
 | 
			
		||||
from logs.models import AuditLog
 | 
			
		||||
from rest_framework import status
 | 
			
		||||
from rest_framework.decorators import api_view, permission_classes
 | 
			
		||||
from rest_framework.exceptions import ParseError
 | 
			
		||||
from rest_framework.exceptions import ParseError, PermissionDenied
 | 
			
		||||
from rest_framework.parsers import FileUploadParser
 | 
			
		||||
from rest_framework.permissions import IsAuthenticated
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
from rest_framework.views import APIView
 | 
			
		||||
 | 
			
		||||
from agents.permissions import MeshPerms
 | 
			
		||||
from tacticalrmm.utils import notify_error
 | 
			
		||||
from tacticalrmm.permissions import (
 | 
			
		||||
    _has_perm_on_client,
 | 
			
		||||
    _has_perm_on_agent,
 | 
			
		||||
    _has_perm_on_site,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
from .models import CodeSignToken, CoreSettings, CustomField, GlobalKVStore, URLAction
 | 
			
		||||
from .permissions import CodeSignPerms, EditCoreSettingsPerms, ServerMaintPerms
 | 
			
		||||
from .permissions import (
 | 
			
		||||
    CodeSignPerms,
 | 
			
		||||
    CoreSettingsPerms,
 | 
			
		||||
    ServerMaintPerms,
 | 
			
		||||
    URLActionPerms,
 | 
			
		||||
    CustomFieldPerms,
 | 
			
		||||
)
 | 
			
		||||
from .serializers import (
 | 
			
		||||
    CodeSignTokenSerializer,
 | 
			
		||||
    CoreSettingsSerializer,
 | 
			
		||||
@@ -26,7 +37,7 @@ from .serializers import (
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UploadMeshAgent(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, MeshPerms]
 | 
			
		||||
    permission_classes = [IsAuthenticated, CoreSettingsPerms]
 | 
			
		||||
    parser_class = (FileUploadParser,)
 | 
			
		||||
 | 
			
		||||
    def put(self, request, format=None):
 | 
			
		||||
@@ -42,24 +53,24 @@ class UploadMeshAgent(APIView):
 | 
			
		||||
            for chunk in f.chunks():
 | 
			
		||||
                j.write(chunk)
 | 
			
		||||
 | 
			
		||||
        return Response(status=status.HTTP_201_CREATED)
 | 
			
		||||
        return Response(
 | 
			
		||||
            "Mesh Agent uploaded successfully", status=status.HTTP_201_CREATED
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view()
 | 
			
		||||
def get_core_settings(request):
 | 
			
		||||
    settings = CoreSettings.objects.first()
 | 
			
		||||
    return Response(CoreSettingsSerializer(settings).data)
 | 
			
		||||
class GetEditCoreSettings(APIView):
 | 
			
		||||
    @permission_classes([IsAuthenticated, CoreSettingsPerms])
 | 
			
		||||
    def get(self, request):
 | 
			
		||||
        settings = CoreSettings.objects.first()
 | 
			
		||||
        return Response(CoreSettingsSerializer(settings).data)
 | 
			
		||||
 | 
			
		||||
    def put(self, request):
 | 
			
		||||
        coresettings = CoreSettings.objects.first()
 | 
			
		||||
        serializer = CoreSettingsSerializer(instance=coresettings, data=request.data)
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        serializer.save()
 | 
			
		||||
 | 
			
		||||
@api_view(["PATCH"])
 | 
			
		||||
@permission_classes([IsAuthenticated, EditCoreSettingsPerms])
 | 
			
		||||
def edit_settings(request):
 | 
			
		||||
    coresettings = CoreSettings.objects.first()
 | 
			
		||||
    serializer = CoreSettingsSerializer(instance=coresettings, data=request.data)
 | 
			
		||||
    serializer.is_valid(raise_exception=True)
 | 
			
		||||
    serializer.save()
 | 
			
		||||
 | 
			
		||||
    return Response("ok")
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view()
 | 
			
		||||
@@ -85,12 +96,14 @@ def dashboard_info(request):
 | 
			
		||||
            "client_tree_sort": request.user.client_tree_sort,
 | 
			
		||||
            "client_tree_splitter": request.user.client_tree_splitter,
 | 
			
		||||
            "loading_bar_color": request.user.loading_bar_color,
 | 
			
		||||
            "clear_search_when_switching": request.user.clear_search_when_switching,
 | 
			
		||||
            "hosted": hasattr(settings, "HOSTED") and settings.HOSTED,
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view()
 | 
			
		||||
@api_view(["POST"])
 | 
			
		||||
@permission_classes([IsAuthenticated, CoreSettingsPerms])
 | 
			
		||||
def email_test(request):
 | 
			
		||||
    core = CoreSettings.objects.first()
 | 
			
		||||
    r = core.send_mail(
 | 
			
		||||
@@ -159,10 +172,13 @@ def server_maintenance(request):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetAddCustomFields(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, EditCoreSettingsPerms]
 | 
			
		||||
    permission_classes = [IsAuthenticated, CustomFieldPerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request):
 | 
			
		||||
        fields = CustomField.objects.all()
 | 
			
		||||
        if "model" in request.query_params.keys():
 | 
			
		||||
            fields = CustomField.objects.filter(model=request.query_params["model"])
 | 
			
		||||
        else:
 | 
			
		||||
            fields = CustomField.objects.all()
 | 
			
		||||
        return Response(CustomFieldSerializer(fields, many=True).data)
 | 
			
		||||
 | 
			
		||||
    def patch(self, request):
 | 
			
		||||
@@ -181,7 +197,7 @@ class GetAddCustomFields(APIView):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetUpdateDeleteCustomFields(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, EditCoreSettingsPerms]
 | 
			
		||||
    permission_classes = [IsAuthenticated, CustomFieldPerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request, pk):
 | 
			
		||||
        custom_field = get_object_or_404(CustomField, pk=pk)
 | 
			
		||||
@@ -264,13 +280,15 @@ class CodeSign(APIView):
 | 
			
		||||
        if t is None or t == "":
 | 
			
		||||
            return notify_error(err)
 | 
			
		||||
 | 
			
		||||
        pks: list[int] = list(Agent.objects.only("pk").values_list("pk", flat=True))
 | 
			
		||||
        force_code_sign.delay(pks=pks)
 | 
			
		||||
        agent_ids: list[str] = list(
 | 
			
		||||
            Agent.objects.only("pk", "agent_id").values_list("agent_id", flat=True)
 | 
			
		||||
        )
 | 
			
		||||
        force_code_sign.delay(agent_ids=agent_ids)
 | 
			
		||||
        return Response("Agents will be code signed shortly")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetAddKeyStore(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, EditCoreSettingsPerms]
 | 
			
		||||
    permission_classes = [IsAuthenticated, CoreSettingsPerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request):
 | 
			
		||||
        keys = GlobalKVStore.objects.all()
 | 
			
		||||
@@ -285,7 +303,7 @@ class GetAddKeyStore(APIView):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UpdateDeleteKeyStore(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, EditCoreSettingsPerms]
 | 
			
		||||
    permission_classes = [IsAuthenticated, CoreSettingsPerms]
 | 
			
		||||
 | 
			
		||||
    def put(self, request, pk):
 | 
			
		||||
        key = get_object_or_404(GlobalKVStore, pk=pk)
 | 
			
		||||
@@ -303,6 +321,8 @@ class UpdateDeleteKeyStore(APIView):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetAddURLAction(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, CoreSettingsPerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request):
 | 
			
		||||
        actions = URLAction.objects.all()
 | 
			
		||||
        return Response(URLActionSerializer(actions, many=True).data)
 | 
			
		||||
@@ -316,6 +336,8 @@ class GetAddURLAction(APIView):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UpdateDeleteURLAction(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, CoreSettingsPerms]
 | 
			
		||||
 | 
			
		||||
    def put(self, request, pk):
 | 
			
		||||
        action = get_object_or_404(URLAction, pk=pk)
 | 
			
		||||
 | 
			
		||||
@@ -334,13 +356,33 @@ class UpdateDeleteURLAction(APIView):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RunURLAction(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, URLActionPerms]
 | 
			
		||||
 | 
			
		||||
    def patch(self, request):
 | 
			
		||||
        from requests.utils import requote_uri
 | 
			
		||||
 | 
			
		||||
        from agents.models import Agent
 | 
			
		||||
        from clients.models import Client, Site
 | 
			
		||||
        from tacticalrmm.utils import replace_db_values
 | 
			
		||||
 | 
			
		||||
        agent = get_object_or_404(Agent, pk=request.data["agent"])
 | 
			
		||||
        if "agent_id" in request.data.keys():
 | 
			
		||||
            if not _has_perm_on_agent(request.user, request.data["agent_id"]):
 | 
			
		||||
                raise PermissionDenied()
 | 
			
		||||
 | 
			
		||||
            instance = get_object_or_404(Agent, agent_id=request.data["agent_id"])
 | 
			
		||||
        elif "site" in request.data.keys():
 | 
			
		||||
            if not _has_perm_on_site(request.user, request.data["site"]):
 | 
			
		||||
                raise PermissionDenied()
 | 
			
		||||
 | 
			
		||||
            instance = get_object_or_404(Site, pk=request.data["site"])
 | 
			
		||||
        elif "client" in request.data.keys():
 | 
			
		||||
            if not _has_perm_on_client(request.user, request.data["client"]):
 | 
			
		||||
                raise PermissionDenied()
 | 
			
		||||
 | 
			
		||||
            instance = get_object_or_404(Client, pk=request.data["client"])
 | 
			
		||||
        else:
 | 
			
		||||
            return notify_error("received an incorrect request")
 | 
			
		||||
 | 
			
		||||
        action = get_object_or_404(URLAction, pk=request.data["action"])
 | 
			
		||||
 | 
			
		||||
        pattern = re.compile("\\{\\{([\\w\\s]+\\.[\\w\\s]+)\\}\\}")
 | 
			
		||||
@@ -348,8 +390,34 @@ class RunURLAction(APIView):
 | 
			
		||||
        url_pattern = action.pattern
 | 
			
		||||
 | 
			
		||||
        for string in re.findall(pattern, action.pattern):
 | 
			
		||||
            value = replace_db_values(string=string, agent=agent, quotes=False)
 | 
			
		||||
            value = replace_db_values(string=string, instance=instance, quotes=False)
 | 
			
		||||
 | 
			
		||||
            url_pattern = re.sub("\\{\\{" + string + "\\}\\}", str(value), url_pattern)
 | 
			
		||||
 | 
			
		||||
        AuditLog.audit_url_action(
 | 
			
		||||
            username=request.user.username,
 | 
			
		||||
            urlaction=action,
 | 
			
		||||
            instance=instance,
 | 
			
		||||
            debug_info={"ip": request._client_ip},
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        return Response(requote_uri(url_pattern))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TwilioSMSTest(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, CoreSettingsPerms]
 | 
			
		||||
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
 | 
			
		||||
        core = CoreSettings.objects.first()
 | 
			
		||||
        if not core.sms_is_configured:
 | 
			
		||||
            return notify_error(
 | 
			
		||||
                "All fields are required, including at least 1 recipient"
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        r = core.send_sms("TacticalRMM Test SMS", test=True)
 | 
			
		||||
 | 
			
		||||
        if not isinstance(r, bool) and isinstance(r, str):
 | 
			
		||||
            return notify_error(r)
 | 
			
		||||
 | 
			
		||||
        return Response("SMS Test sent successfully!")
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
from django.contrib import admin
 | 
			
		||||
 | 
			
		||||
from .models import AuditLog, PendingAction
 | 
			
		||||
from .models import AuditLog, PendingAction, DebugLog
 | 
			
		||||
 | 
			
		||||
admin.site.register(PendingAction)
 | 
			
		||||
admin.site.register(AuditLog)
 | 
			
		||||
admin.site.register(DebugLog)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										68
									
								
								api/tacticalrmm/logs/migrations/0013_auto_20210614_1835.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								api/tacticalrmm/logs/migrations/0013_auto_20210614_1835.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,68 @@
 | 
			
		||||
# Generated by Django 3.2.1 on 2021-06-14 18:35
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
import django.utils.timezone
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("logs", "0012_auto_20210228_0943"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name="debuglog",
 | 
			
		||||
            name="agent",
 | 
			
		||||
            field=models.ForeignKey(
 | 
			
		||||
                blank=True,
 | 
			
		||||
                null=True,
 | 
			
		||||
                on_delete=django.db.models.deletion.CASCADE,
 | 
			
		||||
                related_name="debuglogs",
 | 
			
		||||
                to="agents.agent",
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name="debuglog",
 | 
			
		||||
            name="entry_time",
 | 
			
		||||
            field=models.DateTimeField(
 | 
			
		||||
                auto_now_add=True, default=django.utils.timezone.now
 | 
			
		||||
            ),
 | 
			
		||||
            preserve_default=False,
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name="debuglog",
 | 
			
		||||
            name="log_level",
 | 
			
		||||
            field=models.CharField(
 | 
			
		||||
                choices=[
 | 
			
		||||
                    ("info", "Info"),
 | 
			
		||||
                    ("warning", "Warning"),
 | 
			
		||||
                    ("error", "Error"),
 | 
			
		||||
                    ("critical", "Critical"),
 | 
			
		||||
                ],
 | 
			
		||||
                default="info",
 | 
			
		||||
                max_length=50,
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name="debuglog",
 | 
			
		||||
            name="log_type",
 | 
			
		||||
            field=models.CharField(
 | 
			
		||||
                choices=[
 | 
			
		||||
                    ("agent_update", "Agent Update"),
 | 
			
		||||
                    ("agent_issues", "Agent Issues"),
 | 
			
		||||
                    ("win_updates", "Windows Updates"),
 | 
			
		||||
                    ("system_issues", "System Issues"),
 | 
			
		||||
                    ("scripting", "Scripting"),
 | 
			
		||||
                ],
 | 
			
		||||
                default="system_issues",
 | 
			
		||||
                max_length=50,
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name="debuglog",
 | 
			
		||||
            name="message",
 | 
			
		||||
            field=models.TextField(blank=True, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										18
									
								
								api/tacticalrmm/logs/migrations/0014_auditlog_agent_id.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								api/tacticalrmm/logs/migrations/0014_auditlog_agent_id.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
			
		||||
# Generated by Django 3.2.1 on 2021-06-28 02:37
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('logs', '0013_auto_20210614_1835'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='auditlog',
 | 
			
		||||
            name='agent_id',
 | 
			
		||||
            field=models.PositiveIntegerField(blank=True, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user