Compare commits
	
		
			853 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					9011148adf | ||
| 
						 | 
					897d0590d2 | ||
| 
						 | 
					33b33e8458 | ||
| 
						 | 
					7758f5c187 | ||
| 
						 | 
					a9a0df9699 | ||
| 
						 | 
					216a9ed035 | ||
| 
						 | 
					35d61b6a6c | ||
| 
						 | 
					5fb72cea53 | ||
| 
						 | 
					d54d021e9f | ||
| 
						 | 
					06e78311df | ||
| 
						 | 
					df720f95ca | ||
| 
						 | 
					00faff34d3 | ||
| 
						 | 
					2b5b3ea4f3 | ||
| 
						 | 
					95e608d0b4 | ||
| 
						 | 
					1d55bf87dd | ||
| 
						 | 
					1220ce53eb | ||
| 
						 | 
					2006218f87 | ||
| 
						 | 
					40f427a387 | ||
| 
						 | 
					445e95baed | ||
| 
						 | 
					67fbc9ad33 | ||
| 
						 | 
					1253e9e465 | ||
| 
						 | 
					21069432e8 | ||
| 
						 | 
					6facf6a324 | ||
| 
						 | 
					7556197485 | ||
| 
						 | 
					8dddd2d896 | ||
| 
						 | 
					f319c95c2b | ||
| 
						 | 
					8e972b0907 | ||
| 
						 | 
					395e400215 | ||
| 
						 | 
					3685e3111f | ||
| 
						 | 
					7bb1c75dc6 | ||
| 
						 | 
					b20834929c | ||
| 
						 | 
					181891757e | ||
| 
						 | 
					b16feeae44 | ||
| 
						 | 
					684e049f27 | ||
| 
						 | 
					8cebd901b2 | ||
| 
						 | 
					3c96beb8fb | ||
| 
						 | 
					8a46459cf9 | ||
| 
						 | 
					be5c3e9daa | ||
| 
						 | 
					e44453877c | ||
| 
						 | 
					f772a4ec56 | ||
| 
						 | 
					44182ec683 | ||
| 
						 | 
					b9ab13fa53 | ||
| 
						 | 
					2ad6721c95 | ||
| 
						 | 
					b7d0604e62 | ||
| 
						 | 
					a7518b4b26 | ||
| 
						 | 
					50613f5d3e | ||
| 
						 | 
					f814767703 | ||
| 
						 | 
					4af86d6456 | ||
| 
						 | 
					f0a4f00c2d | ||
| 
						 | 
					4321affddb | ||
| 
						 | 
					926ed55b9b | ||
| 
						 | 
					2ebf308565 | ||
| 
						 | 
					1c5e736dce | ||
| 
						 | 
					b591f9f5b7 | ||
| 
						 | 
					9724882578 | ||
| 
						 | 
					ddef2df101 | ||
| 
						 | 
					8af69c4284 | ||
| 
						 | 
					6ebe1ab467 | ||
| 
						 | 
					24e4d9cf6d | ||
| 
						 | 
					f35fa0aa58 | ||
| 
						 | 
					4942f262f1 | ||
| 
						 | 
					a20b1a973e | ||
| 
						 | 
					eae5e00706 | ||
| 
						 | 
					403762d862 | ||
| 
						 | 
					5c92d4b454 | ||
| 
						 | 
					38179b9d38 | ||
| 
						 | 
					8f510dde5a | ||
| 
						 | 
					be42d56e37 | ||
| 
						 | 
					6294530fa3 | ||
| 
						 | 
					c5c8f5fab1 | ||
| 
						 | 
					3d41d79078 | ||
| 
						 | 
					3005061a11 | ||
| 
						 | 
					65ea46f457 | ||
| 
						 | 
					eca8f32570 | ||
| 
						 | 
					8d1ef19c61 | ||
| 
						 | 
					71d87d866b | ||
| 
						 | 
					c4f88bdce7 | ||
| 
						 | 
					f722a115b1 | ||
| 
						 | 
					1583beea7b | ||
| 
						 | 
					5b388c587b | ||
| 
						 | 
					e254923167 | ||
| 
						 | 
					b0dbdd7803 | ||
| 
						 | 
					aa6ebe0122 | ||
| 
						 | 
					c5f179bab8 | ||
| 
						 | 
					e65cb86638 | ||
| 
						 | 
					a349998640 | ||
| 
						 | 
					43f60610b8 | ||
| 
						 | 
					46d042087a | ||
| 
						 | 
					ee214727f6 | ||
| 
						 | 
					b4c1ec55ec | ||
| 
						 | 
					0fdd54f710 | ||
| 
						 | 
					4f0cdeaec0 | ||
| 
						 | 
					e5cc38857c | ||
| 
						 | 
					fe4b9d71c0 | ||
| 
						 | 
					5c1181e40e | ||
| 
						 | 
					8b71832bc2 | ||
| 
						 | 
					8412ed6065 | ||
| 
						 | 
					207f6cdc7c | ||
| 
						 | 
					b0b51f5730 | ||
| 
						 | 
					def6833ef0 | ||
| 
						 | 
					c528dd3de1 | ||
| 
						 | 
					544270e35d | ||
| 
						 | 
					657e029fee | ||
| 
						 | 
					49469d7689 | ||
| 
						 | 
					4f0dd452c8 | ||
| 
						 | 
					3f741eab11 | ||
| 
						 | 
					190368788f | ||
| 
						 | 
					8306a3f566 | ||
| 
						 | 
					988c134c09 | ||
| 
						 | 
					af0a4d578b | ||
| 
						 | 
					9bc0abc831 | ||
| 
						 | 
					41410e99e7 | ||
| 
						 | 
					deae04d5ff | ||
| 
						 | 
					7d6eeffd66 | ||
| 
						 | 
					629858e095 | ||
| 
						 | 
					dfdb628347 | ||
| 
						 | 
					6e48b28fc9 | ||
| 
						 | 
					3ba450e837 | ||
| 
						 | 
					688ed93500 | ||
| 
						 | 
					7268ba20a2 | ||
| 
						 | 
					63d9e73098 | ||
| 
						 | 
					564c048f90 | ||
| 
						 | 
					5f801c74d5 | ||
| 
						 | 
					b405fbc09a | ||
| 
						 | 
					7a64c2eb49 | ||
| 
						 | 
					c93cbac3b1 | ||
| 
						 | 
					8b0f67b8a6 | ||
| 
						 | 
					0d96129f2d | ||
| 
						 | 
					54ee12d2b3 | ||
| 
						 | 
					92fc042103 | ||
| 
						 | 
					9bb7016fa7 | ||
| 
						 | 
					3ad56feafb | ||
| 
						 | 
					14d59c3dec | ||
| 
						 | 
					443f419770 | ||
| 
						 | 
					ddbb58755e | ||
| 
						 | 
					524283b9ff | ||
| 
						 | 
					fb178d2944 | ||
| 
						 | 
					52f4ad9403 | ||
| 
						 | 
					ba0c08ef1f | ||
| 
						 | 
					9e19b1e04c | ||
| 
						 | 
					b2118201b1 | ||
| 
						 | 
					b4346aa056 | ||
| 
						 | 
					b599f05aab | ||
| 
						 | 
					93d78a0200 | ||
| 
						 | 
					449957b2eb | ||
| 
						 | 
					0a6d44bad3 | ||
| 
						 | 
					17ceaaa503 | ||
| 
						 | 
					d70803b416 | ||
| 
						 | 
					aa414d4702 | ||
| 
						 | 
					f24e1b91ea | ||
| 
						 | 
					1df8163090 | ||
| 
						 | 
					659ddf6a45 | ||
| 
						 | 
					e110068da4 | ||
| 
						 | 
					c943f6f936 | ||
| 
						 | 
					cb1fe7fe54 | ||
| 
						 | 
					593f1f63cc | ||
| 
						 | 
					66aa70cf75 | ||
| 
						 | 
					304be99067 | ||
| 
						 | 
					9a01ec35f4 | ||
| 
						 | 
					bfa5b4fba5 | ||
| 
						 | 
					d2f63ef353 | ||
| 
						 | 
					50f334425e | ||
| 
						 | 
					f78212073c | ||
| 
						 | 
					5c655f5a82 | ||
| 
						 | 
					6a6446bfcb | ||
| 
						 | 
					b60a3a5e50 | ||
| 
						 | 
					02ccbab8e5 | ||
| 
						 | 
					023ff3f964 | ||
| 
						 | 
					7c5e8df3b8 | ||
| 
						 | 
					56fdab260b | ||
| 
						 | 
					7cce49dc1a | ||
| 
						 | 
					2dfaafb20b | ||
| 
						 | 
					6138a5bf54 | ||
| 
						 | 
					828c67cc00 | ||
| 
						 | 
					e70cd44e18 | ||
| 
						 | 
					efa5ac5edd | ||
| 
						 | 
					788b11e759 | ||
| 
						 | 
					d049d7a61f | ||
| 
						 | 
					075c833b58 | ||
| 
						 | 
					e9309c2a96 | ||
| 
						 | 
					a592d2b397 | ||
| 
						 | 
					3ad1805ac0 | ||
| 
						 | 
					dbc2bab698 | ||
| 
						 | 
					79eec5c299 | ||
| 
						 | 
					7754b0c575 | ||
| 
						 | 
					be4289ce76 | ||
| 
						 | 
					67f5226270 | ||
| 
						 | 
					b6d77c581b | ||
| 
						 | 
					d84bf47d04 | ||
| 
						 | 
					aba3a7bb9e | ||
| 
						 | 
					6281736d89 | ||
| 
						 | 
					94d96f89d3 | ||
| 
						 | 
					4b55f9dead | ||
| 
						 | 
					5c6dce94df | ||
| 
						 | 
					f7d8f9c7f5 | ||
| 
						 | 
					053df24f9c | ||
| 
						 | 
					1dc470e434 | ||
| 
						 | 
					cfd8773267 | ||
| 
						 | 
					67045cf6c1 | ||
| 
						 | 
					ddfb9e7239 | ||
| 
						 | 
					9f6eed5472 | ||
| 
						 | 
					15a1e2ebcb | ||
| 
						 | 
					fcfe450b07 | ||
| 
						 | 
					a69bbb3bc9 | ||
| 
						 | 
					6d2559cfc1 | ||
| 
						 | 
					b3a62615f3 | ||
| 
						 | 
					57f5cca1cb | ||
| 
						 | 
					6b9851f540 | ||
| 
						 | 
					36fd203a88 | ||
| 
						 | 
					3f5cb5d61c | ||
| 
						 | 
					862fc6a946 | ||
| 
						 | 
					92c386ac0e | ||
| 
						 | 
					98a11a3645 | ||
| 
						 | 
					62be0ed936 | ||
| 
						 | 
					b7de73fd8a | ||
| 
						 | 
					e2413f1af2 | ||
| 
						 | 
					0e77d575c4 | ||
| 
						 | 
					ba42c5e367 | ||
| 
						 | 
					6a06734192 | ||
| 
						 | 
					5e26a406b7 | ||
| 
						 | 
					b6dd03138d | ||
| 
						 | 
					cf03ee03ee | ||
| 
						 | 
					0e665b6bf0 | ||
| 
						 | 
					e3d0de7313 | ||
| 
						 | 
					bcf3a543a1 | ||
| 
						 | 
					b27f17c74a | ||
| 
						 | 
					75d864771e | ||
| 
						 | 
					6420060f2a | ||
| 
						 | 
					c149ae71b9 | ||
| 
						 | 
					3a49dd034c | ||
| 
						 | 
					b26d7e82e3 | ||
| 
						 | 
					415abdf0ce | ||
| 
						 | 
					f7f6f6ecb2 | ||
| 
						 | 
					43d54f134a | ||
| 
						 | 
					0d2606a13b | ||
| 
						 | 
					1deb10dc88 | ||
| 
						 | 
					1236d55544 | ||
| 
						 | 
					ecccf39455 | ||
| 
						 | 
					8e0316825a | ||
| 
						 | 
					aa45fa87af | ||
| 
						 | 
					71e78bd0c5 | ||
| 
						 | 
					4766477c58 | ||
| 
						 | 
					d97e49ff2b | ||
| 
						 | 
					6b9d775cb9 | ||
| 
						 | 
					e521f580d7 | ||
| 
						 | 
					25e7cf7db0 | ||
| 
						 | 
					0cab33787d | ||
| 
						 | 
					bc6faf817f | ||
| 
						 | 
					d46ae55863 | ||
| 
						 | 
					bbd900ab25 | ||
| 
						 | 
					129ae93e2b | ||
| 
						 | 
					44dd59fa3f | ||
| 
						 | 
					ec4e7559b0 | ||
| 
						 | 
					dce40611cf | ||
| 
						 | 
					e71b8546f9 | ||
| 
						 | 
					f827348467 | ||
| 
						 | 
					f3978343db | ||
| 
						 | 
					2654a7ea70 | ||
| 
						 | 
					1068bf4ef7 | ||
| 
						 | 
					e7fccc97cc | ||
| 
						 | 
					733e289852 | ||
| 
						 | 
					29d71a104c | ||
| 
						 | 
					05200420ad | ||
| 
						 | 
					eb762d4bfd | ||
| 
						 | 
					58ace9eda1 | ||
| 
						 | 
					eeb2623be0 | ||
| 
						 | 
					cfa242c2fe | ||
| 
						 | 
					ec0441ccc2 | ||
| 
						 | 
					ae2782a8fe | ||
| 
						 | 
					58ff570251 | ||
| 
						 | 
					7b554b12c7 | ||
| 
						 | 
					58f7603d4f | ||
| 
						 | 
					8895994c54 | ||
| 
						 | 
					de8f7e36d5 | ||
| 
						 | 
					88d7a50265 | ||
| 
						 | 
					21e19fc7e5 | ||
| 
						 | 
					faf4935a69 | ||
| 
						 | 
					71a1f9d74a | ||
| 
						 | 
					bd8d523e10 | ||
| 
						 | 
					60cae0e3ac | ||
| 
						 | 
					5a342ac012 | ||
| 
						 | 
					bb8767dfc3 | ||
| 
						 | 
					fcb2779c15 | ||
| 
						 | 
					77dd6c1f61 | ||
| 
						 | 
					8118eef300 | ||
| 
						 | 
					802d1489fe | ||
| 
						 | 
					443a029185 | ||
| 
						 | 
					4ee508fdd0 | ||
| 
						 | 
					aa5608f7e8 | ||
| 
						 | 
					cc472b4613 | ||
| 
						 | 
					764b945ddc | ||
| 
						 | 
					fd2206ce4c | ||
| 
						 | 
					48c0ac9f00 | ||
| 
						 | 
					84eb4fe9ed | ||
| 
						 | 
					4a5428812c | ||
| 
						 | 
					023f98a89d | ||
| 
						 | 
					66893dd0c1 | ||
| 
						 | 
					25a6666e35 | ||
| 
						 | 
					19d75309b5 | ||
| 
						 | 
					11110d65c1 | ||
| 
						 | 
					a348f58fe2 | ||
| 
						 | 
					13851dd976 | ||
| 
						 | 
					2ec37c5da9 | ||
| 
						 | 
					8c127160de | ||
| 
						 | 
					2af820de9a | ||
| 
						 | 
					55fb0bb3a0 | ||
| 
						 | 
					9f9ecc521f | ||
| 
						 | 
					dfd01df5ba | ||
| 
						 | 
					474090698c | ||
| 
						 | 
					6b71cdeea4 | ||
| 
						 | 
					581e974236 | ||
| 
						 | 
					ba3c3a42ce | ||
| 
						 | 
					c8bc5671c5 | ||
| 
						 | 
					ff9401a040 | ||
| 
						 | 
					5e1bc1989f | ||
| 
						 | 
					a1dc91cd7d | ||
| 
						 | 
					99f2772bb3 | ||
| 
						 | 
					e5d0e42655 | ||
| 
						 | 
					2c914cc374 | ||
| 
						 | 
					9bceb62381 | ||
| 
						 | 
					de7518a800 | ||
| 
						 | 
					304fb63453 | ||
| 
						 | 
					0f7ef60ca0 | ||
| 
						 | 
					07c74e4641 | ||
| 
						 | 
					de7f325cfb | ||
| 
						 | 
					42cdf70cb4 | ||
| 
						 | 
					6beb6be131 | ||
| 
						 | 
					fa4fc2a708 | ||
| 
						 | 
					2db9758260 | ||
| 
						 | 
					715982e40a | ||
| 
						 | 
					d00cd4453a | ||
| 
						 | 
					429c08c24a | ||
| 
						 | 
					6a71490e20 | ||
| 
						 | 
					9bceda0646 | ||
| 
						 | 
					a1027a6773 | ||
| 
						 | 
					302d4b75f9 | ||
| 
						 | 
					5f6ee0e883 | ||
| 
						 | 
					27f9720de1 | ||
| 
						 | 
					22aa3fdbbc | ||
| 
						 | 
					069ecdd33f | ||
| 
						 | 
					dd545ae933 | ||
| 
						 | 
					6650b705c4 | ||
| 
						 | 
					59b0350289 | ||
| 
						 | 
					1ad159f820 | ||
| 
						 | 
					0bf42190e9 | ||
| 
						 | 
					d2fa836232 | ||
| 
						 | 
					c387774093 | ||
| 
						 | 
					e99736ba3c | ||
| 
						 | 
					16cb54fcc9 | ||
| 
						 | 
					5aa15c51ec | ||
| 
						 | 
					a8aedd9cf3 | ||
| 
						 | 
					b851b632bc | ||
| 
						 | 
					541e07fb65 | ||
| 
						 | 
					6ad16a897d | ||
| 
						 | 
					72f1053a93 | ||
| 
						 | 
					fb15a2762c | ||
| 
						 | 
					9165248b91 | ||
| 
						 | 
					add18b29db | ||
| 
						 | 
					1971653548 | ||
| 
						 | 
					392cd64d7b | ||
| 
						 | 
					b5affbb7c8 | ||
| 
						 | 
					71d1206277 | ||
| 
						 | 
					26e6a8c409 | ||
| 
						 | 
					eb54fae11a | ||
| 
						 | 
					ee773e5966 | ||
| 
						 | 
					7218ccdba8 | ||
| 
						 | 
					332400e48a | ||
| 
						 | 
					ad1a5d3702 | ||
| 
						 | 
					3006b4184d | ||
| 
						 | 
					84eb84a080 | ||
| 
						 | 
					60beea548b | ||
| 
						 | 
					5f9c149e59 | ||
| 
						 | 
					53367c6f04 | ||
| 
						 | 
					d7f817ee44 | ||
| 
						 | 
					d33a87da54 | ||
| 
						 | 
					3aebfb12b7 | ||
| 
						 | 
					1d6c55ffa6 | ||
| 
						 | 
					5e7080aac3 | ||
| 
						 | 
					fad739bc01 | ||
| 
						 | 
					c6b7f23884 | ||
| 
						 | 
					a6f7e446de | ||
| 
						 | 
					89d95d3ae1 | ||
| 
						 | 
					764208698f | ||
| 
						 | 
					57129cf934 | ||
| 
						 | 
					aae1a842d5 | ||
| 
						 | 
					623f35aec7 | ||
| 
						 | 
					870bf842cf | ||
| 
						 | 
					07f2d7dd5c | ||
| 
						 | 
					f223f2edc5 | ||
| 
						 | 
					e848a9a577 | ||
| 
						 | 
					7569d98e07 | ||
| 
						 | 
					596dee2f24 | ||
| 
						 | 
					9970403964 | ||
| 
						 | 
					07a88ae00d | ||
| 
						 | 
					5475b4d287 | ||
| 
						 | 
					6631dcfd3e | ||
| 
						 | 
					0dd3f337f3 | ||
| 
						 | 
					8eb27b5875 | ||
| 
						 | 
					2d1863031c | ||
| 
						 | 
					9feb76ca81 | ||
| 
						 | 
					993e8f4ab3 | ||
| 
						 | 
					e08ae95d4f | ||
| 
						 | 
					15359e8846 | ||
| 
						 | 
					d1457b312b | ||
| 
						 | 
					c9dd2af196 | ||
| 
						 | 
					564ef4e688 | ||
| 
						 | 
					a33e6e8bb5 | ||
| 
						 | 
					cf34f33f04 | ||
| 
						 | 
					827cfe4e8f | ||
| 
						 | 
					2ce1c2383c | ||
| 
						 | 
					6fc0a665ae | ||
| 
						 | 
					4f16d01263 | ||
| 
						 | 
					67cc37354a | ||
| 
						 | 
					e388243ef4 | ||
| 
						 | 
					3dc92763c7 | ||
| 
						 | 
					dfe97dd466 | ||
| 
						 | 
					2803cee29b | ||
| 
						 | 
					3a03020e54 | ||
| 
						 | 
					64443cc703 | ||
| 
						 | 
					4d1aa6ed18 | ||
| 
						 | 
					84837e88d2 | ||
| 
						 | 
					ff49c936ea | ||
| 
						 | 
					e6e0901329 | ||
| 
						 | 
					23b6284b51 | ||
| 
						 | 
					33dfbcbe32 | ||
| 
						 | 
					700c23d537 | ||
| 
						 | 
					369fac9e38 | ||
| 
						 | 
					2229eb1167 | ||
| 
						 | 
					a3dec841b6 | ||
| 
						 | 
					b17620bdb6 | ||
| 
						 | 
					f39cd5ae2f | ||
| 
						 | 
					83a19e005b | ||
| 
						 | 
					a9dd01b0c8 | ||
| 
						 | 
					eb59afa1d1 | ||
| 
						 | 
					2adcfce9d0 | ||
| 
						 | 
					314ab9b304 | ||
| 
						 | 
					8576fb82c7 | ||
| 
						 | 
					0f95a6bb2f | ||
| 
						 | 
					ad5104567d | ||
| 
						 | 
					ece68ba1d5 | ||
| 
						 | 
					acccd3a586 | ||
| 
						 | 
					8ebef1c1ca | ||
| 
						 | 
					28abc0d5ed | ||
| 
						 | 
					1efe25d3ec | ||
| 
						 | 
					c40e4f8e4b | ||
| 
						 | 
					baca84092d | ||
| 
						 | 
					346d4da059 | ||
| 
						 | 
					ade64d6c0a | ||
| 
						 | 
					8204bdfc5f | ||
| 
						 | 
					1a9bb3e986 | ||
| 
						 | 
					49356479e5 | ||
| 
						 | 
					c44e9a7292 | ||
| 
						 | 
					21771a593f | ||
| 
						 | 
					84458dfc4c | ||
| 
						 | 
					5835632dab | ||
| 
						 | 
					67aa7229ef | ||
| 
						 | 
					b72dc3ed3a | ||
| 
						 | 
					0f93d4a5bd | ||
| 
						 | 
					106320b035 | ||
| 
						 | 
					63951705cd | ||
| 
						 | 
					a8d56921d5 | ||
| 
						 | 
					10bc133cf1 | ||
| 
						 | 
					adeb5b35c9 | ||
| 
						 | 
					589ff46ea5 | ||
| 
						 | 
					656fcb9fe7 | ||
| 
						 | 
					1cb9353006 | ||
| 
						 | 
					57bf16ba07 | ||
| 
						 | 
					659846ed88 | ||
| 
						 | 
					25894044e0 | ||
| 
						 | 
					e7a0826beb | ||
| 
						 | 
					1f7ddee23b | ||
| 
						 | 
					7e186730db | ||
| 
						 | 
					6713a50208 | ||
| 
						 | 
					7c9d8fcfec | ||
| 
						 | 
					33bfc8cfe8 | ||
| 
						 | 
					ca735bc14a | ||
| 
						 | 
					4ba748a18b | ||
| 
						 | 
					f1845106f8 | ||
| 
						 | 
					67e7156c4b | ||
| 
						 | 
					4a476adebf | ||
| 
						 | 
					918798f8cc | ||
| 
						 | 
					5a3f868866 | ||
| 
						 | 
					feea2c6396 | ||
| 
						 | 
					707b4c46d9 | ||
| 
						 | 
					89ca39fc2b | ||
| 
						 | 
					204281b12d | ||
| 
						 | 
					a8538a7e95 | ||
| 
						 | 
					dee1b471e9 | ||
| 
						 | 
					aa04e9b01f | ||
| 
						 | 
					350f0dc604 | ||
| 
						 | 
					6021f2efd6 | ||
| 
						 | 
					51838ec25a | ||
| 
						 | 
					54768a121e | ||
| 
						 | 
					8ff72cdca3 | ||
| 
						 | 
					2cb53ad06b | ||
| 
						 | 
					b8349de31d | ||
| 
						 | 
					d7e11af7f8 | ||
| 
						 | 
					dd8d39e698 | ||
| 
						 | 
					afb1316daa | ||
| 
						 | 
					04d7017536 | ||
| 
						 | 
					6a1c75b060 | ||
| 
						 | 
					5c94611f3b | ||
| 
						 | 
					4e5676e80f | ||
| 
						 | 
					c96d688a9c | ||
| 
						 | 
					804242e9a5 | ||
| 
						 | 
					0ec9760b17 | ||
| 
						 | 
					d481ae3da4 | ||
| 
						 | 
					4742c14fc1 | ||
| 
						 | 
					509b0d501b | ||
| 
						 | 
					d4c9b04d4e | ||
| 
						 | 
					16fb4d331b | ||
| 
						 | 
					e9e5bf31a7 | ||
| 
						 | 
					221418120e | ||
| 
						 | 
					46f852e26e | ||
| 
						 | 
					4234cf0a31 | ||
| 
						 | 
					7f3daea648 | ||
| 
						 | 
					2eb16c82f4 | ||
| 
						 | 
					e00b2ce591 | ||
| 
						 | 
					d71e1311ca | ||
| 
						 | 
					2cf16963e3 | ||
| 
						 | 
					10bf7b7fb4 | ||
| 
						 | 
					182c85a228 | ||
| 
						 | 
					94b1988b90 | ||
| 
						 | 
					6f7e62e9a0 | ||
| 
						 | 
					aa7076af04 | ||
| 
						 | 
					c928e8f0d4 | ||
| 
						 | 
					5c6b106f68 | ||
| 
						 | 
					d45bcea1ff | ||
| 
						 | 
					6ff2dc79f8 | ||
| 
						 | 
					b752329987 | ||
| 
						 | 
					f21465335a | ||
| 
						 | 
					0801adfc4b | ||
| 
						 | 
					5bee8052d5 | ||
| 
						 | 
					68dca5dfef | ||
| 
						 | 
					3f51dd1d2f | ||
| 
						 | 
					7f80889d77 | ||
| 
						 | 
					efc61c0222 | ||
| 
						 | 
					6fc0a05d34 | ||
| 
						 | 
					a9be872d7a | ||
| 
						 | 
					6ca85f099e | ||
| 
						 | 
					86ff677b8a | ||
| 
						 | 
					35e295df86 | ||
| 
						 | 
					cd4d301790 | ||
| 
						 | 
					93bb329c3d | ||
| 
						 | 
					7c1e0f2c30 | ||
| 
						 | 
					b57f471f44 | ||
| 
						 | 
					252a9a2ed6 | ||
| 
						 | 
					7258d4d787 | ||
| 
						 | 
					75522fa295 | ||
| 
						 | 
					4ba8f41d95 | ||
| 
						 | 
					f326f8e4de | ||
| 
						 | 
					f863dc058e | ||
| 
						 | 
					20891db251 | ||
| 
						 | 
					f1d05f1342 | ||
| 
						 | 
					8dd636b0eb | ||
| 
						 | 
					6b5bda8ee1 | ||
| 
						 | 
					ddc5597157 | ||
| 
						 | 
					ae112c7257 | ||
| 
						 | 
					c22f10f96a | ||
| 
						 | 
					18d10c9bec | ||
| 
						 | 
					890e430cb7 | ||
| 
						 | 
					dadc3d4cd7 | ||
| 
						 | 
					d98b4d7320 | ||
| 
						 | 
					340f532238 | ||
| 
						 | 
					7669f68e7c | ||
| 
						 | 
					3557e5514f | ||
| 
						 | 
					a9f09b7614 | ||
| 
						 | 
					845b9e4568 | ||
| 
						 | 
					24a6092dcf | ||
| 
						 | 
					195ae7d8b1 | ||
| 
						 | 
					a5c6ea7ffc | ||
| 
						 | 
					eb7a4ac29f | ||
| 
						 | 
					508ef73fde | ||
| 
						 | 
					838d6d8076 | ||
| 
						 | 
					762c3159b8 | ||
| 
						 | 
					7a88a06bcf | ||
| 
						 | 
					0b1e3d7de5 | ||
| 
						 | 
					9a83c73f21 | ||
| 
						 | 
					aa50c7b268 | ||
| 
						 | 
					179a5a80f4 | ||
| 
						 | 
					0ddae527ef | ||
| 
						 | 
					ee7a46de26 | ||
| 
						 | 
					95522fda74 | ||
| 
						 | 
					e58881c2bd | ||
| 
						 | 
					36a902a44e | ||
| 
						 | 
					16b74549a2 | ||
| 
						 | 
					da7ededfb1 | ||
| 
						 | 
					790bb08718 | ||
| 
						 | 
					e6765f421f | ||
| 
						 | 
					7e8f1fe904 | ||
| 
						 | 
					eacce4578a | ||
| 
						 | 
					07b2543972 | ||
| 
						 | 
					d1c3fc8493 | ||
| 
						 | 
					f453b16010 | ||
| 
						 | 
					05151d8978 | ||
| 
						 | 
					8218e1acc3 | ||
| 
						 | 
					30212fc89a | ||
| 
						 | 
					b31c13fcae | ||
| 
						 | 
					6b95fc6f1d | ||
| 
						 | 
					369cf17eb2 | ||
| 
						 | 
					4dd8f512cc | ||
| 
						 | 
					26cfec7d80 | ||
| 
						 | 
					67a87ccf00 | ||
| 
						 | 
					667cebcf94 | ||
| 
						 | 
					bc1747ca1c | ||
| 
						 | 
					945d8647bf | ||
| 
						 | 
					dfe2e94627 | ||
| 
						 | 
					09a5591eec | ||
| 
						 | 
					f2bf06a0ba | ||
| 
						 | 
					eedad4ab1c | ||
| 
						 | 
					336a62ab29 | ||
| 
						 | 
					b5603a5233 | ||
| 
						 | 
					73890f553c | ||
| 
						 | 
					f6243b8968 | ||
| 
						 | 
					3770dc74d4 | ||
| 
						 | 
					45f4e947c5 | ||
| 
						 | 
					9928d7c6e1 | ||
| 
						 | 
					bf776eeb2b | ||
| 
						 | 
					ae7c0e9195 | ||
| 
						 | 
					e90b640602 | ||
| 
						 | 
					ba7529d3f5 | ||
| 
						 | 
					34667f252e | ||
| 
						 | 
					d18bddcb7b | ||
| 
						 | 
					96dff49d33 | ||
| 
						 | 
					b389728338 | ||
| 
						 | 
					cdc7da86f3 | ||
| 
						 | 
					4745cc0378 | ||
| 
						 | 
					434f132479 | ||
| 
						 | 
					fb0f31ffc7 | ||
| 
						 | 
					bb1d73c0ae | ||
| 
						 | 
					0e823d1191 | ||
| 
						 | 
					48f4199ff3 | ||
| 
						 | 
					eaf379587b | ||
| 
						 | 
					672446b7d1 | ||
| 
						 | 
					dfe52c1b07 | ||
| 
						 | 
					d63df03ad8 | ||
| 
						 | 
					aba4f9f2ce | ||
| 
						 | 
					ac5c1e7803 | ||
| 
						 | 
					d521dbf50e | ||
| 
						 | 
					f210ed3e6a | ||
| 
						 | 
					df3cac4ea6 | ||
| 
						 | 
					f778c5175b | ||
| 
						 | 
					6c66ff28dd | ||
| 
						 | 
					d5b6ec702b | ||
| 
						 | 
					c62a5fcef2 | ||
| 
						 | 
					59c47e9200 | ||
| 
						 | 
					4ba44d8932 | ||
| 
						 | 
					27dae05e1b | ||
| 
						 | 
					a251ae9b90 | ||
| 
						 | 
					7e960b2bde | ||
| 
						 | 
					5df4825158 | ||
| 
						 | 
					8984d06d93 | ||
| 
						 | 
					eed7aac047 | ||
| 
						 | 
					54b068de4a | ||
| 
						 | 
					f0f33b00b6 | ||
| 
						 | 
					1043405088 | ||
| 
						 | 
					0131b10805 | ||
| 
						 | 
					a19b441f62 | ||
| 
						 | 
					28edc31d43 | ||
| 
						 | 
					0f9872a818 | ||
| 
						 | 
					76ce4296f3 | ||
| 
						 | 
					3dd2671380 | ||
| 
						 | 
					298ca31332 | ||
| 
						 | 
					8f911aa6b9 | ||
| 
						 | 
					82a5c7d9b1 | ||
| 
						 | 
					7f013dcdba | ||
| 
						 | 
					68e2e16076 | ||
| 
						 | 
					ea23c763c9 | ||
| 
						 | 
					5dcecb3206 | ||
| 
						 | 
					5bd48e2d0e | ||
| 
						 | 
					afd0a02589 | ||
| 
						 | 
					2379192d53 | ||
| 
						 | 
					a6489290c8 | ||
| 
						 | 
					5f74c43415 | ||
| 
						 | 
					aa8b84a302 | ||
| 
						 | 
					b987d041b0 | ||
| 
						 | 
					b62e37307e | ||
| 
						 | 
					61a59aa6ac | ||
| 
						 | 
					f79ec27f1d | ||
| 
						 | 
					b993fe380f | ||
| 
						 | 
					d974b5f55f | ||
| 
						 | 
					f21ae93197 | ||
| 
						 | 
					342ff18be8 | ||
| 
						 | 
					a8236f69bf | ||
| 
						 | 
					ab15a2448d | ||
| 
						 | 
					6ff4d8f558 | ||
| 
						 | 
					bb04ba528c | ||
| 
						 | 
					b94a795189 | ||
| 
						 | 
					9968184733 | ||
| 
						 | 
					1be6f8f87a | ||
| 
						 | 
					426821cceb | ||
| 
						 | 
					4fec0deaf7 | ||
| 
						 | 
					144ac5b6ce | ||
| 
						 | 
					97c73786fa | ||
| 
						 | 
					82e59d7da0 | ||
| 
						 | 
					b2c10de6af | ||
| 
						 | 
					d72029c2c6 | ||
| 
						 | 
					17b9987063 | ||
| 
						 | 
					fde07da2b7 | ||
| 
						 | 
					c23bc29511 | ||
| 
						 | 
					714cad2a52 | ||
| 
						 | 
					357d5d2fde | ||
| 
						 | 
					d477cce901 | ||
| 
						 | 
					eb6af52ad1 | ||
| 
						 | 
					aae75023a7 | ||
| 
						 | 
					41dcd4f458 | ||
| 
						 | 
					4651ae4495 | ||
| 
						 | 
					ed61e0b0fc | ||
| 
						 | 
					1eefc6fbf4 | ||
| 
						 | 
					09ebf2cea2 | ||
| 
						 | 
					b3b0c4cd65 | ||
| 
						 | 
					f4b7924e8f | ||
| 
						 | 
					ea68d38b82 | ||
| 
						 | 
					dfbaa71132 | ||
| 
						 | 
					6c328deb08 | ||
| 
						 | 
					add564d5bf | ||
| 
						 | 
					fa94acb426 | ||
| 
						 | 
					6827468f13 | ||
| 
						 | 
					53fd43868f | ||
| 
						 | 
					9ced7561c5 | ||
| 
						 | 
					31d55d3425 | ||
| 
						 | 
					171d2a5bb9 | ||
| 
						 | 
					c5d05c1205 | ||
| 
						 | 
					2973e0559a | ||
| 
						 | 
					ec27288dcf | ||
| 
						 | 
					f92e5c7093 | ||
| 
						 | 
					7c67155c49 | ||
| 
						 | 
					b102cd4652 | ||
| 
						 | 
					67f9a48c37 | ||
| 
						 | 
					a0c8a1ee65 | ||
| 
						 | 
					7e7d272b06 | ||
| 
						 | 
					3c642240ae | ||
| 
						 | 
					b5157fcaf1 | ||
| 
						 | 
					d1cb42f1bc | ||
| 
						 | 
					84cde1a16a | ||
| 
						 | 
					877f5db1ce | ||
| 
						 | 
					787164e245 | ||
| 
						 | 
					d77fc5e7c5 | ||
| 
						 | 
					cca39a67d6 | ||
| 
						 | 
					a6c9a0431a | ||
| 
						 | 
					729a80a639 | ||
| 
						 | 
					31cb3001f6 | ||
| 
						 | 
					5d0f54a329 | ||
| 
						 | 
					c8c3f5b5b7 | ||
| 
						 | 
					ba473ed75a | ||
| 
						 | 
					7236fd59f8 | ||
| 
						 | 
					9471e8f1fd | ||
| 
						 | 
					a2d39b51bb | ||
| 
						 | 
					2920934b55 | ||
| 
						 | 
					3f709d448e | ||
| 
						 | 
					b79f66183f | ||
| 
						 | 
					8672f57e55 | ||
| 
						 | 
					1e99c82351 | ||
| 
						 | 
					1a2ff851f3 | ||
| 
						 | 
					f1c27c3959 | ||
| 
						 | 
					b30dac0f15 | ||
| 
						 | 
					cc79e5cdaf | ||
| 
						 | 
					d9a3b2f2cb | ||
| 
						 | 
					479b528d09 | ||
| 
						 | 
					461fb84fb9 | ||
| 
						 | 
					bd7685e3fa | ||
| 
						 | 
					cd98cb64b3 | ||
| 
						 | 
					0f32a3ec24 | ||
| 
						 | 
					ca446cac87 | ||
| 
						 | 
					6ea907ffda | ||
| 
						 | 
					5287baa70d | ||
| 
						 | 
					25935fec84 | ||
| 
						 | 
					e855a063ff | ||
| 
						 | 
					c726b8c9f0 | ||
| 
						 | 
					13cb99290e | ||
| 
						 | 
					cea9413fd1 | ||
| 
						 | 
					1432853b39 | ||
| 
						 | 
					6d6c2b86e8 | ||
| 
						 | 
					77b1d964b5 | ||
| 
						 | 
					549936fc09 | ||
| 
						 | 
					c9c32f09c5 | ||
| 
						 | 
					77f7778d4a | ||
| 
						 | 
					84b6be9364 | ||
| 
						 | 
					1e43b55804 | ||
| 
						 | 
					ba9bdaae0a | ||
| 
						 | 
					7dfd7bde8e | ||
| 
						 | 
					5e6c4161d0 | ||
| 
						 | 
					d75d56dfc9 | ||
| 
						 | 
					1d9d350091 | ||
| 
						 | 
					5744053c6f | ||
| 
						 | 
					65589b6ca2 | ||
| 
						 | 
					e03a9d1137 | ||
| 
						 | 
					29f80f2276 | ||
| 
						 | 
					a9b74aa69b | ||
| 
						 | 
					63ebfd3210 | ||
| 
						 | 
					87fa5ff7a6 | ||
| 
						 | 
					b686b53a9c | ||
| 
						 | 
					258261dc64 | ||
| 
						 | 
					9af5c9ead9 | ||
| 
						 | 
					382654188c | ||
| 
						 | 
					fa1df082b7 | ||
| 
						 | 
					5c227d8f80 | ||
| 
						 | 
					81dabdbfb7 | ||
| 
						 | 
					91f89f5a33 | ||
| 
						 | 
					9f92746aa0 | ||
| 
						 | 
					5d6e6f9441 | ||
| 
						 | 
					01395a2726 | ||
| 
						 | 
					465d75c65d | ||
| 
						 | 
					4634f8927e | ||
| 
						 | 
					74a287f9fe | ||
| 
						 | 
					7ff6c79835 | ||
| 
						 | 
					3629982237 | ||
| 
						 | 
					ddb610f1bc | ||
| 
						 | 
					f899905d27 | ||
| 
						 | 
					3e4531b5c5 | ||
| 
						 | 
					a9e189e51d | ||
| 
						 | 
					58ba08a8f3 | ||
| 
						 | 
					9078ff27d8 | ||
| 
						 | 
					6f43e61c24 | ||
| 
						 | 
					4be0d3f212 | ||
| 
						 | 
					00e47e5a27 | ||
| 
						 | 
					152e145b32 | ||
| 
						 | 
					54e55e8f57 | ||
| 
						 | 
					05b8707f9e | ||
| 
						 | 
					543e952023 | ||
| 
						 | 
					6e5f40ea06 | ||
| 
						 | 
					bbafb0be87 | ||
| 
						 | 
					1c9c5232fe | ||
| 
						 | 
					598d79a502 | ||
| 
						 | 
					37d8360b77 | ||
| 
						 | 
					82d9ca3317 | ||
| 
						 | 
					4e4238d486 | ||
| 
						 | 
					c77dbe44dc | ||
| 
						 | 
					e03737f15f | ||
| 
						 | 
					a02629bcd7 | ||
| 
						 | 
					6c3fc23d78 | ||
| 
						 | 
					0fe40f9ccb | ||
| 
						 | 
					9bd7c8edd1 | ||
| 
						 | 
					83ba480863 | ||
| 
						 | 
					f158ea25e9 | ||
| 
						 | 
					0227519eab | ||
| 
						 | 
					616a9685fa | ||
| 
						 | 
					fe61b01320 | ||
| 
						 | 
					7b25144311 | ||
| 
						 | 
					9d42fbbdd7 | ||
| 
						 | 
					39ac5b088b | ||
| 
						 | 
					c14ffd08a0 | ||
| 
						 | 
					6e1239340b | ||
| 
						 | 
					a297dc8b3b | ||
| 
						 | 
					8d4ecc0898 | ||
| 
						 | 
					eae9c04429 | ||
| 
						 | 
					a41c48a9c5 | ||
| 
						 | 
					ff2a94bd9b | ||
| 
						 | 
					4a1f5558b8 | ||
| 
						 | 
					608db9889f | ||
| 
						 | 
					012b697337 | ||
| 
						 | 
					0580506cf3 | ||
| 
						 | 
					ff4ab9b661 | 
@@ -25,4 +25,8 @@ POSTGRES_PASS=postgrespass
 | 
			
		||||
# DEV SETTINGS
 | 
			
		||||
APP_PORT=80
 | 
			
		||||
API_PORT=80
 | 
			
		||||
API_PROTOCOL=https://
 | 
			
		||||
HTTP_PROTOCOL=https
 | 
			
		||||
DOCKER_NETWORK=172.21.0.0/24
 | 
			
		||||
DOCKER_NGINX_IP=172.21.0.20
 | 
			
		||||
NATS_PORTS=4222:4222
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
FROM python:3.9.2-slim
 | 
			
		||||
FROM python:3.9.6-slim
 | 
			
		||||
 | 
			
		||||
ENV TACTICAL_DIR /opt/tactical
 | 
			
		||||
ENV TACTICAL_GO_DIR /usr/local/rmmgo
 | 
			
		||||
ENV TACTICAL_READY_FILE ${TACTICAL_DIR}/tmp/tactical.ready
 | 
			
		||||
ENV WORKSPACE_DIR /workspace
 | 
			
		||||
ENV TACTICAL_USER tactical
 | 
			
		||||
@@ -9,20 +8,22 @@ ENV VIRTUAL_ENV ${WORKSPACE_DIR}/api/tacticalrmm/env
 | 
			
		||||
ENV PYTHONDONTWRITEBYTECODE=1
 | 
			
		||||
ENV PYTHONUNBUFFERED=1
 | 
			
		||||
 | 
			
		||||
EXPOSE 8000
 | 
			
		||||
EXPOSE 8000 8383 8005
 | 
			
		||||
 | 
			
		||||
RUN groupadd -g 1000 tactical && \
 | 
			
		||||
    useradd -u 1000 -g 1000 tactical
 | 
			
		||||
 | 
			
		||||
# Copy Go Files
 | 
			
		||||
COPY --from=golang:1.16 /usr/local/go ${TACTICAL_GO_DIR}/go
 | 
			
		||||
# Copy nats-api file
 | 
			
		||||
COPY natsapi/bin/nats-api /usr/local/bin/
 | 
			
		||||
RUN chmod +x /usr/local/bin/nats-api
 | 
			
		||||
 | 
			
		||||
# Copy Dev python reqs
 | 
			
		||||
COPY ./requirements.txt /
 | 
			
		||||
# Copy dev python reqs
 | 
			
		||||
COPY .devcontainer/requirements.txt  /
 | 
			
		||||
 | 
			
		||||
# Copy Docker Entrypoint
 | 
			
		||||
COPY ./entrypoint.sh /
 | 
			
		||||
# Copy docker entrypoint.sh
 | 
			
		||||
COPY .devcontainer/entrypoint.sh /
 | 
			
		||||
RUN chmod +x /entrypoint.sh
 | 
			
		||||
 | 
			
		||||
ENTRYPOINT ["/entrypoint.sh"]
 | 
			
		||||
 | 
			
		||||
WORKDIR ${WORKSPACE_DIR}/api/tacticalrmm
 | 
			
		||||
 
 | 
			
		||||
@@ -2,11 +2,12 @@ version: '3.4'
 | 
			
		||||
 | 
			
		||||
services:
 | 
			
		||||
  api-dev:
 | 
			
		||||
    container_name: trmm-api-dev
 | 
			
		||||
    image: api-dev
 | 
			
		||||
    restart: always
 | 
			
		||||
    build:
 | 
			
		||||
      context: .
 | 
			
		||||
      dockerfile: ./api.dockerfile
 | 
			
		||||
      context: ..
 | 
			
		||||
      dockerfile: .devcontainer/api.dockerfile
 | 
			
		||||
    command: ["tactical-api"]
 | 
			
		||||
    environment:
 | 
			
		||||
      API_PORT: ${API_PORT}
 | 
			
		||||
@@ -21,9 +22,10 @@ services:
 | 
			
		||||
          - tactical-backend
 | 
			
		||||
 | 
			
		||||
  app-dev:
 | 
			
		||||
    image: node:12-alpine
 | 
			
		||||
    container_name: trmm-app-dev
 | 
			
		||||
    image: node:14-alpine
 | 
			
		||||
    restart: always
 | 
			
		||||
    command: /bin/sh -c "npm install && npm run serve -- --host 0.0.0.0 --port ${APP_PORT}"
 | 
			
		||||
    command: /bin/sh -c "npm install npm@latest -g && npm install && npm run serve -- --host 0.0.0.0 --port ${APP_PORT}"
 | 
			
		||||
    working_dir: /workspace/web
 | 
			
		||||
    volumes:
 | 
			
		||||
      - ..:/workspace:cached
 | 
			
		||||
@@ -36,6 +38,7 @@ services:
 | 
			
		||||
 | 
			
		||||
  # nats
 | 
			
		||||
  nats-dev:
 | 
			
		||||
    container_name: trmm-nats-dev
 | 
			
		||||
    image: ${IMAGE_REPO}tactical-nats:${VERSION}
 | 
			
		||||
    restart: always
 | 
			
		||||
    environment:
 | 
			
		||||
@@ -43,7 +46,7 @@ services:
 | 
			
		||||
      API_PORT: ${API_PORT}
 | 
			
		||||
      DEV: 1
 | 
			
		||||
    ports:
 | 
			
		||||
      - "4222:4222"
 | 
			
		||||
      - "${NATS_PORTS}"
 | 
			
		||||
    volumes:
 | 
			
		||||
      - tactical-data-dev:/opt/tactical
 | 
			
		||||
      - ..:/workspace:cached
 | 
			
		||||
@@ -55,6 +58,7 @@ services:
 | 
			
		||||
 | 
			
		||||
  # meshcentral container
 | 
			
		||||
  meshcentral-dev:
 | 
			
		||||
    container_name: trmm-meshcentral-dev
 | 
			
		||||
    image: ${IMAGE_REPO}tactical-meshcentral:${VERSION}
 | 
			
		||||
    restart: always
 | 
			
		||||
    environment: 
 | 
			
		||||
@@ -63,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:
 | 
			
		||||
@@ -77,6 +81,7 @@ services:
 | 
			
		||||
 | 
			
		||||
  # mongodb container for meshcentral
 | 
			
		||||
  mongodb-dev:
 | 
			
		||||
    container_name: trmm-mongodb-dev
 | 
			
		||||
    image: mongo:4.4
 | 
			
		||||
    restart: always
 | 
			
		||||
    environment:
 | 
			
		||||
@@ -92,6 +97,7 @@ services:
 | 
			
		||||
 | 
			
		||||
  # postgres database for api service
 | 
			
		||||
  postgres-dev:
 | 
			
		||||
    container_name: trmm-postgres-dev
 | 
			
		||||
    image: postgres:13-alpine
 | 
			
		||||
    restart: always
 | 
			
		||||
    environment:
 | 
			
		||||
@@ -107,18 +113,20 @@ services:
 | 
			
		||||
 | 
			
		||||
  # redis container for celery tasks
 | 
			
		||||
  redis-dev:
 | 
			
		||||
    container_name: trmm-redis-dev
 | 
			
		||||
    restart: always
 | 
			
		||||
    command: redis-server --appendonly yes
 | 
			
		||||
    image: redis:6.0-alpine
 | 
			
		||||
    volumes: 
 | 
			
		||||
      - redis-data-dev:/data
 | 
			
		||||
    networks:
 | 
			
		||||
      dev:
 | 
			
		||||
        aliases:
 | 
			
		||||
          - tactical-redis
 | 
			
		||||
 | 
			
		||||
  init-dev:
 | 
			
		||||
    container_name: trmm-init-dev
 | 
			
		||||
    image: api-dev
 | 
			
		||||
    build:
 | 
			
		||||
      context: .
 | 
			
		||||
      dockerfile: ./api.dockerfile
 | 
			
		||||
    restart: on-failure
 | 
			
		||||
    command: ["tactical-init-dev"]
 | 
			
		||||
    environment:
 | 
			
		||||
@@ -143,10 +151,8 @@ services:
 | 
			
		||||
 | 
			
		||||
  # container for celery worker service
 | 
			
		||||
  celery-dev:
 | 
			
		||||
    container_name: trmm-celery-dev
 | 
			
		||||
    image: api-dev
 | 
			
		||||
    build:
 | 
			
		||||
      context: .
 | 
			
		||||
      dockerfile: ./api.dockerfile
 | 
			
		||||
    command: ["tactical-celery-dev"]
 | 
			
		||||
    restart: always
 | 
			
		||||
    networks:
 | 
			
		||||
@@ -160,10 +166,8 @@ services:
 | 
			
		||||
 | 
			
		||||
  # container for celery beat service
 | 
			
		||||
  celerybeat-dev:
 | 
			
		||||
    container_name: trmm-celerybeat-dev
 | 
			
		||||
    image: api-dev
 | 
			
		||||
    build:
 | 
			
		||||
      context: .
 | 
			
		||||
      dockerfile: ./api.dockerfile
 | 
			
		||||
    command: ["tactical-celerybeat-dev"]
 | 
			
		||||
    restart: always
 | 
			
		||||
    networks:
 | 
			
		||||
@@ -175,8 +179,26 @@ services:
 | 
			
		||||
      - postgres-dev
 | 
			
		||||
      - redis-dev
 | 
			
		||||
 | 
			
		||||
  nginx-dev:
 | 
			
		||||
  # container for websockets communication
 | 
			
		||||
  websockets-dev:
 | 
			
		||||
    container_name: trmm-websockets-dev
 | 
			
		||||
    image: api-dev
 | 
			
		||||
    command: ["tactical-websockets-dev"]
 | 
			
		||||
    restart: always
 | 
			
		||||
    networks:
 | 
			
		||||
      dev:
 | 
			
		||||
        aliases:
 | 
			
		||||
          - tactical-websockets
 | 
			
		||||
    volumes:
 | 
			
		||||
      - tactical-data-dev:/opt/tactical
 | 
			
		||||
      - ..:/workspace:cached
 | 
			
		||||
    depends_on:
 | 
			
		||||
      - postgres-dev
 | 
			
		||||
      - redis-dev
 | 
			
		||||
 | 
			
		||||
  # container for tactical reverse proxy
 | 
			
		||||
  nginx-dev:
 | 
			
		||||
    container_name: trmm-nginx-dev
 | 
			
		||||
    image: ${IMAGE_REPO}tactical-nginx:${VERSION}
 | 
			
		||||
    restart: always
 | 
			
		||||
    environment:
 | 
			
		||||
@@ -187,20 +209,35 @@ services:
 | 
			
		||||
      CERT_PRIV_KEY: ${CERT_PRIV_KEY}
 | 
			
		||||
      APP_PORT: ${APP_PORT}
 | 
			
		||||
      API_PORT: ${API_PORT}
 | 
			
		||||
      API_PROTOCOL: ${API_PROTOCOL}
 | 
			
		||||
      DEV: 1
 | 
			
		||||
    networks:
 | 
			
		||||
      dev:
 | 
			
		||||
        ipv4_address: 172.21.0.20
 | 
			
		||||
        ipv4_address: ${DOCKER_NGINX_IP}
 | 
			
		||||
    ports:
 | 
			
		||||
      - "80:80"
 | 
			
		||||
      - "443:443"
 | 
			
		||||
    volumes:
 | 
			
		||||
      - tactical-data-dev:/opt/tactical
 | 
			
		||||
 | 
			
		||||
  mkdocs-dev:
 | 
			
		||||
    container_name: trmm-mkdocs-dev
 | 
			
		||||
    image: api-dev
 | 
			
		||||
    restart: always
 | 
			
		||||
    command: ["tactical-mkdocs-dev"]
 | 
			
		||||
    ports:
 | 
			
		||||
      - "8005:8005"
 | 
			
		||||
    volumes:
 | 
			
		||||
      - ..:/workspace:cached
 | 
			
		||||
    networks:
 | 
			
		||||
      - dev
 | 
			
		||||
 | 
			
		||||
volumes:
 | 
			
		||||
  tactical-data-dev:
 | 
			
		||||
  postgres-data-dev:
 | 
			
		||||
  mongo-dev-data:
 | 
			
		||||
  mesh-data-dev:
 | 
			
		||||
  redis-data-dev:
 | 
			
		||||
 | 
			
		||||
networks:
 | 
			
		||||
  dev:
 | 
			
		||||
@@ -208,4 +245,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
 | 
			
		||||
@@ -136,10 +119,11 @@ if [ "$1" = 'tactical-init-dev' ]; then
 | 
			
		||||
  webenv="$(cat << EOF
 | 
			
		||||
PROD_URL = "${HTTP_PROTOCOL}://${API_HOST}"
 | 
			
		||||
DEV_URL = "${HTTP_PROTOCOL}://${API_HOST}"
 | 
			
		||||
APP_URL = https://${APP_HOST}
 | 
			
		||||
APP_URL = "https://${APP_HOST}"
 | 
			
		||||
DOCKER_BUILD = 1
 | 
			
		||||
EOF
 | 
			
		||||
)"
 | 
			
		||||
  echo "${webenv}" | tee ${WORKSPACE_DIR}/web/.env > /dev/null
 | 
			
		||||
  echo "${webenv}" | tee "${WORKSPACE_DIR}"/web/.env > /dev/null
 | 
			
		||||
 | 
			
		||||
  # chown everything to tactical user
 | 
			
		||||
  chown -R "${TACTICAL_USER}":"${TACTICAL_USER}" "${WORKSPACE_DIR}"
 | 
			
		||||
@@ -150,9 +134,6 @@ EOF
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
if [ "$1" = 'tactical-api' ]; then
 | 
			
		||||
  cp "${WORKSPACE_DIR}"/api/tacticalrmm/core/goinstaller/bin/goversioninfo /usr/local/bin/goversioninfo
 | 
			
		||||
  chmod +x /usr/local/bin/goversioninfo
 | 
			
		||||
  
 | 
			
		||||
  check_tactical_ready
 | 
			
		||||
  "${VIRTUAL_ENV}"/bin/python manage.py runserver 0.0.0.0:"${API_PORT}"
 | 
			
		||||
fi
 | 
			
		||||
@@ -167,3 +148,13 @@ if [ "$1" = 'tactical-celerybeat-dev' ]; then
 | 
			
		||||
  test -f "${WORKSPACE_DIR}/api/tacticalrmm/celerybeat.pid" && rm "${WORKSPACE_DIR}/api/tacticalrmm/celerybeat.pid"
 | 
			
		||||
  "${VIRTUAL_ENV}"/bin/celery -A tacticalrmm beat -l debug
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
if [ "$1" = 'tactical-websockets-dev' ]; then
 | 
			
		||||
  check_tactical_ready
 | 
			
		||||
  "${VIRTUAL_ENV}"/bin/daphne tacticalrmm.asgi:application --port 8383 -b 0.0.0.0
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
if [ "$1" = 'tactical-mkdocs-dev' ]; then
 | 
			
		||||
  cd "${WORKSPACE_DIR}/docs"
 | 
			
		||||
  "${VIRTUAL_ENV}"/bin/mkdocs serve
 | 
			
		||||
fi
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,9 @@
 | 
			
		||||
# To ensure app dependencies are ported from your virtual environment/host machine into your container, run 'pip freeze > requirements.txt' in the terminal to overwrite this file
 | 
			
		||||
asyncio-nats-client
 | 
			
		||||
celery
 | 
			
		||||
channels
 | 
			
		||||
channels_redis
 | 
			
		||||
django-ipware
 | 
			
		||||
Django
 | 
			
		||||
django-cors-headers
 | 
			
		||||
django-rest-knox
 | 
			
		||||
@@ -30,3 +33,5 @@ mkdocs-material
 | 
			
		||||
pymdown-extensions
 | 
			
		||||
Pygments
 | 
			
		||||
mypy
 | 
			
		||||
pysnooper
 | 
			
		||||
isort
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										40
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,40 @@
 | 
			
		||||
---
 | 
			
		||||
name: Bug report
 | 
			
		||||
about: Create a bug report
 | 
			
		||||
title: ''
 | 
			
		||||
labels: ''
 | 
			
		||||
assignees: ''
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
**Server Info (please complete the following information):**
 | 
			
		||||
 - OS: [e.g. Ubuntu 20.04, Debian 10]
 | 
			
		||||
 - Browser: [e.g. chrome, safari]
 | 
			
		||||
 - RMM Version (as shown in top left of web UI):
 | 
			
		||||
 | 
			
		||||
**Installation Method:**
 | 
			
		||||
  - [ ] Standard
 | 
			
		||||
  - [ ] Docker
 | 
			
		||||
 | 
			
		||||
**Agent Info (please complete the following information):**
 | 
			
		||||
- Agent version (as shown in the 'Summary' tab of the agent from web UI):
 | 
			
		||||
- Agent OS: [e.g. Win 10 v2004, Server 2012 R2]
 | 
			
		||||
 | 
			
		||||
**Describe the bug**
 | 
			
		||||
A clear and concise description of what the bug is.
 | 
			
		||||
 | 
			
		||||
**To Reproduce**
 | 
			
		||||
Steps to reproduce the behavior:
 | 
			
		||||
1. Go to '...'
 | 
			
		||||
2. Click on '....'
 | 
			
		||||
3. Scroll down to '....'
 | 
			
		||||
4. See error
 | 
			
		||||
 | 
			
		||||
**Expected behavior**
 | 
			
		||||
A clear and concise description of what you expected to happen.
 | 
			
		||||
 | 
			
		||||
**Screenshots**
 | 
			
		||||
If applicable, add screenshots to help explain your problem.
 | 
			
		||||
 | 
			
		||||
**Additional context**
 | 
			
		||||
Add any other context about the problem here.
 | 
			
		||||
							
								
								
									
										20
									
								
								.github/ISSUE_TEMPLATE/feature_request.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								.github/ISSUE_TEMPLATE/feature_request.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
			
		||||
---
 | 
			
		||||
name: Feature request
 | 
			
		||||
about: Suggest an idea for this project
 | 
			
		||||
title: ''
 | 
			
		||||
labels: ''
 | 
			
		||||
assignees: ''
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
**Is your feature request related to a problem? Please describe.**
 | 
			
		||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
 | 
			
		||||
 | 
			
		||||
**Describe the solution you'd like**
 | 
			
		||||
A clear and concise description of what you want to happen.
 | 
			
		||||
 | 
			
		||||
**Describe alternatives you've considered**
 | 
			
		||||
A clear and concise description of any alternative solutions or features you've considered.
 | 
			
		||||
 | 
			
		||||
**Additional context**
 | 
			
		||||
Add any other context or screenshots about the feature request here.
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/workflows/deploy-docs.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/deploy-docs.yml
									
									
									
									
										vendored
									
									
								
							@@ -2,7 +2,7 @@ name: Deploy Docs
 | 
			
		||||
on:
 | 
			
		||||
  push:
 | 
			
		||||
    branches:
 | 
			
		||||
      - develop
 | 
			
		||||
      - master
 | 
			
		||||
 | 
			
		||||
defaults:
 | 
			
		||||
  run:
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -47,3 +47,5 @@ docs/.vuepress/dist
 | 
			
		||||
nats-rmm.conf
 | 
			
		||||
.mypy_cache
 | 
			
		||||
docs/site/
 | 
			
		||||
reset_db.sh
 | 
			
		||||
run_go_cmd.py
 | 
			
		||||
 
 | 
			
		||||
@@ -9,9 +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.
 | 
			
		||||
 | 
			
		||||
*Tactical RMM is currently in alpha and subject to breaking changes. Use in production at your own risk.*
 | 
			
		||||
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)
 | 
			
		||||
 | 
			
		||||
@@ -37,4 +35,4 @@ Demo database resets every hour. Alot of features are disabled for obvious reaso
 | 
			
		||||
 | 
			
		||||
## Installation / Backup / Restore / Usage
 | 
			
		||||
 | 
			
		||||
### Refer to the [documentation](https://wh1te909.github.io/tacticalrmm/)
 | 
			
		||||
### Refer to the [documentation](https://wh1te909.github.io/tacticalrmm/)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,8 @@
 | 
			
		||||
from django.contrib import admin
 | 
			
		||||
from rest_framework.authtoken.admin import TokenAdmin
 | 
			
		||||
 | 
			
		||||
from .models import User
 | 
			
		||||
from .models import User, Role
 | 
			
		||||
 | 
			
		||||
admin.site.register(User)
 | 
			
		||||
TokenAdmin.raw_id_fields = ("user",)
 | 
			
		||||
admin.site.register(Role)
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,19 @@
 | 
			
		||||
import uuid
 | 
			
		||||
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
from accounts.models import User
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
    help = "Creates the installer user"
 | 
			
		||||
 | 
			
		||||
    def handle(self, *args, **kwargs):
 | 
			
		||||
        if User.objects.filter(is_installer_user=True).exists():
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        User.objects.create_user(  # type: ignore
 | 
			
		||||
            username=uuid.uuid4().hex,
 | 
			
		||||
            is_installer_user=True,
 | 
			
		||||
            password=User.objects.make_random_password(60),  # type: ignore
 | 
			
		||||
            block_dashboard_login=True,
 | 
			
		||||
        )
 | 
			
		||||
@@ -0,0 +1,18 @@
 | 
			
		||||
# Generated by Django 3.2 on 2021-04-11 01:43
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('accounts', '0013_user_client_tree_sort'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='client_tree_splitter',
 | 
			
		||||
            field=models.PositiveIntegerField(default=11),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -0,0 +1,18 @@
 | 
			
		||||
# Generated by Django 3.2 on 2021-04-11 03:03
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('accounts', '0014_user_client_tree_splitter'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='loading_bar_color',
 | 
			
		||||
            field=models.CharField(default='red', max_length=255),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -0,0 +1,25 @@
 | 
			
		||||
# Generated by Django 3.2.1 on 2021-05-07 15:26
 | 
			
		||||
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('core', '0022_urlaction'),
 | 
			
		||||
        ('accounts', '0015_user_loading_bar_color'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='url_action',
 | 
			
		||||
            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='user', to='core.urlaction'),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='agent_dblclick_action',
 | 
			
		||||
            field=models.CharField(choices=[('editagent', 'Edit Agent'), ('takecontrol', 'Take Control'), ('remotebg', 'Remote Background'), ('urlaction', 'URL Action')], default='editagent', max_length=50),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										173
									
								
								api/tacticalrmm/accounts/migrations/0017_auto_20210508_1716.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										173
									
								
								api/tacticalrmm/accounts/migrations/0017_auto_20210508_1716.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,173 @@
 | 
			
		||||
# Generated by Django 3.2.1 on 2021-05-08 17:16
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('accounts', '0016_auto_20210507_1526'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_code_sign',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_do_server_maint',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_edit_agent',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_edit_core_settings',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_install_agents',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_manage_accounts',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_manage_alerts',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_manage_automation_policies',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_manage_autotasks',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_manage_checks',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_manage_clients',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_manage_deployments',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_manage_notes',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_manage_pendingactions',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_manage_procs',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_manage_scripts',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_manage_sites',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_manage_software',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_manage_winsvcs',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_manage_winupdates',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_reboot_agents',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_run_autotasks',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_run_bulk',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_run_checks',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_run_scripts',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_send_cmd',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_uninstall_agents',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_update_agents',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_use_mesh',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_view_auditlogs',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_view_debuglogs',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_view_eventlogs',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										181
									
								
								api/tacticalrmm/accounts/migrations/0018_auto_20210511_0233.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										181
									
								
								api/tacticalrmm/accounts/migrations/0018_auto_20210511_0233.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,181 @@
 | 
			
		||||
# Generated by Django 3.2.1 on 2021-05-11 02:33
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('accounts', '0017_auto_20210508_1716'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='Role',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
			
		||||
                ('name', models.CharField(max_length=255, unique=True)),
 | 
			
		||||
                ('is_superuser', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_use_mesh', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_uninstall_agents', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_update_agents', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_edit_agent', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_manage_procs', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_view_eventlogs', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_send_cmd', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_reboot_agents', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_install_agents', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_run_scripts', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_run_bulk', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_manage_notes', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_edit_core_settings', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_do_server_maint', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_code_sign', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_manage_checks', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_run_checks', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_manage_clients', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_manage_sites', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_manage_deployments', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_manage_automation_policies', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_manage_autotasks', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_run_autotasks', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_view_auditlogs', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_manage_pendingactions', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_view_debuglogs', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_manage_scripts', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_manage_alerts', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_manage_winsvcs', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_manage_software', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_manage_winupdates', models.BooleanField(default=False)),
 | 
			
		||||
                ('can_manage_accounts', models.BooleanField(default=False)),
 | 
			
		||||
            ],
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_code_sign',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_do_server_maint',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_edit_agent',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_edit_core_settings',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_install_agents',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_manage_accounts',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_manage_alerts',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_manage_automation_policies',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_manage_autotasks',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_manage_checks',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_manage_clients',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_manage_deployments',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_manage_notes',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_manage_pendingactions',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_manage_procs',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_manage_scripts',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_manage_sites',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_manage_software',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_manage_winsvcs',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_manage_winupdates',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_reboot_agents',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_run_autotasks',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_run_bulk',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_run_checks',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_run_scripts',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_send_cmd',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_uninstall_agents',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_update_agents',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_use_mesh',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_view_auditlogs',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_view_debuglogs',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='can_view_eventlogs',
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										25
									
								
								api/tacticalrmm/accounts/migrations/0019_user_role.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								api/tacticalrmm/accounts/migrations/0019_user_role.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,25 @@
 | 
			
		||||
# Generated by Django 3.2.1 on 2021-05-11 02:33
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("accounts", "0018_auto_20210511_0233"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name="user",
 | 
			
		||||
            name="role",
 | 
			
		||||
            field=models.ForeignKey(
 | 
			
		||||
                blank=True,
 | 
			
		||||
                null=True,
 | 
			
		||||
                on_delete=django.db.models.deletion.SET_NULL,
 | 
			
		||||
                related_name="roles",
 | 
			
		||||
                to="accounts.role",
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -0,0 +1,18 @@
 | 
			
		||||
# Generated by Django 3.2.1 on 2021-05-11 17:37
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('accounts', '0019_user_role'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='role',
 | 
			
		||||
            name='can_manage_roles',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -0,0 +1,18 @@
 | 
			
		||||
# Generated by Django 3.2.4 on 2021-06-17 04:29
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('accounts', '0020_role_can_manage_roles'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='role',
 | 
			
		||||
            name='can_view_core_settings',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -0,0 +1,18 @@
 | 
			
		||||
# Generated by Django 3.2.4 on 2021-06-28 05:01
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('accounts', '0021_role_can_view_core_settings'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='clear_search_when_switching',
 | 
			
		||||
            field=models.BooleanField(default=True),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -0,0 +1,18 @@
 | 
			
		||||
# Generated by Django 3.2.4 on 2021-06-30 03:22
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('accounts', '0022_user_clear_search_when_switching'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='is_installer_user',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -0,0 +1,18 @@
 | 
			
		||||
# Generated by Django 3.2.1 on 2021-07-20 20:26
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('accounts', '0023_user_is_installer_user'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='last_login_ip',
 | 
			
		||||
            field=models.GenericIPAddressField(blank=True, default=None, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -0,0 +1,33 @@
 | 
			
		||||
# Generated by Django 3.2.1 on 2021-07-21 04:24
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('accounts', '0024_user_last_login_ip'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='role',
 | 
			
		||||
            name='created_by',
 | 
			
		||||
            field=models.CharField(blank=True, max_length=100, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='role',
 | 
			
		||||
            name='created_time',
 | 
			
		||||
            field=models.DateTimeField(auto_now_add=True, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='role',
 | 
			
		||||
            name='modified_by',
 | 
			
		||||
            field=models.CharField(blank=True, max_length=100, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='role',
 | 
			
		||||
            name='modified_time',
 | 
			
		||||
            field=models.DateTimeField(auto_now=True, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -0,0 +1,34 @@
 | 
			
		||||
# Generated by Django 3.2.6 on 2021-09-01 12:47
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('accounts', '0025_auto_20210721_0424'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='APIKey',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
			
		||||
                ('created_by', models.CharField(blank=True, max_length=100, null=True)),
 | 
			
		||||
                ('created_time', models.DateTimeField(auto_now_add=True, null=True)),
 | 
			
		||||
                ('modified_by', models.CharField(blank=True, max_length=100, null=True)),
 | 
			
		||||
                ('modified_time', models.DateTimeField(auto_now=True, null=True)),
 | 
			
		||||
                ('name', models.CharField(max_length=25, unique=True)),
 | 
			
		||||
                ('key', models.CharField(blank=True, max_length=48, unique=True)),
 | 
			
		||||
                ('expiration', models.DateTimeField(blank=True, default=None, null=True)),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                'abstract': False,
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='role',
 | 
			
		||||
            name='can_manage_api_keys',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -0,0 +1,25 @@
 | 
			
		||||
# Generated by Django 3.2.6 on 2021-09-03 00:54
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('accounts', '0026_auto_20210901_1247'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='apikey',
 | 
			
		||||
            name='user',
 | 
			
		||||
            field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='api_key', to='accounts.user'),
 | 
			
		||||
            preserve_default=False,
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='user',
 | 
			
		||||
            name='block_dashboard_login',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
from django.contrib.auth.models import AbstractUser
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.db.models.fields import CharField, DateTimeField
 | 
			
		||||
 | 
			
		||||
from logs.models import BaseAuditModel
 | 
			
		||||
 | 
			
		||||
@@ -7,6 +8,7 @@ AGENT_DBLCLICK_CHOICES = [
 | 
			
		||||
    ("editagent", "Edit Agent"),
 | 
			
		||||
    ("takecontrol", "Take Control"),
 | 
			
		||||
    ("remotebg", "Remote Background"),
 | 
			
		||||
    ("urlaction", "URL Action"),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
AGENT_TBL_TAB_CHOICES = [
 | 
			
		||||
@@ -23,12 +25,20 @@ CLIENT_TREE_SORT_CHOICES = [
 | 
			
		||||
 | 
			
		||||
class User(AbstractUser, BaseAuditModel):
 | 
			
		||||
    is_active = models.BooleanField(default=True)
 | 
			
		||||
    block_dashboard_login = models.BooleanField(default=False)
 | 
			
		||||
    totp_key = models.CharField(max_length=50, null=True, blank=True)
 | 
			
		||||
    dark_mode = models.BooleanField(default=True)
 | 
			
		||||
    show_community_scripts = models.BooleanField(default=True)
 | 
			
		||||
    agent_dblclick_action = models.CharField(
 | 
			
		||||
        max_length=50, choices=AGENT_DBLCLICK_CHOICES, default="editagent"
 | 
			
		||||
    )
 | 
			
		||||
    url_action = models.ForeignKey(
 | 
			
		||||
        "core.URLAction",
 | 
			
		||||
        related_name="user",
 | 
			
		||||
        null=True,
 | 
			
		||||
        blank=True,
 | 
			
		||||
        on_delete=models.SET_NULL,
 | 
			
		||||
    )
 | 
			
		||||
    default_agent_tbl_tab = models.CharField(
 | 
			
		||||
        max_length=50, choices=AGENT_TBL_TAB_CHOICES, default="server"
 | 
			
		||||
    )
 | 
			
		||||
@@ -36,6 +46,11 @@ class User(AbstractUser, BaseAuditModel):
 | 
			
		||||
    client_tree_sort = models.CharField(
 | 
			
		||||
        max_length=50, choices=CLIENT_TREE_SORT_CHOICES, default="alphafail"
 | 
			
		||||
    )
 | 
			
		||||
    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",
 | 
			
		||||
@@ -45,9 +60,153 @@ class User(AbstractUser, BaseAuditModel):
 | 
			
		||||
        on_delete=models.CASCADE,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    role = models.ForeignKey(
 | 
			
		||||
        "accounts.Role",
 | 
			
		||||
        null=True,
 | 
			
		||||
        blank=True,
 | 
			
		||||
        related_name="roles",
 | 
			
		||||
        on_delete=models.SET_NULL,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def serialize(user):
 | 
			
		||||
        # serializes the task and returns json
 | 
			
		||||
        from .serializers import UserSerializer
 | 
			
		||||
 | 
			
		||||
        return UserSerializer(user).data
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Role(BaseAuditModel):
 | 
			
		||||
    name = models.CharField(max_length=255, unique=True)
 | 
			
		||||
    is_superuser = models.BooleanField(default=False)
 | 
			
		||||
 | 
			
		||||
    # agents
 | 
			
		||||
    can_use_mesh = models.BooleanField(default=False)
 | 
			
		||||
    can_uninstall_agents = models.BooleanField(default=False)
 | 
			
		||||
    can_update_agents = models.BooleanField(default=False)
 | 
			
		||||
    can_edit_agent = models.BooleanField(default=False)
 | 
			
		||||
    can_manage_procs = models.BooleanField(default=False)
 | 
			
		||||
    can_view_eventlogs = models.BooleanField(default=False)
 | 
			
		||||
    can_send_cmd = models.BooleanField(default=False)
 | 
			
		||||
    can_reboot_agents = models.BooleanField(default=False)
 | 
			
		||||
    can_install_agents = models.BooleanField(default=False)
 | 
			
		||||
    can_run_scripts = models.BooleanField(default=False)
 | 
			
		||||
    can_run_bulk = models.BooleanField(default=False)
 | 
			
		||||
 | 
			
		||||
    # core
 | 
			
		||||
    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)
 | 
			
		||||
 | 
			
		||||
    # checks
 | 
			
		||||
    can_manage_checks = models.BooleanField(default=False)
 | 
			
		||||
    can_run_checks = models.BooleanField(default=False)
 | 
			
		||||
 | 
			
		||||
    # clients
 | 
			
		||||
    can_manage_clients = models.BooleanField(default=False)
 | 
			
		||||
    can_manage_sites = models.BooleanField(default=False)
 | 
			
		||||
    can_manage_deployments = models.BooleanField(default=False)
 | 
			
		||||
 | 
			
		||||
    # automation
 | 
			
		||||
    can_manage_automation_policies = models.BooleanField(default=False)
 | 
			
		||||
 | 
			
		||||
    # automated tasks
 | 
			
		||||
    can_manage_autotasks = models.BooleanField(default=False)
 | 
			
		||||
    can_run_autotasks = models.BooleanField(default=False)
 | 
			
		||||
 | 
			
		||||
    # logs
 | 
			
		||||
    can_view_auditlogs = models.BooleanField(default=False)
 | 
			
		||||
    can_manage_pendingactions = models.BooleanField(default=False)
 | 
			
		||||
    can_view_debuglogs = models.BooleanField(default=False)
 | 
			
		||||
 | 
			
		||||
    # scripts
 | 
			
		||||
    can_manage_scripts = models.BooleanField(default=False)
 | 
			
		||||
 | 
			
		||||
    # alerts
 | 
			
		||||
    can_manage_alerts = models.BooleanField(default=False)
 | 
			
		||||
 | 
			
		||||
    # win services
 | 
			
		||||
    can_manage_winsvcs = models.BooleanField(default=False)
 | 
			
		||||
 | 
			
		||||
    # software
 | 
			
		||||
    can_manage_software = models.BooleanField(default=False)
 | 
			
		||||
 | 
			
		||||
    # windows updates
 | 
			
		||||
    can_manage_winupdates = models.BooleanField(default=False)
 | 
			
		||||
 | 
			
		||||
    # accounts
 | 
			
		||||
    can_manage_accounts = models.BooleanField(default=False)
 | 
			
		||||
    can_manage_roles = models.BooleanField(default=False)
 | 
			
		||||
 | 
			
		||||
    # authentication
 | 
			
		||||
    can_manage_api_keys = models.BooleanField(default=False)
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return self.name
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def serialize(role):
 | 
			
		||||
        # serializes the agent and returns json
 | 
			
		||||
        from .serializers import RoleAuditSerializer
 | 
			
		||||
 | 
			
		||||
        return RoleAuditSerializer(role).data
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def perms():
 | 
			
		||||
        return [
 | 
			
		||||
            "is_superuser",
 | 
			
		||||
            "can_use_mesh",
 | 
			
		||||
            "can_uninstall_agents",
 | 
			
		||||
            "can_update_agents",
 | 
			
		||||
            "can_edit_agent",
 | 
			
		||||
            "can_manage_procs",
 | 
			
		||||
            "can_view_eventlogs",
 | 
			
		||||
            "can_send_cmd",
 | 
			
		||||
            "can_reboot_agents",
 | 
			
		||||
            "can_install_agents",
 | 
			
		||||
            "can_run_scripts",
 | 
			
		||||
            "can_run_bulk",
 | 
			
		||||
            "can_manage_notes",
 | 
			
		||||
            "can_view_core_settings",
 | 
			
		||||
            "can_edit_core_settings",
 | 
			
		||||
            "can_do_server_maint",
 | 
			
		||||
            "can_code_sign",
 | 
			
		||||
            "can_manage_checks",
 | 
			
		||||
            "can_run_checks",
 | 
			
		||||
            "can_manage_clients",
 | 
			
		||||
            "can_manage_sites",
 | 
			
		||||
            "can_manage_deployments",
 | 
			
		||||
            "can_manage_automation_policies",
 | 
			
		||||
            "can_manage_autotasks",
 | 
			
		||||
            "can_run_autotasks",
 | 
			
		||||
            "can_view_auditlogs",
 | 
			
		||||
            "can_manage_pendingactions",
 | 
			
		||||
            "can_view_debuglogs",
 | 
			
		||||
            "can_manage_scripts",
 | 
			
		||||
            "can_manage_alerts",
 | 
			
		||||
            "can_manage_winsvcs",
 | 
			
		||||
            "can_manage_software",
 | 
			
		||||
            "can_manage_winupdates",
 | 
			
		||||
            "can_manage_accounts",
 | 
			
		||||
            "can_manage_roles",
 | 
			
		||||
            "can_manage_api_keys",
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class APIKey(BaseAuditModel):
 | 
			
		||||
    name = CharField(unique=True, max_length=25)
 | 
			
		||||
    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
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										40
									
								
								api/tacticalrmm/accounts/permissions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								api/tacticalrmm/accounts/permissions.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,40 @@
 | 
			
		||||
from rest_framework import permissions
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.permissions import _has_perm
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AccountsPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        if r.method == "GET":
 | 
			
		||||
            return True
 | 
			
		||||
 | 
			
		||||
        # 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_manage_roles")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class APIKeyPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
 | 
			
		||||
        return _has_perm(r, "can_manage_api_keys")
 | 
			
		||||
@@ -1,7 +1,11 @@
 | 
			
		||||
import pyotp
 | 
			
		||||
from rest_framework.serializers import ModelSerializer, SerializerMethodField
 | 
			
		||||
from rest_framework.serializers import (
 | 
			
		||||
    ModelSerializer,
 | 
			
		||||
    SerializerMethodField,
 | 
			
		||||
    ReadOnlyField,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
from .models import User
 | 
			
		||||
from .models import APIKey, User, Role
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UserUISerializer(ModelSerializer):
 | 
			
		||||
@@ -11,15 +15,20 @@ class UserUISerializer(ModelSerializer):
 | 
			
		||||
            "dark_mode",
 | 
			
		||||
            "show_community_scripts",
 | 
			
		||||
            "agent_dblclick_action",
 | 
			
		||||
            "url_action",
 | 
			
		||||
            "default_agent_tbl_tab",
 | 
			
		||||
            "client_tree_sort",
 | 
			
		||||
            "client_tree_splitter",
 | 
			
		||||
            "loading_bar_color",
 | 
			
		||||
            "clear_search_when_switching",
 | 
			
		||||
            "block_dashboard_login",
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UserSerializer(ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = User
 | 
			
		||||
        fields = (
 | 
			
		||||
        fields = [
 | 
			
		||||
            "id",
 | 
			
		||||
            "username",
 | 
			
		||||
            "first_name",
 | 
			
		||||
@@ -27,7 +36,10 @@ class UserSerializer(ModelSerializer):
 | 
			
		||||
            "email",
 | 
			
		||||
            "is_active",
 | 
			
		||||
            "last_login",
 | 
			
		||||
        )
 | 
			
		||||
            "last_login_ip",
 | 
			
		||||
            "role",
 | 
			
		||||
            "block_dashboard_login",
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TOTPSetupSerializer(ModelSerializer):
 | 
			
		||||
@@ -46,3 +58,36 @@ class TOTPSetupSerializer(ModelSerializer):
 | 
			
		||||
        return pyotp.totp.TOTP(obj.totp_key).provisioning_uri(
 | 
			
		||||
            obj.username, issuer_name="Tactical RMM"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RoleSerializer(ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Role
 | 
			
		||||
        fields = "__all__"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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):
 | 
			
		||||
@@ -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/"
 | 
			
		||||
@@ -278,6 +286,9 @@ class TestUserAction(TacticalTestCase):
 | 
			
		||||
            "agent_dblclick_action": "editagent",
 | 
			
		||||
            "default_agent_tbl_tab": "mixed",
 | 
			
		||||
            "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)
 | 
			
		||||
@@ -285,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()
 | 
			
		||||
@@ -310,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/clients/"
 | 
			
		||||
        # auth should fail if no header set
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
        # invalid api key in header should return code 400
 | 
			
		||||
        self.client.credentials(HTTP_X_API_KEY="000000")
 | 
			
		||||
        r = self.client.get(url, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 401)
 | 
			
		||||
 | 
			
		||||
        # valid api key in header should return code 200
 | 
			
		||||
        self.client.credentials(HTTP_X_API_KEY="123456")
 | 
			
		||||
        r = self.client.get(url, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 
 | 
			
		||||
@@ -9,4 +9,9 @@ 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("apikeys/", views.GetAddAPIKeys.as_view()),
 | 
			
		||||
    path("apikeys/<int:pk>/", views.GetUpdateDeleteAPIKey.as_view()),
 | 
			
		||||
]
 | 
			
		||||
 
 | 
			
		||||
@@ -3,18 +3,25 @@ from django.conf import settings
 | 
			
		||||
from django.contrib.auth import login
 | 
			
		||||
from django.db import IntegrityError
 | 
			
		||||
from django.shortcuts import get_object_or_404
 | 
			
		||||
from ipware import get_client_ip
 | 
			
		||||
from knox.views import LoginView as KnoxLoginView
 | 
			
		||||
from logs.models import AuditLog
 | 
			
		||||
from rest_framework import status
 | 
			
		||||
from rest_framework.authtoken.serializers import AuthTokenSerializer
 | 
			
		||||
from rest_framework.permissions import AllowAny
 | 
			
		||||
from rest_framework.permissions import AllowAny, IsAuthenticated
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
from rest_framework.views import APIView
 | 
			
		||||
 | 
			
		||||
from logs.models import AuditLog
 | 
			
		||||
from tacticalrmm.utils import notify_error
 | 
			
		||||
 | 
			
		||||
from .models import User
 | 
			
		||||
from .serializers import TOTPSetupSerializer, UserSerializer, UserUISerializer
 | 
			
		||||
from .models import APIKey, Role, User
 | 
			
		||||
from .permissions import APIKeyPerms, AccountsPerms, RolesPerms
 | 
			
		||||
from .serializers import (
 | 
			
		||||
    APIKeySerializer,
 | 
			
		||||
    RoleSerializer,
 | 
			
		||||
    TOTPSetupSerializer,
 | 
			
		||||
    UserSerializer,
 | 
			
		||||
    UserUISerializer,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _is_root_user(request, user) -> bool:
 | 
			
		||||
@@ -34,11 +41,16 @@ class CheckCreds(KnoxLoginView):
 | 
			
		||||
        # check credentials
 | 
			
		||||
        serializer = AuthTokenSerializer(data=request.data)
 | 
			
		||||
        if not serializer.is_valid():
 | 
			
		||||
            AuditLog.audit_user_failed_login(request.data["username"])
 | 
			
		||||
            AuditLog.audit_user_failed_login(
 | 
			
		||||
                request.data["username"], debug_info={"ip": request._client_ip}
 | 
			
		||||
            )
 | 
			
		||||
            return Response("bad credentials", status=status.HTTP_400_BAD_REQUEST)
 | 
			
		||||
 | 
			
		||||
        user = serializer.validated_data["user"]
 | 
			
		||||
 | 
			
		||||
        if user.block_dashboard_login:
 | 
			
		||||
            return Response("bad credentials", status=status.HTTP_400_BAD_REQUEST)
 | 
			
		||||
 | 
			
		||||
        # if totp token not set modify response to notify frontend
 | 
			
		||||
        if not user.totp_key:
 | 
			
		||||
            login(request, user)
 | 
			
		||||
@@ -60,6 +72,9 @@ class LoginView(KnoxLoginView):
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        user = serializer.validated_data["user"]
 | 
			
		||||
 | 
			
		||||
        if user.block_dashboard_login:
 | 
			
		||||
            return Response("bad credentials", status=status.HTTP_400_BAD_REQUEST)
 | 
			
		||||
 | 
			
		||||
        token = request.data["twofactor"]
 | 
			
		||||
        totp = pyotp.TOTP(user.totp_key)
 | 
			
		||||
 | 
			
		||||
@@ -70,16 +85,35 @@ class LoginView(KnoxLoginView):
 | 
			
		||||
 | 
			
		||||
        if valid:
 | 
			
		||||
            login(request, user)
 | 
			
		||||
            AuditLog.audit_user_login_successful(request.data["username"])
 | 
			
		||||
 | 
			
		||||
            # save ip information
 | 
			
		||||
            client_ip, is_routable = get_client_ip(request)
 | 
			
		||||
            user.last_login_ip = client_ip
 | 
			
		||||
            user.save()
 | 
			
		||||
 | 
			
		||||
            AuditLog.audit_user_login_successful(
 | 
			
		||||
                request.data["username"], debug_info={"ip": request._client_ip}
 | 
			
		||||
            )
 | 
			
		||||
            return super(LoginView, self).post(request, format=None)
 | 
			
		||||
        else:
 | 
			
		||||
            AuditLog.audit_user_failed_twofactor(request.data["username"])
 | 
			
		||||
            AuditLog.audit_user_failed_twofactor(
 | 
			
		||||
                request.data["username"], debug_info={"ip": request._client_ip}
 | 
			
		||||
            )
 | 
			
		||||
            return Response("bad credentials", status=status.HTTP_400_BAD_REQUEST)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetAddUsers(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, AccountsPerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request):
 | 
			
		||||
        users = User.objects.filter(agent=None)
 | 
			
		||||
        search = request.GET.get("search", None)
 | 
			
		||||
 | 
			
		||||
        if search:
 | 
			
		||||
            users = User.objects.filter(agent=None, is_installer_user=False).filter(
 | 
			
		||||
                username__icontains=search
 | 
			
		||||
            )
 | 
			
		||||
        else:
 | 
			
		||||
            users = User.objects.filter(agent=None, is_installer_user=False)
 | 
			
		||||
 | 
			
		||||
        return Response(UserSerializer(users, many=True).data)
 | 
			
		||||
 | 
			
		||||
@@ -96,15 +130,21 @@ class GetAddUsers(APIView):
 | 
			
		||||
                f"ERROR: User {request.data['username']} already exists!"
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        user.first_name = request.data["first_name"]
 | 
			
		||||
        user.last_name = request.data["last_name"]
 | 
			
		||||
        # Can be changed once permissions and groups are introduced
 | 
			
		||||
        user.is_superuser = True
 | 
			
		||||
        if "first_name" in request.data.keys():
 | 
			
		||||
            user.first_name = request.data["first_name"]
 | 
			
		||||
        if "last_name" in request.data.keys():
 | 
			
		||||
            user.last_name = request.data["last_name"]
 | 
			
		||||
        if "role" in request.data.keys() and isinstance(request.data["role"], int):
 | 
			
		||||
            role = get_object_or_404(Role, pk=request.data["role"])
 | 
			
		||||
            user.role = role
 | 
			
		||||
 | 
			
		||||
        user.save()
 | 
			
		||||
        return Response(user.username)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetUpdateDeleteUser(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, AccountsPerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request, pk):
 | 
			
		||||
        user = get_object_or_404(User, pk=pk)
 | 
			
		||||
 | 
			
		||||
@@ -133,7 +173,7 @@ class GetUpdateDeleteUser(APIView):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UserActions(APIView):
 | 
			
		||||
 | 
			
		||||
    permission_classes = [IsAuthenticated, AccountsPerms]
 | 
			
		||||
    # reset password
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
        user = get_object_or_404(User, pk=request.data["id"])
 | 
			
		||||
@@ -182,3 +222,87 @@ class UserUI(APIView):
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        serializer.save()
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PermsList(APIView):
 | 
			
		||||
    def get(self, request):
 | 
			
		||||
        return Response(Role.perms())
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetAddRoles(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, RolesPerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request):
 | 
			
		||||
        roles = Role.objects.all()
 | 
			
		||||
        return Response(RoleSerializer(roles, many=True).data)
 | 
			
		||||
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
        serializer = RoleSerializer(data=request.data)
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        serializer.save()
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetUpdateDeleteRole(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, RolesPerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request, pk):
 | 
			
		||||
        role = get_object_or_404(Role, pk=pk)
 | 
			
		||||
        return Response(RoleSerializer(role).data)
 | 
			
		||||
 | 
			
		||||
    def put(self, request, pk):
 | 
			
		||||
        role = get_object_or_404(Role, pk=pk)
 | 
			
		||||
        serializer = RoleSerializer(instance=role, data=request.data)
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        serializer.save()
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
    def delete(self, request, pk):
 | 
			
		||||
        role = get_object_or_404(Role, pk=pk)
 | 
			
		||||
        role.delete()
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
        # https://stackoverflow.com/questions/2257441/random-string-generation-with-upper-case-letters-and-digits/23728630#23728630
 | 
			
		||||
        import random
 | 
			
		||||
        import string
 | 
			
		||||
 | 
			
		||||
        request.data["key"] = "".join(
 | 
			
		||||
            random.SystemRandom().choice(string.ascii_uppercase + string.digits)
 | 
			
		||||
            for _ in range(32)
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        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,7 +1,9 @@
 | 
			
		||||
from django.contrib import admin
 | 
			
		||||
 | 
			
		||||
from .models import Agent, 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)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										24
									
								
								api/tacticalrmm/agents/migrations/0032_agentcustomfield.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								api/tacticalrmm/agents/migrations/0032_agentcustomfield.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,24 @@
 | 
			
		||||
# Generated by Django 3.1.7 on 2021-03-17 14:45
 | 
			
		||||
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('core', '0014_customfield'),
 | 
			
		||||
        ('agents', '0031_agent_alert_template'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='AgentCustomField',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
			
		||||
                ('value', models.TextField(blank=True, null=True)),
 | 
			
		||||
                ('agent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='custom_fields', to='agents.agent')),
 | 
			
		||||
                ('field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='agent_fields', to='core.customfield')),
 | 
			
		||||
            ],
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -0,0 +1,19 @@
 | 
			
		||||
# Generated by Django 3.1.7 on 2021-03-29 02:51
 | 
			
		||||
 | 
			
		||||
import django.contrib.postgres.fields
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('agents', '0032_agentcustomfield'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='agentcustomfield',
 | 
			
		||||
            name='multiple_value',
 | 
			
		||||
            field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(blank=True, null=True), blank=True, default=list, null=True, size=None),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -0,0 +1,18 @@
 | 
			
		||||
# Generated by Django 3.1.7 on 2021-03-29 03:01
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('agents', '0033_agentcustomfield_multiple_value'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='agentcustomfield',
 | 
			
		||||
            name='checkbox_value',
 | 
			
		||||
            field=models.BooleanField(blank=True, default=False),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										23
									
								
								api/tacticalrmm/agents/migrations/0035_auto_20210329_1709.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								api/tacticalrmm/agents/migrations/0035_auto_20210329_1709.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
# Generated by Django 3.1.7 on 2021-03-29 17:09
 | 
			
		||||
 | 
			
		||||
from django.db import migrations
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('agents', '0034_agentcustomfield_checkbox_value'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.RenameField(
 | 
			
		||||
            model_name='agentcustomfield',
 | 
			
		||||
            old_name='checkbox_value',
 | 
			
		||||
            new_name='bool_value',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RenameField(
 | 
			
		||||
            model_name='agentcustomfield',
 | 
			
		||||
            old_name='value',
 | 
			
		||||
            new_name='string_value',
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -0,0 +1,18 @@
 | 
			
		||||
# Generated by Django 3.1.7 on 2021-04-17 01:28
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('agents', '0035_auto_20210329_1709'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='agent',
 | 
			
		||||
            name='block_policy_inheritance',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										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),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -4,7 +4,7 @@ import re
 | 
			
		||||
import time
 | 
			
		||||
from collections import Counter
 | 
			
		||||
from distutils.version import LooseVersion
 | 
			
		||||
from typing import Any, Union
 | 
			
		||||
from typing import Any
 | 
			
		||||
 | 
			
		||||
import msgpack
 | 
			
		||||
import validators
 | 
			
		||||
@@ -13,17 +13,15 @@ from Crypto.Hash import SHA3_384
 | 
			
		||||
from Crypto.Random import get_random_bytes
 | 
			
		||||
from Crypto.Util.Padding import pad
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Agent(BaseAuditModel):
 | 
			
		||||
@@ -63,6 +61,9 @@ class Agent(BaseAuditModel):
 | 
			
		||||
        max_length=255, choices=TZ_CHOICES, null=True, blank=True
 | 
			
		||||
    )
 | 
			
		||||
    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",
 | 
			
		||||
@@ -86,22 +87,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 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
 | 
			
		||||
@@ -118,7 +125,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):
 | 
			
		||||
@@ -160,13 +167,9 @@ 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 = 0, 0, 0
 | 
			
		||||
        total, passing, failing, warning, info = 0, 0, 0, 0, 0
 | 
			
		||||
 | 
			
		||||
        if self.agentchecks.exists():  # type: ignore
 | 
			
		||||
            for i in self.agentchecks.all():  # type: ignore
 | 
			
		||||
@@ -174,13 +177,20 @@ class Agent(BaseAuditModel):
 | 
			
		||||
                if i.status == "passing":
 | 
			
		||||
                    passing += 1
 | 
			
		||||
                elif i.status == "failing":
 | 
			
		||||
                    failing += 1
 | 
			
		||||
                    if i.alert_severity == "error":
 | 
			
		||||
                        failing += 1
 | 
			
		||||
                    elif i.alert_severity == "warning":
 | 
			
		||||
                        warning += 1
 | 
			
		||||
                    elif i.alert_severity == "info":
 | 
			
		||||
                        info += 1
 | 
			
		||||
 | 
			
		||||
        ret = {
 | 
			
		||||
            "total": total,
 | 
			
		||||
            "passing": passing,
 | 
			
		||||
            "failing": failing,
 | 
			
		||||
            "has_failing_checks": failing > 0,
 | 
			
		||||
            "warning": warning,
 | 
			
		||||
            "info": info,
 | 
			
		||||
            "has_failing_checks": failing > 0 or warning > 0,
 | 
			
		||||
        }
 | 
			
		||||
        return ret
 | 
			
		||||
 | 
			
		||||
@@ -195,6 +205,27 @@ class Agent(BaseAuditModel):
 | 
			
		||||
        except:
 | 
			
		||||
            return ["unknown cpu model"]
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def graphics(self):
 | 
			
		||||
        ret, mrda = [], []
 | 
			
		||||
        try:
 | 
			
		||||
            graphics = self.wmi_detail["graphics"]
 | 
			
		||||
            for i in graphics:
 | 
			
		||||
                caption = [x["Caption"] for x in i if "Caption" in x][0]
 | 
			
		||||
                if "microsoft remote display adapter" in caption.lower():
 | 
			
		||||
                    mrda.append("yes")
 | 
			
		||||
                    continue
 | 
			
		||||
 | 
			
		||||
                ret.append([x["Caption"] for x in i if "Caption" in x][0])
 | 
			
		||||
 | 
			
		||||
            # only return this if no other graphics cards
 | 
			
		||||
            if not ret and mrda:
 | 
			
		||||
                return "Microsoft Remote Display Adapter"
 | 
			
		||||
 | 
			
		||||
            return ", ".join(ret)
 | 
			
		||||
        except:
 | 
			
		||||
            return "Graphics info requires agent v1.4.14"
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def local_ips(self):
 | 
			
		||||
        ret = []
 | 
			
		||||
@@ -234,6 +265,11 @@ class Agent(BaseAuditModel):
 | 
			
		||||
                make = [x["Manufacturer"] for x in mobo if "Manufacturer" in x][0]
 | 
			
		||||
                model = [x["Product"] for x in mobo if "Product" in x][0]
 | 
			
		||||
 | 
			
		||||
            if make.lower() == "lenovo":
 | 
			
		||||
                sysfam = [x["SystemFamily"] for x in comp_sys if "SystemFamily" in x][0]
 | 
			
		||||
                if "to be filled" not in sysfam.lower():
 | 
			
		||||
                    model = sysfam
 | 
			
		||||
 | 
			
		||||
            return f"{make} {model}"
 | 
			
		||||
        except:
 | 
			
		||||
            pass
 | 
			
		||||
@@ -291,21 +327,28 @@ class Agent(BaseAuditModel):
 | 
			
		||||
        full: bool = False,
 | 
			
		||||
        wait: bool = False,
 | 
			
		||||
        run_on_any: bool = False,
 | 
			
		||||
        history_pk: int = 0,
 | 
			
		||||
    ) -> Any:
 | 
			
		||||
 | 
			
		||||
        from scripts.models import Script
 | 
			
		||||
 | 
			
		||||
        script = Script.objects.get(pk=scriptpk)
 | 
			
		||||
 | 
			
		||||
        parsed_args = script.parse_script_args(self, script.shell, args)
 | 
			
		||||
 | 
			
		||||
        data = {
 | 
			
		||||
            "func": "runscriptfull" if full else "runscript",
 | 
			
		||||
            "timeout": timeout,
 | 
			
		||||
            "script_args": args,
 | 
			
		||||
            "script_args": parsed_args,
 | 
			
		||||
            "payload": {
 | 
			
		||||
                "code": script.code,
 | 
			
		||||
                "shell": script.shell,
 | 
			
		||||
            },
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        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"}
 | 
			
		||||
@@ -319,7 +362,7 @@ class Agent(BaseAuditModel):
 | 
			
		||||
                online = [
 | 
			
		||||
                    agent
 | 
			
		||||
                    for agent in Agent.objects.only(
 | 
			
		||||
                        "pk", "last_seen", "overdue_time", "offline_time"
 | 
			
		||||
                        "pk", "agent_id", "last_seen", "overdue_time", "offline_time"
 | 
			
		||||
                    )
 | 
			
		||||
                    if agent.status == "online"
 | 
			
		||||
                ]
 | 
			
		||||
@@ -374,6 +417,13 @@ class Agent(BaseAuditModel):
 | 
			
		||||
            update.action = "approve"
 | 
			
		||||
            update.save(update_fields=["action"])
 | 
			
		||||
 | 
			
		||||
        if updates:
 | 
			
		||||
            DebugLog.info(
 | 
			
		||||
                agent=self,
 | 
			
		||||
                log_type="windows_updates",
 | 
			
		||||
                message=f"Approving windows updates on {self.hostname}",
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
    # returns agent policy merged with a client or site specific policy
 | 
			
		||||
    def get_patch_policy(self):
 | 
			
		||||
 | 
			
		||||
@@ -390,21 +440,34 @@ class Agent(BaseAuditModel):
 | 
			
		||||
 | 
			
		||||
            # check site policy if agent policy doesn't have one
 | 
			
		||||
            elif site.server_policy and site.server_policy.winupdatepolicy.exists():
 | 
			
		||||
                patch_policy = site.server_policy.winupdatepolicy.get()
 | 
			
		||||
                # make sure agent isn;t blocking policy inheritance
 | 
			
		||||
                if not self.block_policy_inheritance:
 | 
			
		||||
                    patch_policy = site.server_policy.winupdatepolicy.get()
 | 
			
		||||
 | 
			
		||||
            # if site doesn't have a patch policy check the client
 | 
			
		||||
            elif (
 | 
			
		||||
                site.client.server_policy
 | 
			
		||||
                and site.client.server_policy.winupdatepolicy.exists()
 | 
			
		||||
            ):
 | 
			
		||||
                patch_policy = site.client.server_policy.winupdatepolicy.get()
 | 
			
		||||
                # make sure agent and site are not blocking inheritance
 | 
			
		||||
                if (
 | 
			
		||||
                    not self.block_policy_inheritance
 | 
			
		||||
                    and not site.block_policy_inheritance
 | 
			
		||||
                ):
 | 
			
		||||
                    patch_policy = site.client.server_policy.winupdatepolicy.get()
 | 
			
		||||
 | 
			
		||||
            # 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
 | 
			
		||||
            ):
 | 
			
		||||
                patch_policy = core_settings.server_policy.winupdatepolicy.get()
 | 
			
		||||
                # make sure agent site and client are not blocking inheritance
 | 
			
		||||
                if (
 | 
			
		||||
                    not self.block_policy_inheritance
 | 
			
		||||
                    and not site.block_policy_inheritance
 | 
			
		||||
                    and not site.client.block_policy_inheritance
 | 
			
		||||
                ):
 | 
			
		||||
                    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
 | 
			
		||||
@@ -415,21 +478,36 @@ class Agent(BaseAuditModel):
 | 
			
		||||
                site.workstation_policy
 | 
			
		||||
                and site.workstation_policy.winupdatepolicy.exists()
 | 
			
		||||
            ):
 | 
			
		||||
                patch_policy = site.workstation_policy.winupdatepolicy.get()
 | 
			
		||||
                # make sure agent isn;t blocking policy inheritance
 | 
			
		||||
                if not self.block_policy_inheritance:
 | 
			
		||||
                    patch_policy = site.workstation_policy.winupdatepolicy.get()
 | 
			
		||||
 | 
			
		||||
            # if site doesn't have a patch policy check the client
 | 
			
		||||
            elif (
 | 
			
		||||
                site.client.workstation_policy
 | 
			
		||||
                and site.client.workstation_policy.winupdatepolicy.exists()
 | 
			
		||||
            ):
 | 
			
		||||
                patch_policy = site.client.workstation_policy.winupdatepolicy.get()
 | 
			
		||||
                # make sure agent and site are not blocking inheritance
 | 
			
		||||
                if (
 | 
			
		||||
                    not self.block_policy_inheritance
 | 
			
		||||
                    and not site.block_policy_inheritance
 | 
			
		||||
                ):
 | 
			
		||||
                    patch_policy = site.client.workstation_policy.winupdatepolicy.get()
 | 
			
		||||
 | 
			
		||||
            # 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
 | 
			
		||||
            ):
 | 
			
		||||
                patch_policy = core_settings.workstation_policy.winupdatepolicy.get()
 | 
			
		||||
                # make sure agent site and client are not blocking inheritance
 | 
			
		||||
                if (
 | 
			
		||||
                    not self.block_policy_inheritance
 | 
			
		||||
                    and not site.block_policy_inheritance
 | 
			
		||||
                    and not site.client.block_policy_inheritance
 | 
			
		||||
                ):
 | 
			
		||||
                    patch_policy = (
 | 
			
		||||
                        core_settings.workstation_policy.winupdatepolicy.get()  # type: ignore
 | 
			
		||||
                    )
 | 
			
		||||
 | 
			
		||||
        # if policy still doesn't exist return the agent patch policy
 | 
			
		||||
        if not patch_policy:
 | 
			
		||||
@@ -496,6 +574,7 @@ class Agent(BaseAuditModel):
 | 
			
		||||
            and site.server_policy
 | 
			
		||||
            and site.server_policy.alert_template
 | 
			
		||||
            and site.server_policy.alert_template.is_active
 | 
			
		||||
            and not self.block_policy_inheritance
 | 
			
		||||
        ):
 | 
			
		||||
            templates.append(site.server_policy.alert_template)
 | 
			
		||||
        if (
 | 
			
		||||
@@ -503,6 +582,7 @@ class Agent(BaseAuditModel):
 | 
			
		||||
            and site.workstation_policy
 | 
			
		||||
            and site.workstation_policy.alert_template
 | 
			
		||||
            and site.workstation_policy.alert_template.is_active
 | 
			
		||||
            and not self.block_policy_inheritance
 | 
			
		||||
        ):
 | 
			
		||||
            templates.append(site.workstation_policy.alert_template)
 | 
			
		||||
 | 
			
		||||
@@ -516,6 +596,8 @@ class Agent(BaseAuditModel):
 | 
			
		||||
            and client.server_policy
 | 
			
		||||
            and client.server_policy.alert_template
 | 
			
		||||
            and client.server_policy.alert_template.is_active
 | 
			
		||||
            and not self.block_policy_inheritance
 | 
			
		||||
            and not site.block_policy_inheritance
 | 
			
		||||
        ):
 | 
			
		||||
            templates.append(client.server_policy.alert_template)
 | 
			
		||||
        if (
 | 
			
		||||
@@ -523,32 +605,51 @@ class Agent(BaseAuditModel):
 | 
			
		||||
            and client.workstation_policy
 | 
			
		||||
            and client.workstation_policy.alert_template
 | 
			
		||||
            and client.workstation_policy.alert_template.is_active
 | 
			
		||||
            and not self.block_policy_inheritance
 | 
			
		||||
            and not site.block_policy_inheritance
 | 
			
		||||
        ):
 | 
			
		||||
            templates.append(client.workstation_policy.alert_template)
 | 
			
		||||
 | 
			
		||||
        # check if alert template is on client and return
 | 
			
		||||
        if client.alert_template and client.alert_template.is_active:
 | 
			
		||||
        if (
 | 
			
		||||
            client.alert_template
 | 
			
		||||
            and client.alert_template.is_active
 | 
			
		||||
            and not self.block_policy_inheritance
 | 
			
		||||
            and not site.block_policy_inheritance
 | 
			
		||||
        ):
 | 
			
		||||
            templates.append(client.alert_template)
 | 
			
		||||
 | 
			
		||||
        # check if alert template is applied globally and return
 | 
			
		||||
        if core.alert_template and core.alert_template.is_active:
 | 
			
		||||
            templates.append(core.alert_template)
 | 
			
		||||
        if (
 | 
			
		||||
            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)  # 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:
 | 
			
		||||
@@ -648,7 +749,11 @@ class Agent(BaseAuditModel):
 | 
			
		||||
            except ErrTimeout:
 | 
			
		||||
                ret = "timeout"
 | 
			
		||||
            else:
 | 
			
		||||
                ret = msgpack.loads(msg.data)  # type: ignore
 | 
			
		||||
                try:
 | 
			
		||||
                    ret = msgpack.loads(msg.data)  # type: ignore
 | 
			
		||||
                except Exception as e:
 | 
			
		||||
                    DebugLog.error(agent=self, log_type="agent_issues", message=e)
 | 
			
		||||
                    ret = str(e)
 | 
			
		||||
 | 
			
		||||
            await nc.close()
 | 
			
		||||
            return ret
 | 
			
		||||
@@ -660,12 +765,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:
 | 
			
		||||
@@ -680,7 +782,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)
 | 
			
		||||
@@ -696,36 +798,6 @@ class Agent(BaseAuditModel):
 | 
			
		||||
        except:
 | 
			
		||||
            pass
 | 
			
		||||
 | 
			
		||||
    # define how the agent should handle pending actions
 | 
			
		||||
    def handle_pending_actions(self):
 | 
			
		||||
        pending_actions = self.pendingactions.filter(status="pending")  # type: ignore
 | 
			
		||||
 | 
			
		||||
        for action in pending_actions:
 | 
			
		||||
            if action.action_type == "taskaction":
 | 
			
		||||
                from autotasks.tasks import (
 | 
			
		||||
                    create_win_task_schedule,
 | 
			
		||||
                    delete_win_task_schedule,
 | 
			
		||||
                    enable_or_disable_win_task,
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
                task_id = action.details["task_id"]
 | 
			
		||||
 | 
			
		||||
                if action.details["action"] == "taskcreate":
 | 
			
		||||
                    create_win_task_schedule.delay(task_id, pending_action=action.id)
 | 
			
		||||
                elif action.details["action"] == "tasktoggle":
 | 
			
		||||
                    enable_or_disable_win_task.delay(
 | 
			
		||||
                        task_id, action.details["value"], pending_action=action.id
 | 
			
		||||
                    )
 | 
			
		||||
                elif action.details["action"] == "taskdelete":
 | 
			
		||||
                    delete_win_task_schedule.delay(task_id, pending_action=action.id)
 | 
			
		||||
 | 
			
		||||
    # for clearing duplicate pending actions on agent
 | 
			
		||||
    def remove_matching_pending_task_actions(self, task_id):
 | 
			
		||||
        # remove any other pending actions on agent with same task_id
 | 
			
		||||
        for action in self.pendingactions.filter(action_type="taskaction").exclude(status="completed"):  # type: ignore
 | 
			
		||||
            if action.details["task_id"] == task_id:
 | 
			
		||||
                action.delete()
 | 
			
		||||
 | 
			
		||||
    def should_create_alert(self, alert_template=None):
 | 
			
		||||
        return (
 | 
			
		||||
            self.overdue_dashboard_alert
 | 
			
		||||
@@ -745,7 +817,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}, "
 | 
			
		||||
@@ -760,7 +832,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}, "
 | 
			
		||||
@@ -775,7 +847,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,
 | 
			
		||||
        )
 | 
			
		||||
@@ -784,7 +856,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,
 | 
			
		||||
        )
 | 
			
		||||
@@ -812,12 +884,6 @@ class RecoveryAction(models.Model):
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return f"{self.agent.hostname} - {self.mode}"
 | 
			
		||||
 | 
			
		||||
    def send(self):
 | 
			
		||||
        ret = {"recovery": self.mode}
 | 
			
		||||
        if self.mode == "command":
 | 
			
		||||
            ret["cmd"] = self.command
 | 
			
		||||
        return ret
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Note(models.Model):
 | 
			
		||||
    agent = models.ForeignKey(
 | 
			
		||||
@@ -837,3 +903,92 @@ class Note(models.Model):
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return self.agent.hostname
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AgentCustomField(models.Model):
 | 
			
		||||
    agent = models.ForeignKey(
 | 
			
		||||
        Agent,
 | 
			
		||||
        related_name="custom_fields",
 | 
			
		||||
        on_delete=models.CASCADE,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    field = models.ForeignKey(
 | 
			
		||||
        "core.CustomField",
 | 
			
		||||
        related_name="agent_fields",
 | 
			
		||||
        on_delete=models.CASCADE,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    string_value = models.TextField(null=True, blank=True)
 | 
			
		||||
    bool_value = models.BooleanField(blank=True, default=False)
 | 
			
		||||
    multiple_value = ArrayField(
 | 
			
		||||
        models.TextField(null=True, blank=True),
 | 
			
		||||
        null=True,
 | 
			
		||||
        blank=True,
 | 
			
		||||
        default=list,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return self.field
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def value(self):
 | 
			
		||||
        if self.field.type == "multiple":
 | 
			
		||||
            return self.multiple_value
 | 
			
		||||
        elif self.field.type == "checkbox":
 | 
			
		||||
            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):
 | 
			
		||||
    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=50, 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}"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										63
									
								
								api/tacticalrmm/agents/permissions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								api/tacticalrmm/agents/permissions.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,63 @@
 | 
			
		||||
from rest_framework import permissions
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.permissions import _has_perm
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UpdateAgentPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        return _has_perm(r, "can_update_agents")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class EditAgentPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        return _has_perm(r, "can_edit_agent")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ManageProcPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        return _has_perm(r, "can_manage_procs")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class EvtLogPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        return _has_perm(r, "can_view_eventlogs")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SendCMDPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        return _has_perm(r, "can_send_cmd")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RebootAgentPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        return _has_perm(r, "can_reboot_agents")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class InstallAgentPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        return _has_perm(r, "can_install_agents")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RunScriptPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        return _has_perm(r, "can_run_scripts")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ManageNotesPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        return _has_perm(r, "can_manage_notes")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RunBulkPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        return _has_perm(r, "can_run_bulk")
 | 
			
		||||
@@ -1,21 +1,21 @@
 | 
			
		||||
import pytz
 | 
			
		||||
from rest_framework import serializers
 | 
			
		||||
 | 
			
		||||
from clients.serializers import ClientSerializer
 | 
			
		||||
from rest_framework import serializers
 | 
			
		||||
from tacticalrmm.utils import get_default_timezone
 | 
			
		||||
from winupdate.serializers import WinUpdatePolicySerializer
 | 
			
		||||
 | 
			
		||||
from .models import Agent, Note
 | 
			
		||||
from .models import Agent, AgentCustomField, Note, AgentHistory
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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()
 | 
			
		||||
    local_ips = serializers.ReadOnlyField()
 | 
			
		||||
    make_model = serializers.ReadOnlyField()
 | 
			
		||||
    physical_disks = serializers.ReadOnlyField()
 | 
			
		||||
    graphics = serializers.ReadOnlyField()
 | 
			
		||||
    checks = serializers.ReadOnlyField()
 | 
			
		||||
    timezone = serializers.ReadOnlyField()
 | 
			
		||||
    all_timezones = serializers.SerializerMethodField()
 | 
			
		||||
@@ -44,8 +44,6 @@ class AgentOverdueActionSerializer(serializers.ModelSerializer):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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()
 | 
			
		||||
@@ -68,9 +66,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)
 | 
			
		||||
@@ -102,8 +97,8 @@ class AgentTableSerializer(serializers.ModelSerializer):
 | 
			
		||||
            "monitoring_type",
 | 
			
		||||
            "description",
 | 
			
		||||
            "needs_reboot",
 | 
			
		||||
            "patches_pending",
 | 
			
		||||
            "pending_actions",
 | 
			
		||||
            "has_patches_pending",
 | 
			
		||||
            "pending_actions_count",
 | 
			
		||||
            "status",
 | 
			
		||||
            "overdue_text_alert",
 | 
			
		||||
            "overdue_email_alert",
 | 
			
		||||
@@ -115,14 +110,35 @@ class AgentTableSerializer(serializers.ModelSerializer):
 | 
			
		||||
            "logged_username",
 | 
			
		||||
            "italic",
 | 
			
		||||
            "policy",
 | 
			
		||||
            "block_policy_inheritance",
 | 
			
		||||
        ]
 | 
			
		||||
        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
 | 
			
		||||
@@ -143,18 +159,15 @@ class AgentEditSerializer(serializers.ModelSerializer):
 | 
			
		||||
            "offline_time",
 | 
			
		||||
            "overdue_text_alert",
 | 
			
		||||
            "overdue_email_alert",
 | 
			
		||||
            "overdue_dashboard_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__"
 | 
			
		||||
@@ -188,3 +201,22 @@ class NotesSerializer(serializers.ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Agent
 | 
			
		||||
        fields = ["hostname", "pk", "notes"]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
        exclude = ["disks", "services", "wmi_detail"]
 | 
			
		||||
 
 | 
			
		||||
@@ -1,68 +1,67 @@
 | 
			
		||||
import asyncio
 | 
			
		||||
import datetime as dt
 | 
			
		||||
import random
 | 
			
		||||
import urllib.parse
 | 
			
		||||
from time import sleep
 | 
			
		||||
from typing import Union
 | 
			
		||||
 | 
			
		||||
from alerts.models import Alert
 | 
			
		||||
from core.models import CodeSignToken, CoreSettings
 | 
			
		||||
from 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 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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def agent_update(pk: int) -> str:
 | 
			
		||||
def agent_update(pk: int, codesigntoken: str = None, force: bool = False) -> str:
 | 
			
		||||
    from agents.utils import get_exegen_url
 | 
			
		||||
 | 
			
		||||
    agent = Agent.objects.get(pk=pk)
 | 
			
		||||
 | 
			
		||||
    if pyver.parse(agent.version) <= pyver.parse("1.1.11"):
 | 
			
		||||
        logger.warning(
 | 
			
		||||
            f"{agent.hostname} v{agent.version} is running an unsupported version. Refusing to auto update."
 | 
			
		||||
        )
 | 
			
		||||
    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.pk}). Skipping agent update.",
 | 
			
		||||
        )
 | 
			
		||||
        return "noarch"
 | 
			
		||||
 | 
			
		||||
    # removed sqlite in 1.4.0 to get rid of cgo dependency
 | 
			
		||||
    # 1.3.0 has migration func to move from sqlite to win registry, so force an upgrade to 1.3.0 if old agent
 | 
			
		||||
    if pyver.parse(agent.version) >= pyver.parse("1.3.0"):
 | 
			
		||||
        version = settings.LATEST_AGENT_VER
 | 
			
		||||
        url = agent.winagent_dl
 | 
			
		||||
        inno = agent.win_inno_exe
 | 
			
		||||
    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:
 | 
			
		||||
        version = "1.3.0"
 | 
			
		||||
        inno = (
 | 
			
		||||
            "winagent-v1.3.0.exe" if agent.arch == "64" else "winagent-v1.3.0-x86.exe"
 | 
			
		||||
        )
 | 
			
		||||
        url = f"https://github.com/wh1te909/rmmagent/releases/download/v1.3.0/{inno}"
 | 
			
		||||
        url = agent.winagent_dl
 | 
			
		||||
 | 
			
		||||
    if agent.pendingactions.filter(
 | 
			
		||||
        action_type="agentupdate", status="pending"
 | 
			
		||||
    ).exists():
 | 
			
		||||
        agent.pendingactions.filter(
 | 
			
		||||
    if not force:
 | 
			
		||||
        if agent.pendingactions.filter(
 | 
			
		||||
            action_type="agentupdate", status="pending"
 | 
			
		||||
        ).delete()
 | 
			
		||||
        ).exists():
 | 
			
		||||
            agent.pendingactions.filter(
 | 
			
		||||
                action_type="agentupdate", status="pending"
 | 
			
		||||
            ).delete()
 | 
			
		||||
 | 
			
		||||
    PendingAction.objects.create(
 | 
			
		||||
        agent=agent,
 | 
			
		||||
        action_type="agentupdate",
 | 
			
		||||
        details={
 | 
			
		||||
            "url": url,
 | 
			
		||||
            "version": version,
 | 
			
		||||
            "inno": inno,
 | 
			
		||||
        },
 | 
			
		||||
    )
 | 
			
		||||
        PendingAction.objects.create(
 | 
			
		||||
            agent=agent,
 | 
			
		||||
            action_type="agentupdate",
 | 
			
		||||
            details={
 | 
			
		||||
                "url": url,
 | 
			
		||||
                "version": version,
 | 
			
		||||
                "inno": inno,
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    nats_data = {
 | 
			
		||||
        "func": "agentupdate",
 | 
			
		||||
@@ -76,12 +75,32 @@ def agent_update(pk: int) -> str:
 | 
			
		||||
    return "created"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
def force_code_sign(pks: list[int]) -> None:
 | 
			
		||||
    try:
 | 
			
		||||
        token = CodeSignToken.objects.first().token  # type:ignore
 | 
			
		||||
    except:
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    chunks = (pks[i : i + 50] for i in range(0, len(pks), 50))
 | 
			
		||||
    for chunk in chunks:
 | 
			
		||||
        for pk in chunk:
 | 
			
		||||
            agent_update(pk=pk, codesigntoken=token, force=True)
 | 
			
		||||
            sleep(0.05)
 | 
			
		||||
        sleep(4)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
def send_agent_update_task(pks: list[int]) -> None:
 | 
			
		||||
    try:
 | 
			
		||||
        codesigntoken = CodeSignToken.objects.first().token  # type:ignore
 | 
			
		||||
    except:
 | 
			
		||||
        codesigntoken = None
 | 
			
		||||
 | 
			
		||||
    chunks = (pks[i : i + 30] for i in range(0, len(pks), 30))
 | 
			
		||||
    for chunk in chunks:
 | 
			
		||||
        for pk in chunk:
 | 
			
		||||
            agent_update(pk)
 | 
			
		||||
            agent_update(pk, codesigntoken)
 | 
			
		||||
            sleep(0.05)
 | 
			
		||||
        sleep(4)
 | 
			
		||||
 | 
			
		||||
@@ -89,9 +108,14 @@ 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  # type:ignore
 | 
			
		||||
    except:
 | 
			
		||||
        codesigntoken = None
 | 
			
		||||
 | 
			
		||||
    q = Agent.objects.only("pk", "version")
 | 
			
		||||
    pks: list[int] = [
 | 
			
		||||
        i.pk
 | 
			
		||||
@@ -102,7 +126,7 @@ def auto_self_agent_update_task() -> None:
 | 
			
		||||
    chunks = (pks[i : i + 30] for i in range(0, len(pks), 30))
 | 
			
		||||
    for chunk in chunks:
 | 
			
		||||
        for pk in chunk:
 | 
			
		||||
            agent_update(pk)
 | 
			
		||||
            agent_update(pk, codesigntoken)
 | 
			
		||||
            sleep(0.05)
 | 
			
		||||
        sleep(4)
 | 
			
		||||
 | 
			
		||||
@@ -187,6 +211,7 @@ def agent_outages_task() -> None:
 | 
			
		||||
 | 
			
		||||
    agents = Agent.objects.only(
 | 
			
		||||
        "pk",
 | 
			
		||||
        "agent_id",
 | 
			
		||||
        "last_seen",
 | 
			
		||||
        "offline_time",
 | 
			
		||||
        "overdue_time",
 | 
			
		||||
@@ -207,14 +232,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()
 | 
			
		||||
@@ -230,25 +265,129 @@ def run_script_email_results_task(
 | 
			
		||||
 | 
			
		||||
    msg = EmailMessage()
 | 
			
		||||
    msg["Subject"] = subject
 | 
			
		||||
    msg["From"] = CORE.smtp_from_email
 | 
			
		||||
    msg["From"] = CORE.smtp_from_email  # type:ignore
 | 
			
		||||
 | 
			
		||||
    if emails:
 | 
			
		||||
        msg["To"] = ", ".join(emails)
 | 
			
		||||
    else:
 | 
			
		||||
        msg["To"] = ", ".join(CORE.email_alert_recipients)
 | 
			
		||||
        msg["To"] = ", ".join(CORE.email_alert_recipients)  # type:ignore
 | 
			
		||||
 | 
			
		||||
    msg.set_content(body)
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        with smtplib.SMTP(CORE.smtp_host, CORE.smtp_port, timeout=20) as server:
 | 
			
		||||
            if CORE.smtp_requires_auth:
 | 
			
		||||
        with smtplib.SMTP(
 | 
			
		||||
            CORE.smtp_host, CORE.smtp_port, timeout=20  # type:ignore
 | 
			
		||||
        ) as server:  # type:ignore
 | 
			
		||||
            if CORE.smtp_requires_auth:  # type:ignore
 | 
			
		||||
                server.ehlo()
 | 
			
		||||
                server.starttls()
 | 
			
		||||
                server.login(CORE.smtp_host_user, CORE.smtp_host_password)
 | 
			
		||||
                server.login(
 | 
			
		||||
                    CORE.smtp_host_user, CORE.smtp_host_password  # type:ignore
 | 
			
		||||
                )  # type:ignore
 | 
			
		||||
                server.send_message(msg)
 | 
			
		||||
                server.quit()
 | 
			
		||||
            else:
 | 
			
		||||
                server.send_message(msg)
 | 
			
		||||
                server.quit()
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        logger.error(e)
 | 
			
		||||
        DebugLog.error(message=e)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
def clear_faults_task(older_than_days: int) -> None:
 | 
			
		||||
    # https://github.com/wh1te909/tacticalrmm/issues/484
 | 
			
		||||
    agents = Agent.objects.exclude(last_seen__isnull=True).filter(
 | 
			
		||||
        last_seen__lt=djangotime.now() - djangotime.timedelta(days=older_than_days)
 | 
			
		||||
    )
 | 
			
		||||
    for agent in agents:
 | 
			
		||||
        if agent.agentchecks.exists():
 | 
			
		||||
            for check in agent.agentchecks.all():
 | 
			
		||||
                # reset check status
 | 
			
		||||
                check.status = "passing"
 | 
			
		||||
                check.save(update_fields=["status"])
 | 
			
		||||
                if check.alert.filter(resolved=False).exists():
 | 
			
		||||
                    check.alert.get(resolved=False).resolve()
 | 
			
		||||
 | 
			
		||||
        # reset overdue alerts
 | 
			
		||||
        agent.overdue_email_alert = False
 | 
			
		||||
        agent.overdue_text_alert = False
 | 
			
		||||
        agent.overdue_dashboard_alert = False
 | 
			
		||||
        agent.save(
 | 
			
		||||
            update_fields=[
 | 
			
		||||
                "overdue_email_alert",
 | 
			
		||||
                "overdue_text_alert",
 | 
			
		||||
                "overdue_dashboard_alert",
 | 
			
		||||
            ]
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
def get_wmi_task() -> None:
 | 
			
		||||
    agents = Agent.objects.only(
 | 
			
		||||
        "pk", "agent_id", "last_seen", "overdue_time", "offline_time"
 | 
			
		||||
    )
 | 
			
		||||
    ids = [i.agent_id for i in agents if i.status == "online"]
 | 
			
		||||
    run_nats_api_cmd("wmi", ids, 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
 | 
			
		||||
 
 | 
			
		||||
@@ -1,19 +1,19 @@
 | 
			
		||||
import json
 | 
			
		||||
import os
 | 
			
		||||
from itertools import cycle
 | 
			
		||||
import pytz
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from unittest.mock import patch
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from logs.models import PendingAction
 | 
			
		||||
from model_bakery import baker
 | 
			
		||||
from packaging import version as pyver
 | 
			
		||||
 | 
			
		||||
from logs.models import PendingAction
 | 
			
		||||
from tacticalrmm.test import TacticalTestCase
 | 
			
		||||
from winupdate.models import WinUpdatePolicy
 | 
			
		||||
from winupdate.serializers import WinUpdatePolicySerializer
 | 
			
		||||
 | 
			
		||||
from .models import Agent
 | 
			
		||||
from .serializers import AgentSerializer
 | 
			
		||||
from .models import Agent, AgentCustomField, AgentHistory
 | 
			
		||||
from .serializers import AgentHistorySerializer, AgentSerializer
 | 
			
		||||
from .tasks import auto_self_agent_update_task
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -152,8 +152,9 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("post", url)
 | 
			
		||||
 | 
			
		||||
    @patch("time.sleep")
 | 
			
		||||
    @patch("agents.models.Agent.nats_cmd")
 | 
			
		||||
    def test_ping(self, nats_cmd):
 | 
			
		||||
    def test_ping(self, nats_cmd, mock_sleep):
 | 
			
		||||
        url = f"/agents/{self.agent.pk}/ping/"
 | 
			
		||||
 | 
			
		||||
        nats_cmd.return_value = "timeout"
 | 
			
		||||
@@ -305,7 +306,7 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
            "shell": "cmd",
 | 
			
		||||
            "timeout": 30,
 | 
			
		||||
        }
 | 
			
		||||
        mock_ret.return_value = "nt authority\system"
 | 
			
		||||
        mock_ret.return_value = "nt authority\\system"
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertIsInstance(r.data, str)  # type: ignore
 | 
			
		||||
@@ -363,9 +364,8 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
        self.check_not_authenticated("patch", url)
 | 
			
		||||
 | 
			
		||||
    @patch("os.path.exists")
 | 
			
		||||
    @patch("subprocess.run")
 | 
			
		||||
    def test_install_agent(self, mock_subprocess, mock_file_exists):
 | 
			
		||||
        url = f"/agents/installagent/"
 | 
			
		||||
    def test_install_agent(self, mock_file_exists):
 | 
			
		||||
        url = "/agents/installagent/"
 | 
			
		||||
 | 
			
		||||
        site = baker.make("clients.Site")
 | 
			
		||||
        data = {
 | 
			
		||||
@@ -373,38 +373,29 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
            "site": site.id,  # type: ignore
 | 
			
		||||
            "arch": "64",
 | 
			
		||||
            "expires": 23,
 | 
			
		||||
            "installMethod": "exe",
 | 
			
		||||
            "installMethod": "manual",
 | 
			
		||||
            "api": "https://api.example.com",
 | 
			
		||||
            "agenttype": "server",
 | 
			
		||||
            "rdp": 1,
 | 
			
		||||
            "ping": 0,
 | 
			
		||||
            "power": 0,
 | 
			
		||||
            "fileName": "rmm-client-site-server.exe",
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        mock_file_exists.return_value = False
 | 
			
		||||
        mock_subprocess.return_value.returncode = 0
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 406)
 | 
			
		||||
 | 
			
		||||
        mock_file_exists.return_value = True
 | 
			
		||||
        mock_subprocess.return_value.returncode = 1
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 413)
 | 
			
		||||
 | 
			
		||||
        mock_file_exists.return_value = True
 | 
			
		||||
        mock_subprocess.return_value.returncode = 0
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        data["arch"] = "32"
 | 
			
		||||
        mock_subprocess.return_value.returncode = 0
 | 
			
		||||
        mock_file_exists.return_value = False
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 415)
 | 
			
		||||
 | 
			
		||||
        data["installMethod"] = "manual"
 | 
			
		||||
        data["arch"] = "64"
 | 
			
		||||
        mock_subprocess.return_value.returncode = 0
 | 
			
		||||
        mock_file_exists.return_value = True
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertIn("rdp", r.json()["cmd"])
 | 
			
		||||
@@ -415,6 +406,9 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
        self.assertIn("power", r.json()["cmd"])
 | 
			
		||||
        self.assertIn("ping", r.json()["cmd"])
 | 
			
		||||
 | 
			
		||||
        data["installMethod"] = "powershell"
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("post", url)
 | 
			
		||||
 | 
			
		||||
    @patch("agents.models.Agent.nats_cmd")
 | 
			
		||||
@@ -443,7 +437,7 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(RecoveryAction.objects.count(), 1)
 | 
			
		||||
        mesh_recovery = RecoveryAction.objects.first()
 | 
			
		||||
        self.assertEqual(mesh_recovery.mode, "mesh")
 | 
			
		||||
        self.assertEqual(mesh_recovery.mode, "mesh")  # type: ignore
 | 
			
		||||
        nats_cmd.reset_mock()
 | 
			
		||||
        RecoveryAction.objects.all().delete()
 | 
			
		||||
 | 
			
		||||
@@ -478,8 +472,8 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(RecoveryAction.objects.count(), 1)
 | 
			
		||||
        cmd_recovery = RecoveryAction.objects.first()
 | 
			
		||||
        self.assertEqual(cmd_recovery.mode, "command")
 | 
			
		||||
        self.assertEqual(cmd_recovery.command, "shutdown /r /t 10 /f")
 | 
			
		||||
        self.assertEqual(cmd_recovery.mode, "command")  # type: ignore
 | 
			
		||||
        self.assertEqual(cmd_recovery.command, "shutdown /r /t 10 /f")  # type: ignore
 | 
			
		||||
 | 
			
		||||
    def test_agents_agent_detail(self):
 | 
			
		||||
        url = f"/agents/{self.agent.pk}/agentdetail/"
 | 
			
		||||
@@ -534,6 +528,35 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
        data = WinUpdatePolicySerializer(policy).data
 | 
			
		||||
        self.assertEqual(data["run_time_days"], [2, 3, 6])
 | 
			
		||||
 | 
			
		||||
        # test adding custom fields
 | 
			
		||||
        field = baker.make("core.CustomField", model="agent", type="number")
 | 
			
		||||
        edit = {
 | 
			
		||||
            "id": self.agent.pk,
 | 
			
		||||
            "site": site.id,  # type: ignore
 | 
			
		||||
            "description": "asjdk234andasd",
 | 
			
		||||
            "custom_fields": [{"field": field.id, "string_value": "123"}],  # type: ignore
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        r = self.client.patch(url, edit, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertTrue(
 | 
			
		||||
            AgentCustomField.objects.filter(agent=self.agent, field=field).exists()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # test edit custom field
 | 
			
		||||
        edit = {
 | 
			
		||||
            "id": self.agent.pk,
 | 
			
		||||
            "site": site.id,  # type: ignore
 | 
			
		||||
            "description": "asjdk234andasd",
 | 
			
		||||
            "custom_fields": [{"field": field.id, "string_value": "456"}],  # type: ignore
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        r = self.client.patch(url, edit, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            AgentCustomField.objects.get(agent=agent, field=field).value,
 | 
			
		||||
            "456",
 | 
			
		||||
        )
 | 
			
		||||
        self.check_not_authenticated("patch", url)
 | 
			
		||||
 | 
			
		||||
    @patch("agents.models.Agent.get_login_token")
 | 
			
		||||
@@ -731,7 +754,7 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertIn(self.agent.hostname, r.data)  # type: ignore
 | 
			
		||||
        nats_cmd.assert_called_with(
 | 
			
		||||
            {"func": "recover", "payload": {"mode": "mesh"}}, timeout=45
 | 
			
		||||
            {"func": "recover", "payload": {"mode": "mesh"}}, timeout=90
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        nats_cmd.return_value = "timeout"
 | 
			
		||||
@@ -747,6 +770,9 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
    @patch("agents.tasks.run_script_email_results_task.delay")
 | 
			
		||||
    @patch("agents.models.Agent.run_script")
 | 
			
		||||
    def test_run_script(self, run_script, email_task):
 | 
			
		||||
        from .models import AgentCustomField, Note
 | 
			
		||||
        from clients.models import ClientCustomField, SiteCustomField
 | 
			
		||||
 | 
			
		||||
        run_script.return_value = "ok"
 | 
			
		||||
        url = "/agents/runscript/"
 | 
			
		||||
        script = baker.make_recipe("scripts.script")
 | 
			
		||||
@@ -754,7 +780,7 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
        # test wait
 | 
			
		||||
        data = {
 | 
			
		||||
            "pk": self.agent.pk,
 | 
			
		||||
            "scriptPK": script.pk,
 | 
			
		||||
            "script": script.pk,
 | 
			
		||||
            "output": "wait",
 | 
			
		||||
            "args": [],
 | 
			
		||||
            "timeout": 15,
 | 
			
		||||
@@ -763,18 +789,18 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        run_script.assert_called_with(
 | 
			
		||||
            scriptpk=script.pk, args=[], timeout=18, wait=True
 | 
			
		||||
            scriptpk=script.pk, args=[], timeout=18, wait=True, history_pk=0
 | 
			
		||||
        )
 | 
			
		||||
        run_script.reset_mock()
 | 
			
		||||
 | 
			
		||||
        # test email default
 | 
			
		||||
        data = {
 | 
			
		||||
            "pk": self.agent.pk,
 | 
			
		||||
            "scriptPK": script.pk,
 | 
			
		||||
            "script": script.pk,
 | 
			
		||||
            "output": "email",
 | 
			
		||||
            "args": ["abc", "123"],
 | 
			
		||||
            "timeout": 15,
 | 
			
		||||
            "emailmode": "default",
 | 
			
		||||
            "emailMode": "default",
 | 
			
		||||
            "emails": ["admin@example.com", "bob@example.com"],
 | 
			
		||||
        }
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
@@ -789,7 +815,7 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
        email_task.reset_mock()
 | 
			
		||||
 | 
			
		||||
        # test email overrides
 | 
			
		||||
        data["emailmode"] = "custom"
 | 
			
		||||
        data["emailMode"] = "custom"
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        email_task.assert_called_with(
 | 
			
		||||
@@ -803,7 +829,7 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
        # test fire and forget
 | 
			
		||||
        data = {
 | 
			
		||||
            "pk": self.agent.pk,
 | 
			
		||||
            "scriptPK": script.pk,
 | 
			
		||||
            "script": script.pk,
 | 
			
		||||
            "output": "forget",
 | 
			
		||||
            "args": ["hello", "world"],
 | 
			
		||||
            "timeout": 22,
 | 
			
		||||
@@ -812,8 +838,139 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        run_script.assert_called_with(
 | 
			
		||||
            scriptpk=script.pk, args=["hello", "world"], timeout=25
 | 
			
		||||
            scriptpk=script.pk, args=["hello", "world"], timeout=25, history_pk=0
 | 
			
		||||
        )
 | 
			
		||||
        run_script.reset_mock()
 | 
			
		||||
 | 
			
		||||
        # test collector
 | 
			
		||||
 | 
			
		||||
        # save to agent custom field
 | 
			
		||||
        custom_field = baker.make("core.CustomField", model="agent")
 | 
			
		||||
        data = {
 | 
			
		||||
            "pk": self.agent.pk,
 | 
			
		||||
            "script": script.pk,
 | 
			
		||||
            "output": "collector",
 | 
			
		||||
            "args": ["hello", "world"],
 | 
			
		||||
            "timeout": 22,
 | 
			
		||||
            "custom_field": custom_field.id,  # type: ignore
 | 
			
		||||
            "save_all_output": True,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        run_script.assert_called_with(
 | 
			
		||||
            scriptpk=script.pk,
 | 
			
		||||
            args=["hello", "world"],
 | 
			
		||||
            timeout=25,
 | 
			
		||||
            wait=True,
 | 
			
		||||
            history_pk=0,
 | 
			
		||||
        )
 | 
			
		||||
        run_script.reset_mock()
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            AgentCustomField.objects.get(agent=self.agent.pk, field=custom_field).value,
 | 
			
		||||
            "ok",
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # save to site custom field
 | 
			
		||||
        custom_field = baker.make("core.CustomField", model="site")
 | 
			
		||||
        data = {
 | 
			
		||||
            "pk": self.agent.pk,
 | 
			
		||||
            "script": script.pk,
 | 
			
		||||
            "output": "collector",
 | 
			
		||||
            "args": ["hello", "world"],
 | 
			
		||||
            "timeout": 22,
 | 
			
		||||
            "custom_field": custom_field.id,  # type: ignore
 | 
			
		||||
            "save_all_output": False,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        run_script.assert_called_with(
 | 
			
		||||
            scriptpk=script.pk,
 | 
			
		||||
            args=["hello", "world"],
 | 
			
		||||
            timeout=25,
 | 
			
		||||
            wait=True,
 | 
			
		||||
            history_pk=0,
 | 
			
		||||
        )
 | 
			
		||||
        run_script.reset_mock()
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            SiteCustomField.objects.get(
 | 
			
		||||
                site=self.agent.site.pk, field=custom_field
 | 
			
		||||
            ).value,
 | 
			
		||||
            "ok",
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # save to client custom field
 | 
			
		||||
        custom_field = baker.make("core.CustomField", model="client")
 | 
			
		||||
        data = {
 | 
			
		||||
            "pk": self.agent.pk,
 | 
			
		||||
            "script": script.pk,
 | 
			
		||||
            "output": "collector",
 | 
			
		||||
            "args": ["hello", "world"],
 | 
			
		||||
            "timeout": 22,
 | 
			
		||||
            "custom_field": custom_field.id,  # type: ignore
 | 
			
		||||
            "save_all_output": False,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        run_script.assert_called_with(
 | 
			
		||||
            scriptpk=script.pk,
 | 
			
		||||
            args=["hello", "world"],
 | 
			
		||||
            timeout=25,
 | 
			
		||||
            wait=True,
 | 
			
		||||
            history_pk=0,
 | 
			
		||||
        )
 | 
			
		||||
        run_script.reset_mock()
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            ClientCustomField.objects.get(
 | 
			
		||||
                client=self.agent.client.pk, field=custom_field
 | 
			
		||||
            ).value,
 | 
			
		||||
            "ok",
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # test save to note
 | 
			
		||||
        data = {
 | 
			
		||||
            "pk": self.agent.pk,
 | 
			
		||||
            "script": script.pk,
 | 
			
		||||
            "output": "note",
 | 
			
		||||
            "args": ["hello", "world"],
 | 
			
		||||
            "timeout": 22,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        run_script.assert_called_with(
 | 
			
		||||
            scriptpk=script.pk,
 | 
			
		||||
            args=["hello", "world"],
 | 
			
		||||
            timeout=25,
 | 
			
		||||
            wait=True,
 | 
			
		||||
            history_pk=0,
 | 
			
		||||
        )
 | 
			
		||||
        run_script.reset_mock()
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(Note.objects.get(agent=self.agent).note, "ok")
 | 
			
		||||
 | 
			
		||||
    def test_get_agent_history(self):
 | 
			
		||||
 | 
			
		||||
        # setup data
 | 
			
		||||
        agent = baker.make_recipe("agents.agent")
 | 
			
		||||
        history = baker.make("agents.AgentHistory", agent=agent, _quantity=30)
 | 
			
		||||
        url = f"/agents/history/{agent.id}/"
 | 
			
		||||
 | 
			
		||||
        # test agent not found
 | 
			
		||||
        r = self.client.get("/agents/history/500/", format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 404)
 | 
			
		||||
 | 
			
		||||
        # test pulling data
 | 
			
		||||
        r = self.client.get(url, format="json")
 | 
			
		||||
        ctx = {"default_tz": pytz.timezone("America/Los_Angeles")}
 | 
			
		||||
        data = AgentHistorySerializer(history, many=True, context=ctx).data
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(r.data, data)  # type:ignore
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestAgentViewsNew(TacticalTestCase):
 | 
			
		||||
@@ -821,7 +978,7 @@ class TestAgentViewsNew(TacticalTestCase):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
        self.setup_coresettings()
 | 
			
		||||
 | 
			
		||||
    def test_agent_counts(self):
 | 
			
		||||
    """ def test_agent_counts(self):
 | 
			
		||||
        url = "/agents/agent_counts/"
 | 
			
		||||
 | 
			
		||||
        # create some data
 | 
			
		||||
@@ -848,7 +1005,7 @@ class TestAgentViewsNew(TacticalTestCase):
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(r.data, data)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("post", url)
 | 
			
		||||
        self.check_not_authenticated("post", url) """
 | 
			
		||||
 | 
			
		||||
    def test_agent_maintenance_mode(self):
 | 
			
		||||
        url = "/agents/maintenance/"
 | 
			
		||||
@@ -892,8 +1049,9 @@ class TestAgentTasks(TacticalTestCase):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
        self.setup_coresettings()
 | 
			
		||||
 | 
			
		||||
    @patch("agents.utils.get_exegen_url")
 | 
			
		||||
    @patch("agents.models.Agent.nats_cmd")
 | 
			
		||||
    def test_agent_update(self, nats_cmd):
 | 
			
		||||
    def test_agent_update(self, nats_cmd, get_exe):
 | 
			
		||||
        from agents.tasks import agent_update
 | 
			
		||||
 | 
			
		||||
        agent_noarch = baker.make_recipe(
 | 
			
		||||
@@ -904,63 +1062,96 @@ class TestAgentTasks(TacticalTestCase):
 | 
			
		||||
        r = agent_update(agent_noarch.pk)
 | 
			
		||||
        self.assertEqual(r, "noarch")
 | 
			
		||||
 | 
			
		||||
        agent_1111 = baker.make_recipe(
 | 
			
		||||
            "agents.agent",
 | 
			
		||||
            operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
 | 
			
		||||
            version="1.1.11",
 | 
			
		||||
        )
 | 
			
		||||
        r = agent_update(agent_1111.pk)
 | 
			
		||||
        self.assertEqual(r, "not supported")
 | 
			
		||||
 | 
			
		||||
        agent64_1112 = baker.make_recipe(
 | 
			
		||||
            "agents.agent",
 | 
			
		||||
            operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
 | 
			
		||||
            version="1.1.12",
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        r = agent_update(agent64_1112.pk)
 | 
			
		||||
        self.assertEqual(r, "created")
 | 
			
		||||
        action = PendingAction.objects.get(agent__pk=agent64_1112.pk)
 | 
			
		||||
        self.assertEqual(action.action_type, "agentupdate")
 | 
			
		||||
        self.assertEqual(action.status, "pending")
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            action.details["url"],
 | 
			
		||||
            "https://github.com/wh1te909/rmmagent/releases/download/v1.3.0/winagent-v1.3.0.exe",
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(action.details["inno"], "winagent-v1.3.0.exe")
 | 
			
		||||
        self.assertEqual(action.details["version"], "1.3.0")
 | 
			
		||||
        nats_cmd.assert_called_with(
 | 
			
		||||
            {
 | 
			
		||||
                "func": "agentupdate",
 | 
			
		||||
                "payload": {
 | 
			
		||||
                    "url": "https://github.com/wh1te909/rmmagent/releases/download/v1.3.0/winagent-v1.3.0.exe",
 | 
			
		||||
                    "version": "1.3.0",
 | 
			
		||||
                    "inno": "winagent-v1.3.0.exe",
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
            wait=False,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        agent_64_130 = baker.make_recipe(
 | 
			
		||||
        agent_130 = baker.make_recipe(
 | 
			
		||||
            "agents.agent",
 | 
			
		||||
            operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
 | 
			
		||||
            version="1.3.0",
 | 
			
		||||
        )
 | 
			
		||||
        nats_cmd.return_value = "ok"
 | 
			
		||||
        r = agent_update(agent_64_130.pk)
 | 
			
		||||
        r = agent_update(agent_130.pk)
 | 
			
		||||
        self.assertEqual(r, "not supported")
 | 
			
		||||
 | 
			
		||||
        # test __without__ code signing
 | 
			
		||||
        agent64_nosign = baker.make_recipe(
 | 
			
		||||
            "agents.agent",
 | 
			
		||||
            operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
 | 
			
		||||
            version="1.4.14",
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        r = agent_update(agent64_nosign.pk, None)
 | 
			
		||||
        self.assertEqual(r, "created")
 | 
			
		||||
        action = PendingAction.objects.get(agent__pk=agent64_nosign.pk)
 | 
			
		||||
        self.assertEqual(action.action_type, "agentupdate")
 | 
			
		||||
        self.assertEqual(action.status, "pending")
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            action.details["url"],
 | 
			
		||||
            f"https://github.com/wh1te909/rmmagent/releases/download/v{settings.LATEST_AGENT_VER}/winagent-v{settings.LATEST_AGENT_VER}.exe",
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            action.details["inno"], f"winagent-v{settings.LATEST_AGENT_VER}.exe"
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(action.details["version"], settings.LATEST_AGENT_VER)
 | 
			
		||||
        nats_cmd.assert_called_with(
 | 
			
		||||
            {
 | 
			
		||||
                "func": "agentupdate",
 | 
			
		||||
                "payload": {
 | 
			
		||||
                    "url": settings.DL_64,
 | 
			
		||||
                    "url": f"https://github.com/wh1te909/rmmagent/releases/download/v{settings.LATEST_AGENT_VER}/winagent-v{settings.LATEST_AGENT_VER}.exe",
 | 
			
		||||
                    "version": settings.LATEST_AGENT_VER,
 | 
			
		||||
                    "inno": f"winagent-v{settings.LATEST_AGENT_VER}.exe",
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
            wait=False,
 | 
			
		||||
        )
 | 
			
		||||
        action = PendingAction.objects.get(agent__pk=agent_64_130.pk)
 | 
			
		||||
 | 
			
		||||
        # test __with__ code signing (64 bit)
 | 
			
		||||
        codesign = baker.make("core.CodeSignToken", token="testtoken123")
 | 
			
		||||
        agent64_sign = baker.make_recipe(
 | 
			
		||||
            "agents.agent",
 | 
			
		||||
            operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
 | 
			
		||||
            version="1.4.14",
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        nats_cmd.return_value = "ok"
 | 
			
		||||
        get_exe.return_value = "https://exe.tacticalrmm.io"
 | 
			
		||||
        r = agent_update(agent64_sign.pk, codesign.token)  # type: ignore
 | 
			
		||||
        self.assertEqual(r, "created")
 | 
			
		||||
        nats_cmd.assert_called_with(
 | 
			
		||||
            {
 | 
			
		||||
                "func": "agentupdate",
 | 
			
		||||
                "payload": {
 | 
			
		||||
                    "url": f"https://exe.tacticalrmm.io/api/v1/winagents/?version={settings.LATEST_AGENT_VER}&arch=64&token=testtoken123",  # type: ignore
 | 
			
		||||
                    "version": settings.LATEST_AGENT_VER,
 | 
			
		||||
                    "inno": f"winagent-v{settings.LATEST_AGENT_VER}.exe",
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
            wait=False,
 | 
			
		||||
        )
 | 
			
		||||
        action = PendingAction.objects.get(agent__pk=agent64_sign.pk)
 | 
			
		||||
        self.assertEqual(action.action_type, "agentupdate")
 | 
			
		||||
        self.assertEqual(action.status, "pending")
 | 
			
		||||
 | 
			
		||||
        # test __with__ code signing (32 bit)
 | 
			
		||||
        agent32_sign = baker.make_recipe(
 | 
			
		||||
            "agents.agent",
 | 
			
		||||
            operating_system="Windows 10 Pro, 32 bit (build 19041.450)",
 | 
			
		||||
            version="1.4.14",
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        nats_cmd.return_value = "ok"
 | 
			
		||||
        get_exe.return_value = "https://exe.tacticalrmm.io"
 | 
			
		||||
        r = agent_update(agent32_sign.pk, codesign.token)  # type: ignore
 | 
			
		||||
        self.assertEqual(r, "created")
 | 
			
		||||
        nats_cmd.assert_called_with(
 | 
			
		||||
            {
 | 
			
		||||
                "func": "agentupdate",
 | 
			
		||||
                "payload": {
 | 
			
		||||
                    "url": f"https://exe.tacticalrmm.io/api/v1/winagents/?version={settings.LATEST_AGENT_VER}&arch=32&token=testtoken123",  # type: ignore
 | 
			
		||||
                    "version": settings.LATEST_AGENT_VER,
 | 
			
		||||
                    "inno": f"winagent-v{settings.LATEST_AGENT_VER}-x86.exe",
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
            wait=False,
 | 
			
		||||
        )
 | 
			
		||||
        action = PendingAction.objects.get(agent__pk=agent32_sign.pk)
 | 
			
		||||
        self.assertEqual(action.action_type, "agentupdate")
 | 
			
		||||
        self.assertEqual(action.status, "pending")
 | 
			
		||||
 | 
			
		||||
@@ -991,3 +1182,25 @@ class TestAgentTasks(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        r = auto_self_agent_update_task.s().apply()
 | 
			
		||||
        self.assertEqual(agent_update.call_count, 33)
 | 
			
		||||
 | 
			
		||||
    def test_agent_history_prune_task(self):
 | 
			
		||||
        from .tasks import prune_agent_history
 | 
			
		||||
 | 
			
		||||
        # setup data
 | 
			
		||||
        agent = baker.make_recipe("agents.agent")
 | 
			
		||||
        history = baker.make(
 | 
			
		||||
            "agents.AgentHistory",
 | 
			
		||||
            agent=agent,
 | 
			
		||||
            _quantity=50,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        days = 0
 | 
			
		||||
        for item in history:  # type: ignore
 | 
			
		||||
            item.time = djangotime.now() - djangotime.timedelta(days=days)
 | 
			
		||||
            item.save()
 | 
			
		||||
            days = days + 5
 | 
			
		||||
 | 
			
		||||
        # delete AgentHistory older than 30 days
 | 
			
		||||
        prune_agent_history(30)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(AgentHistory.objects.filter(agent=agent).count(), 6)
 | 
			
		||||
 
 | 
			
		||||
@@ -27,7 +27,7 @@ urlpatterns = [
 | 
			
		||||
    path("<int:pk>/notes/", views.GetAddNotes.as_view()),
 | 
			
		||||
    path("<int:pk>/note/", views.GetEditDeleteNote.as_view()),
 | 
			
		||||
    path("bulk/", views.bulk),
 | 
			
		||||
    path("agent_counts/", views.agent_counts),
 | 
			
		||||
    path("maintenance/", views.agent_maintenance),
 | 
			
		||||
    path("<int:pk>/wmi/", views.WMI.as_view()),
 | 
			
		||||
    path("history/<int:pk>/", views.AgentHistoryView.as_view()),
 | 
			
		||||
]
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										37
									
								
								api/tacticalrmm/agents/utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								api/tacticalrmm/agents/utils.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,37 @@
 | 
			
		||||
import random
 | 
			
		||||
import urllib.parse
 | 
			
		||||
 | 
			
		||||
import requests
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_exegen_url() -> str:
 | 
			
		||||
    urls: list[str] = settings.EXE_GEN_URLS
 | 
			
		||||
    for url in urls:
 | 
			
		||||
        try:
 | 
			
		||||
            r = requests.get(url, timeout=10)
 | 
			
		||||
        except:
 | 
			
		||||
            continue
 | 
			
		||||
 | 
			
		||||
        if r.status_code == 200:
 | 
			
		||||
            return url
 | 
			
		||||
 | 
			
		||||
    return random.choice(urls)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_winagent_url(arch: str) -> str:
 | 
			
		||||
    from core.models import CodeSignToken
 | 
			
		||||
 | 
			
		||||
    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)
 | 
			
		||||
    except:
 | 
			
		||||
        dl_url = settings.DL_64 if arch == "64" else settings.DL_32
 | 
			
		||||
 | 
			
		||||
    return dl_url
 | 
			
		||||
@@ -3,33 +3,45 @@ import datetime as dt
 | 
			
		||||
import os
 | 
			
		||||
import random
 | 
			
		||||
import string
 | 
			
		||||
import time
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.http import HttpResponse
 | 
			
		||||
from django.shortcuts import get_object_or_404
 | 
			
		||||
from loguru import logger
 | 
			
		||||
from packaging import version as pyver
 | 
			
		||||
from rest_framework import status
 | 
			
		||||
from rest_framework.decorators import api_view
 | 
			
		||||
from rest_framework.decorators import api_view, permission_classes
 | 
			
		||||
from rest_framework.permissions import IsAuthenticated
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
from rest_framework.views import APIView
 | 
			
		||||
 | 
			
		||||
from 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 (
 | 
			
		||||
    generate_installer_exe,
 | 
			
		||||
    get_default_timezone,
 | 
			
		||||
    notify_error,
 | 
			
		||||
    reload_nats,
 | 
			
		||||
)
 | 
			
		||||
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 .models import Agent, Note, RecoveryAction
 | 
			
		||||
from .models import Agent, AgentCustomField, Note, RecoveryAction, AgentHistory
 | 
			
		||||
from .permissions import (
 | 
			
		||||
    EditAgentPerms,
 | 
			
		||||
    EvtLogPerms,
 | 
			
		||||
    InstallAgentPerms,
 | 
			
		||||
    ManageNotesPerms,
 | 
			
		||||
    ManageProcPerms,
 | 
			
		||||
    MeshPerms,
 | 
			
		||||
    RebootAgentPerms,
 | 
			
		||||
    RunBulkPerms,
 | 
			
		||||
    RunScriptPerms,
 | 
			
		||||
    SendCMDPerms,
 | 
			
		||||
    UninstallPerms,
 | 
			
		||||
    UpdateAgentPerms,
 | 
			
		||||
)
 | 
			
		||||
from .serializers import (
 | 
			
		||||
    AgentCustomFieldSerializer,
 | 
			
		||||
    AgentEditSerializer,
 | 
			
		||||
    AgentHistorySerializer,
 | 
			
		||||
    AgentHostnameSerializer,
 | 
			
		||||
    AgentOverdueActionSerializer,
 | 
			
		||||
    AgentSerializer,
 | 
			
		||||
@@ -39,12 +51,10 @@ from .serializers import (
 | 
			
		||||
)
 | 
			
		||||
from .tasks import run_script_email_results_task, send_agent_update_task
 | 
			
		||||
 | 
			
		||||
logger.configure(**settings.LOG_CONFIG)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view()
 | 
			
		||||
def get_agent_versions(request):
 | 
			
		||||
    agents = Agent.objects.only("pk")
 | 
			
		||||
    agents = Agent.objects.prefetch_related("site").only("pk", "hostname")
 | 
			
		||||
    return Response(
 | 
			
		||||
        {
 | 
			
		||||
            "versions": [settings.LATEST_AGENT_VER],
 | 
			
		||||
@@ -54,6 +64,7 @@ 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] = [
 | 
			
		||||
@@ -66,32 +77,43 @@ def update_agents(request):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view()
 | 
			
		||||
@permission_classes([IsAuthenticated, UninstallPerms])
 | 
			
		||||
def ping(request, pk):
 | 
			
		||||
    agent = get_object_or_404(Agent, pk=pk)
 | 
			
		||||
    status = "offline"
 | 
			
		||||
    r = asyncio.run(agent.nats_cmd({"func": "ping"}, timeout=5))
 | 
			
		||||
    if r == "pong":
 | 
			
		||||
        status = "online"
 | 
			
		||||
    attempts = 0
 | 
			
		||||
    while 1:
 | 
			
		||||
        r = asyncio.run(agent.nats_cmd({"func": "ping"}, timeout=2))
 | 
			
		||||
        if r == "pong":
 | 
			
		||||
            status = "online"
 | 
			
		||||
            break
 | 
			
		||||
        else:
 | 
			
		||||
            attempts += 1
 | 
			
		||||
            time.sleep(1)
 | 
			
		||||
 | 
			
		||||
        if attempts >= 5:
 | 
			
		||||
            break
 | 
			
		||||
 | 
			
		||||
    return Response({"name": agent.hostname, "status": status})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view(["DELETE"])
 | 
			
		||||
@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"])
 | 
			
		||||
@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 = AgentEditSerializer(instance=agent, data=request.data, partial=True)
 | 
			
		||||
    a_serializer.is_valid(raise_exception=True)
 | 
			
		||||
    a_serializer.save()
 | 
			
		||||
 | 
			
		||||
@@ -103,26 +125,54 @@ def edit_agent(request):
 | 
			
		||||
        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}"
 | 
			
		||||
        key=core.mesh_token, user=f"user//{core.mesh_username}"  # type:ignore
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    if token == "err":
 | 
			
		||||
        return notify_error("Invalid mesh token")
 | 
			
		||||
 | 
			
		||||
    control = f"{core.mesh_site}/?login={token}&gotonode={agent.mesh_node_id}&viewmode=11&hide=31"
 | 
			
		||||
    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"
 | 
			
		||||
    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, hostname=agent.hostname)
 | 
			
		||||
    AuditLog.audit_mesh_session(
 | 
			
		||||
        username=request.user.username,
 | 
			
		||||
        agent=agent,
 | 
			
		||||
        debug_info={"ip": request._client_ip},
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    ret = {
 | 
			
		||||
        "hostname": agent.hostname,
 | 
			
		||||
@@ -152,6 +202,7 @@ def get_processes(request, pk):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view()
 | 
			
		||||
@permission_classes([IsAuthenticated, ManageProcPerms])
 | 
			
		||||
def kill_proc(request, pk, pid):
 | 
			
		||||
    agent = get_object_or_404(Agent, pk=pk)
 | 
			
		||||
    r = asyncio.run(
 | 
			
		||||
@@ -167,6 +218,7 @@ def kill_proc(request, pk, pid):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view()
 | 
			
		||||
@permission_classes([IsAuthenticated, EvtLogPerms])
 | 
			
		||||
def get_event_log(request, pk, logtype, days):
 | 
			
		||||
    agent = get_object_or_404(Agent, pk=pk)
 | 
			
		||||
    timeout = 180 if logtype == "Security" else 30
 | 
			
		||||
@@ -186,6 +238,7 @@ 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"])
 | 
			
		||||
    timeout = int(request.data["timeout"])
 | 
			
		||||
@@ -197,6 +250,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":
 | 
			
		||||
@@ -204,9 +267,10 @@ 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)
 | 
			
		||||
@@ -251,6 +315,8 @@ class AgentsTableList(APIView):
 | 
			
		||||
            "last_logged_in_user",
 | 
			
		||||
            "time_zone",
 | 
			
		||||
            "maintenance_mode",
 | 
			
		||||
            "pending_actions_count",
 | 
			
		||||
            "has_patches_pending",
 | 
			
		||||
        )
 | 
			
		||||
        ctx = {"default_tz": get_default_timezone()}
 | 
			
		||||
        serializer = AgentTableSerializer(queryset, many=True, context=ctx)
 | 
			
		||||
@@ -281,6 +347,7 @@ def overdue_action(request):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Reboot(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, RebootAgentPerms]
 | 
			
		||||
    # reboot now
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
        agent = get_object_or_404(Agent, pk=request.data["pk"])
 | 
			
		||||
@@ -333,8 +400,12 @@ class Reboot(APIView):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view(["POST"])
 | 
			
		||||
@permission_classes([IsAuthenticated, InstallAgentPerms])
 | 
			
		||||
def install_agent(request):
 | 
			
		||||
    from knox.models import AuthToken
 | 
			
		||||
    from accounts.models import User
 | 
			
		||||
 | 
			
		||||
    from agents.utils import get_winagent_url
 | 
			
		||||
 | 
			
		||||
    client_id = request.data["client"]
 | 
			
		||||
    site_id = request.data["site"]
 | 
			
		||||
@@ -356,26 +427,28 @@ def install_agent(request):
 | 
			
		||||
    inno = (
 | 
			
		||||
        f"winagent-v{version}.exe" if arch == "64" else f"winagent-v{version}-x86.exe"
 | 
			
		||||
    )
 | 
			
		||||
    download_url = settings.DL_64 if arch == "64" else settings.DL_32
 | 
			
		||||
    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":
 | 
			
		||||
        return generate_installer_exe(
 | 
			
		||||
            file_name="rmm-installer.exe",
 | 
			
		||||
            goarch="amd64" if arch == "64" else "386",
 | 
			
		||||
            inno=inno,
 | 
			
		||||
            api=request.data["api"],
 | 
			
		||||
            client_id=client_id,
 | 
			
		||||
            site_id=site_id,
 | 
			
		||||
            atype=request.data["agenttype"],
 | 
			
		||||
        from tacticalrmm.utils import generate_winagent_exe
 | 
			
		||||
 | 
			
		||||
        return generate_winagent_exe(
 | 
			
		||||
            client=client_id,
 | 
			
		||||
            site=site_id,
 | 
			
		||||
            agent_type=request.data["agenttype"],
 | 
			
		||||
            rdp=request.data["rdp"],
 | 
			
		||||
            ping=request.data["ping"],
 | 
			
		||||
            power=request.data["power"],
 | 
			
		||||
            download_url=download_url,
 | 
			
		||||
            arch=arch,
 | 
			
		||||
            token=token,
 | 
			
		||||
            api=request.data["api"],
 | 
			
		||||
            file_name=request.data["fileName"],
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    elif request.data["installMethod"] == "manual":
 | 
			
		||||
@@ -448,7 +521,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)
 | 
			
		||||
@@ -503,28 +576,44 @@ 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"])
 | 
			
		||||
    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,
 | 
			
		||||
@@ -533,8 +622,51 @@ 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}")
 | 
			
		||||
 | 
			
		||||
@@ -543,7 +675,7 @@ def run_script(request):
 | 
			
		||||
def recover_mesh(request, pk):
 | 
			
		||||
    agent = get_object_or_404(Agent, pk=pk)
 | 
			
		||||
    data = {"func": "recover", "payload": {"mode": "mesh"}}
 | 
			
		||||
    r = asyncio.run(agent.nats_cmd(data, timeout=45))
 | 
			
		||||
    r = asyncio.run(agent.nats_cmd(data, timeout=90))
 | 
			
		||||
    if r != "ok":
 | 
			
		||||
        return notify_error("Unable to contact the agent")
 | 
			
		||||
 | 
			
		||||
@@ -585,6 +717,8 @@ class GetAddNotes(APIView):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetEditDeleteNote(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, ManageNotesPerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request, pk):
 | 
			
		||||
        note = get_object_or_404(Note, pk=pk)
 | 
			
		||||
        return Response(NoteSerializer(note).data)
 | 
			
		||||
@@ -603,8 +737,9 @@ 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":
 | 
			
		||||
@@ -612,7 +747,7 @@ def bulk(request):
 | 
			
		||||
    elif request.data["target"] == "site":
 | 
			
		||||
        q = Agent.objects.filter(site_id=request.data["site"])
 | 
			
		||||
    elif request.data["target"] == "agents":
 | 
			
		||||
        q = Agent.objects.filter(pk__in=request.data["agentPKs"])
 | 
			
		||||
        q = Agent.objects.filter(pk__in=request.data["agents"])
 | 
			
		||||
    elif request.data["target"] == "all":
 | 
			
		||||
        q = Agent.objects.only("pk", "monitoring_type")
 | 
			
		||||
    else:
 | 
			
		||||
@@ -625,76 +760,52 @@ 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"])
 | 
			
		||||
def agent_counts(request):
 | 
			
		||||
 | 
			
		||||
    server_offline_count = len(
 | 
			
		||||
        [
 | 
			
		||||
            agent
 | 
			
		||||
            for agent in Agent.objects.filter(monitoring_type="server").only(
 | 
			
		||||
                "pk",
 | 
			
		||||
                "last_seen",
 | 
			
		||||
                "overdue_time",
 | 
			
		||||
                "offline_time",
 | 
			
		||||
            )
 | 
			
		||||
            if not agent.status == "online"
 | 
			
		||||
        ]
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    workstation_offline_count = len(
 | 
			
		||||
        [
 | 
			
		||||
            agent
 | 
			
		||||
            for agent in Agent.objects.filter(monitoring_type="workstation").only(
 | 
			
		||||
                "pk",
 | 
			
		||||
                "last_seen",
 | 
			
		||||
                "overdue_time",
 | 
			
		||||
                "offline_time",
 | 
			
		||||
            )
 | 
			
		||||
            if not agent.status == "online"
 | 
			
		||||
        ]
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    return Response(
 | 
			
		||||
        {
 | 
			
		||||
            "total_server_count": Agent.objects.filter(
 | 
			
		||||
                monitoring_type="server"
 | 
			
		||||
            ).count(),
 | 
			
		||||
            "total_server_offline_count": server_offline_count,
 | 
			
		||||
            "total_workstation_count": Agent.objects.filter(
 | 
			
		||||
                monitoring_type="workstation"
 | 
			
		||||
            ).count(),
 | 
			
		||||
            "total_workstation_offline_count": workstation_offline_count,
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view(["POST"])
 | 
			
		||||
def agent_maintenance(request):
 | 
			
		||||
    if request.data["type"] == "Client":
 | 
			
		||||
@@ -725,3 +836,11 @@ class WMI(APIView):
 | 
			
		||||
        if r != "ok":
 | 
			
		||||
            return notify_error("Unable to contact the agent")
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AgentHistoryView(APIView):
 | 
			
		||||
    def get(self, request, pk):
 | 
			
		||||
        agent = get_object_or_404(Agent, pk=pk)
 | 
			
		||||
        history = AgentHistory.objects.filter(agent=agent)
 | 
			
		||||
        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),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -1,20 +1,20 @@
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
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"),
 | 
			
		||||
@@ -172,6 +172,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)
 | 
			
		||||
@@ -208,6 +209,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)
 | 
			
		||||
@@ -241,6 +243,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)
 | 
			
		||||
@@ -294,10 +297,10 @@ 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_template.action_args,
 | 
			
		||||
                args=alert.parse_script_args(alert_template.action_args),
 | 
			
		||||
                timeout=alert_template.action_timeout,
 | 
			
		||||
                wait=True,
 | 
			
		||||
                full=True,
 | 
			
		||||
@@ -313,8 +316,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
 | 
			
		||||
@@ -344,6 +349,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 (
 | 
			
		||||
@@ -362,6 +368,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 (
 | 
			
		||||
@@ -380,6 +387,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
 | 
			
		||||
@@ -402,11 +410,12 @@ 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(
 | 
			
		||||
                scriptpk=alert_template.resolved_action.pk,
 | 
			
		||||
                args=alert_template.resolved_action_args,
 | 
			
		||||
                args=alert.parse_script_args(alert_template.resolved_action_args),
 | 
			
		||||
                timeout=alert_template.resolved_action_timeout,
 | 
			
		||||
                wait=True,
 | 
			
		||||
                full=True,
 | 
			
		||||
@@ -424,12 +433,44 @@ 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]):
 | 
			
		||||
 | 
			
		||||
class AlertTemplate(models.Model):
 | 
			
		||||
        if not args:
 | 
			
		||||
            return []
 | 
			
		||||
 | 
			
		||||
        temp_args = list()
 | 
			
		||||
        # pattern to match for injection
 | 
			
		||||
        pattern = re.compile(".*\\{\\{alert\\.(.*)\\}\\}.*")
 | 
			
		||||
 | 
			
		||||
        for arg in args:
 | 
			
		||||
            match = pattern.match(arg)
 | 
			
		||||
            if match:
 | 
			
		||||
                name = match.group(1)
 | 
			
		||||
 | 
			
		||||
                if hasattr(self, name):
 | 
			
		||||
                    value = f"'{getattr(self, name)}'"
 | 
			
		||||
                else:
 | 
			
		||||
                    continue
 | 
			
		||||
 | 
			
		||||
                try:
 | 
			
		||||
                    temp_args.append(re.sub("\\{\\{.*\\}\\}", value, arg))  # type: ignore
 | 
			
		||||
                except Exception as e:
 | 
			
		||||
                    DebugLog.error(log_type="scripting", message=e)
 | 
			
		||||
                    continue
 | 
			
		||||
 | 
			
		||||
            else:
 | 
			
		||||
                temp_args.append(arg)
 | 
			
		||||
 | 
			
		||||
        return temp_args
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AlertTemplate(BaseAuditModel):
 | 
			
		||||
    name = models.CharField(max_length=100)
 | 
			
		||||
    is_active = models.BooleanField(default=True)
 | 
			
		||||
 | 
			
		||||
@@ -486,6 +527,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(
 | 
			
		||||
@@ -509,6 +551,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(
 | 
			
		||||
@@ -532,6 +575,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)
 | 
			
		||||
@@ -550,6 +594,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 (
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										11
									
								
								api/tacticalrmm/alerts/permissions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								api/tacticalrmm/alerts/permissions.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
			
		||||
from rest_framework import permissions
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.permissions import _has_perm
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ManageAlertsPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        if r.method == "GET" or r.method == "PATCH":
 | 
			
		||||
            return True
 | 
			
		||||
 | 
			
		||||
        return _has_perm(r, "can_manage_alerts")
 | 
			
		||||
@@ -119,3 +119,9 @@ class AlertTemplateRelationSerializer(ModelSerializer):
 | 
			
		||||
    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,13 @@
 | 
			
		||||
from datetime import datetime, timedelta
 | 
			
		||||
from unittest.mock import patch
 | 
			
		||||
 | 
			
		||||
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 (
 | 
			
		||||
@@ -330,8 +329,8 @@ 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
 | 
			
		||||
 | 
			
		||||
@@ -403,16 +402,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 +513,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 +727,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 +735,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 +1012,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 +1020,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 +1274,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 +1317,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 +1331,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 +1351,6 @@ class TestAlertTasks(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        nats_cmd.reset_mock()
 | 
			
		||||
 | 
			
		||||
        # Setup cmd mock
 | 
			
		||||
        success = {
 | 
			
		||||
            "retcode": 0,
 | 
			
		||||
            "stdout": "success!",
 | 
			
		||||
            "stderr": "",
 | 
			
		||||
            "execution_time": 5.0000,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        nats_cmd.side_effect = ["pong", success]
 | 
			
		||||
 | 
			
		||||
        # make sure script run results were stored
 | 
			
		||||
@@ -1387,3 +1390,47 @@ class TestAlertTasks(TacticalTestCase):
 | 
			
		||||
        self.assertEqual(alert.resolved_action_execution_time, "5.0000")
 | 
			
		||||
        self.assertEqual(alert.resolved_action_stdout, "success!")
 | 
			
		||||
        self.assertEqual(alert.resolved_action_stderr, "")
 | 
			
		||||
 | 
			
		||||
    def test_parse_script_args(self):
 | 
			
		||||
        alert = baker.make("alerts.Alert")
 | 
			
		||||
 | 
			
		||||
        args = ["-Parameter", "-Another {{alert.id}}"]
 | 
			
		||||
 | 
			
		||||
        # test default value
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            ["-Parameter", f"-Another '{alert.id}'"],  # type: ignore
 | 
			
		||||
            alert.parse_script_args(args=args),  # type: ignore
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_prune_resolved_alerts(self):
 | 
			
		||||
        from .tasks import prune_resolved_alerts
 | 
			
		||||
 | 
			
		||||
        # setup data
 | 
			
		||||
        resolved_alerts = baker.make(
 | 
			
		||||
            "alerts.Alert",
 | 
			
		||||
            resolved=True,
 | 
			
		||||
            _quantity=25,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        alerts = baker.make(
 | 
			
		||||
            "alerts.Alert",
 | 
			
		||||
            resolved=False,
 | 
			
		||||
            _quantity=25,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        days = 0
 | 
			
		||||
        for alert in resolved_alerts:  # type: ignore
 | 
			
		||||
            alert.alert_time = djangotime.now() - djangotime.timedelta(days=days)
 | 
			
		||||
            alert.save()
 | 
			
		||||
            days = days + 5
 | 
			
		||||
 | 
			
		||||
        days = 0
 | 
			
		||||
        for alert in alerts:  # type: ignore
 | 
			
		||||
            alert.alert_time = djangotime.now() - djangotime.timedelta(days=days)
 | 
			
		||||
            alert.save()
 | 
			
		||||
            days = days + 5
 | 
			
		||||
 | 
			
		||||
        # delete AgentHistory older than 30 days
 | 
			
		||||
        prune_resolved_alerts(30)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(Alert.objects.count(), 31)
 | 
			
		||||
 
 | 
			
		||||
@@ -3,12 +3,14 @@ from datetime import datetime as dt
 | 
			
		||||
from django.db.models import Q
 | 
			
		||||
from django.shortcuts import get_object_or_404
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from rest_framework.permissions import IsAuthenticated
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
from rest_framework.views import APIView
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.utils import notify_error
 | 
			
		||||
 | 
			
		||||
from .models import Alert, AlertTemplate
 | 
			
		||||
from .permissions import ManageAlertsPerms
 | 
			
		||||
from .serializers import (
 | 
			
		||||
    AlertSerializer,
 | 
			
		||||
    AlertTemplateRelationSerializer,
 | 
			
		||||
@@ -18,6 +20,8 @@ from .tasks import cache_agents_alert_template
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetAddAlerts(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, ManageAlertsPerms]
 | 
			
		||||
 | 
			
		||||
    def patch(self, request):
 | 
			
		||||
 | 
			
		||||
        # top 10 alerts for dashboard icon
 | 
			
		||||
@@ -109,6 +113,8 @@ class GetAddAlerts(APIView):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetUpdateDeleteAlert(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, ManageAlertsPerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request, pk):
 | 
			
		||||
        alert = get_object_or_404(Alert, pk=pk)
 | 
			
		||||
 | 
			
		||||
@@ -163,6 +169,8 @@ class GetUpdateDeleteAlert(APIView):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BulkAlerts(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, ManageAlertsPerms]
 | 
			
		||||
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
        if request.data["bulk_action"] == "resolve":
 | 
			
		||||
            Alert.objects.filter(id__in=request.data["alerts"]).update(
 | 
			
		||||
@@ -185,6 +193,8 @@ class BulkAlerts(APIView):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetAddAlertTemplates(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, ManageAlertsPerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request):
 | 
			
		||||
        alert_templates = AlertTemplate.objects.all()
 | 
			
		||||
 | 
			
		||||
@@ -202,6 +212,8 @@ class GetAddAlertTemplates(APIView):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetUpdateDeleteAlertTemplate(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, ManageAlertsPerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request, pk):
 | 
			
		||||
        alert_template = get_object_or_404(AlertTemplate, pk=pk)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@ from django.conf import settings
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from model_bakery import baker
 | 
			
		||||
 | 
			
		||||
from autotasks.models import AutomatedTask
 | 
			
		||||
from tacticalrmm.test import TacticalTestCase
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -203,3 +204,139 @@ class TestAPIv3(TacticalTestCase):
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(r.json(), {"mode": "rpc", "shellcmd": ""})
 | 
			
		||||
        reload_nats.assert_called_once()
 | 
			
		||||
 | 
			
		||||
    def test_task_runner_get(self):
 | 
			
		||||
        from autotasks.serializers import TaskGOGetSerializer
 | 
			
		||||
 | 
			
		||||
        r = self.client.get("/api/v3/500/asdf9df9dfdf/taskrunner/")
 | 
			
		||||
        self.assertEqual(r.status_code, 404)
 | 
			
		||||
 | 
			
		||||
        # setup data
 | 
			
		||||
        agent = baker.make_recipe("agents.agent")
 | 
			
		||||
        script = baker.make_recipe("scripts.script")
 | 
			
		||||
        task = baker.make("autotasks.AutomatedTask", agent=agent, script=script)
 | 
			
		||||
 | 
			
		||||
        url = f"/api/v3/{task.pk}/{agent.agent_id}/taskrunner/"  # type: ignore
 | 
			
		||||
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(TaskGOGetSerializer(task).data, r.data)  # type: ignore
 | 
			
		||||
 | 
			
		||||
    def test_task_runner_results(self):
 | 
			
		||||
        from agents.models import AgentCustomField
 | 
			
		||||
 | 
			
		||||
        r = self.client.patch("/api/v3/500/asdf9df9dfdf/taskrunner/")
 | 
			
		||||
        self.assertEqual(r.status_code, 404)
 | 
			
		||||
 | 
			
		||||
        # setup data
 | 
			
		||||
        agent = baker.make_recipe("agents.agent")
 | 
			
		||||
        task = baker.make("autotasks.AutomatedTask", agent=agent)
 | 
			
		||||
 | 
			
		||||
        url = f"/api/v3/{task.pk}/{agent.agent_id}/taskrunner/"  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # test passing task
 | 
			
		||||
        data = {
 | 
			
		||||
            "stdout": "test test \ntestest stdgsd\n",
 | 
			
		||||
            "stderr": "",
 | 
			
		||||
            "retcode": 0,
 | 
			
		||||
            "execution_time": 3.560,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        r = self.client.patch(url, data)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertTrue(AutomatedTask.objects.get(pk=task.pk).status == "passing")  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # test failing task
 | 
			
		||||
        data = {
 | 
			
		||||
            "stdout": "test test \ntestest stdgsd\n",
 | 
			
		||||
            "stderr": "",
 | 
			
		||||
            "retcode": 1,
 | 
			
		||||
            "execution_time": 3.560,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        r = self.client.patch(url, data)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertTrue(AutomatedTask.objects.get(pk=task.pk).status == "failing")  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # test collector task
 | 
			
		||||
        text = baker.make("core.CustomField", model="agent", type="text", name="Test")
 | 
			
		||||
        boolean = baker.make(
 | 
			
		||||
            "core.CustomField", model="agent", type="checkbox", name="Test1"
 | 
			
		||||
        )
 | 
			
		||||
        multiple = baker.make(
 | 
			
		||||
            "core.CustomField", model="agent", type="multiple", name="Test2"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # test text fields
 | 
			
		||||
        task.custom_field = text  # type: ignore
 | 
			
		||||
        task.save()  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # test failing failing with stderr
 | 
			
		||||
        data = {
 | 
			
		||||
            "stdout": "test test \nthe last line",
 | 
			
		||||
            "stderr": "This is an error",
 | 
			
		||||
            "retcode": 1,
 | 
			
		||||
            "execution_time": 3.560,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        r = self.client.patch(url, data)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertTrue(AutomatedTask.objects.get(pk=task.pk).status == "failing")  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # test saving to text field
 | 
			
		||||
        data = {
 | 
			
		||||
            "stdout": "test test \nthe last line",
 | 
			
		||||
            "stderr": "",
 | 
			
		||||
            "retcode": 0,
 | 
			
		||||
            "execution_time": 3.560,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        r = self.client.patch(url, data)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(AutomatedTask.objects.get(pk=task.pk).status, "passing")  # type: ignore
 | 
			
		||||
        self.assertEqual(AgentCustomField.objects.get(field=text, agent=task.agent).value, "the last line")  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # test saving to checkbox field
 | 
			
		||||
        task.custom_field = boolean  # type: ignore
 | 
			
		||||
        task.save()  # type: ignore
 | 
			
		||||
 | 
			
		||||
        data = {
 | 
			
		||||
            "stdout": "1",
 | 
			
		||||
            "stderr": "",
 | 
			
		||||
            "retcode": 0,
 | 
			
		||||
            "execution_time": 3.560,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        r = self.client.patch(url, data)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(AutomatedTask.objects.get(pk=task.pk).status, "passing")  # type: ignore
 | 
			
		||||
        self.assertTrue(AgentCustomField.objects.get(field=boolean, agent=task.agent).value)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # test saving to multiple field with commas
 | 
			
		||||
        task.custom_field = multiple  # type: ignore
 | 
			
		||||
        task.save()  # type: ignore
 | 
			
		||||
 | 
			
		||||
        data = {
 | 
			
		||||
            "stdout": "this,is,an,array",
 | 
			
		||||
            "stderr": "",
 | 
			
		||||
            "retcode": 0,
 | 
			
		||||
            "execution_time": 3.560,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        r = self.client.patch(url, data)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(AutomatedTask.objects.get(pk=task.pk).status, "passing")  # type: ignore
 | 
			
		||||
        self.assertEqual(AgentCustomField.objects.get(field=multiple, agent=task.agent).value, ["this", "is", "an", "array"])  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # test mutiple with a single value
 | 
			
		||||
        data = {
 | 
			
		||||
            "stdout": "this",
 | 
			
		||||
            "stderr": "",
 | 
			
		||||
            "retcode": 0,
 | 
			
		||||
            "execution_time": 3.560,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        r = self.client.patch(url, data)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(AutomatedTask.objects.get(pk=task.pk).status, "passing")  # type: ignore
 | 
			
		||||
        self.assertEqual(AgentCustomField.objects.get(field=multiple, agent=task.agent).value, ["this"])  # type: ignore
 | 
			
		||||
 
 | 
			
		||||
@@ -20,4 +20,5 @@ urlpatterns = [
 | 
			
		||||
    path("superseded/", views.SupersededWinUpdate.as_view()),
 | 
			
		||||
    path("<int:pk>/chocoresult/", views.ChocoResult.as_view()),
 | 
			
		||||
    path("<str:agentid>/recovery/", views.AgentRecovery.as_view()),
 | 
			
		||||
    path("<int:pk>/<str:agentid>/histresult/", views.AgentHistoryResult.as_view()),
 | 
			
		||||
]
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,6 @@ from django.conf import settings
 | 
			
		||||
from django.http import HttpResponse
 | 
			
		||||
from django.shortcuts import get_object_or_404
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from loguru import logger
 | 
			
		||||
from packaging import version as pyver
 | 
			
		||||
from rest_framework.authentication import TokenAuthentication
 | 
			
		||||
from rest_framework.authtoken.models import Token
 | 
			
		||||
@@ -15,20 +14,18 @@ from rest_framework.response import Response
 | 
			
		||||
from rest_framework.views import APIView
 | 
			
		||||
 | 
			
		||||
from accounts.models import User
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
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
 | 
			
		||||
@@ -65,9 +66,17 @@ class CheckIn(APIView):
 | 
			
		||||
        if Alert.objects.filter(agent=agent, resolved=False).exists():
 | 
			
		||||
            Alert.handle_alert_resolve(agent)
 | 
			
		||||
 | 
			
		||||
        # get any pending actions
 | 
			
		||||
        if agent.pendingactions.filter(status="pending").exists():  # type: ignore
 | 
			
		||||
            agent.handle_pending_actions()
 | 
			
		||||
        # sync scheduled tasks
 | 
			
		||||
        if agent.autotasks.exclude(sync_status="synced").exists():  # type: ignore
 | 
			
		||||
            tasks = agent.autotasks.exclude(sync_status="synced")  # type: ignore
 | 
			
		||||
 | 
			
		||||
            for task in tasks:
 | 
			
		||||
                if task.sync_status == "pendingdeletion":
 | 
			
		||||
                    task.delete_task_on_agent()
 | 
			
		||||
                elif task.sync_status == "initial":
 | 
			
		||||
                    task.modify_task_on_agent()
 | 
			
		||||
                elif task.sync_status == "notsynced":
 | 
			
		||||
                    task.create_task_on_agent()
 | 
			
		||||
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
@@ -174,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")
 | 
			
		||||
@@ -296,10 +309,11 @@ class CheckRunner(APIView):
 | 
			
		||||
                    < djangotime.now()
 | 
			
		||||
                    - djangotime.timedelta(seconds=check.run_interval)
 | 
			
		||||
                )
 | 
			
		||||
                # if check interval isn't set, make sure the agent's check interval has passed before running
 | 
			
		||||
            )
 | 
			
		||||
            # if check interval isn't set, make sure the agent's check interval has passed before running
 | 
			
		||||
            or (
 | 
			
		||||
                check.last_run
 | 
			
		||||
                not check.run_interval
 | 
			
		||||
                and check.last_run
 | 
			
		||||
                < djangotime.now() - djangotime.timedelta(seconds=agent.check_interval)
 | 
			
		||||
            )
 | 
			
		||||
        ]
 | 
			
		||||
@@ -312,11 +326,16 @@ class CheckRunner(APIView):
 | 
			
		||||
 | 
			
		||||
    def patch(self, request):
 | 
			
		||||
        check = get_object_or_404(Check, pk=request.data["id"])
 | 
			
		||||
        if pyver.parse(check.agent.version) < pyver.parse("1.5.7"):
 | 
			
		||||
            return notify_error("unsupported")
 | 
			
		||||
 | 
			
		||||
        check.last_run = djangotime.now()
 | 
			
		||||
        check.save(update_fields=["last_run"])
 | 
			
		||||
        status = check.handle_checkv2(request.data)
 | 
			
		||||
        status = check.handle_check(request.data)
 | 
			
		||||
        if status == "failing" and check.assignedtask.exists():  # type: ignore
 | 
			
		||||
            check.handle_assigned_task()
 | 
			
		||||
 | 
			
		||||
        return Response(status)
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CheckRunnerInterval(APIView):
 | 
			
		||||
@@ -336,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)
 | 
			
		||||
@@ -351,11 +369,20 @@ class TaskRunner(APIView):
 | 
			
		||||
            instance=task, data=request.data, partial=True
 | 
			
		||||
        )
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        serializer.save(last_run=djangotime.now())
 | 
			
		||||
        new_task = serializer.save(last_run=djangotime.now())
 | 
			
		||||
 | 
			
		||||
        status = "failing" if task.retcode != 0 else "passing"
 | 
			
		||||
        # check if task is a collector and update the custom field
 | 
			
		||||
        if task.custom_field:
 | 
			
		||||
            if not task.stderr:
 | 
			
		||||
 | 
			
		||||
                task.save_collector_results()
 | 
			
		||||
 | 
			
		||||
                status = "passing"
 | 
			
		||||
            else:
 | 
			
		||||
                status = "failing"
 | 
			
		||||
        else:
 | 
			
		||||
            status = "failing" if task.retcode != 0 else "passing"
 | 
			
		||||
 | 
			
		||||
        new_task: AutomatedTask = AutomatedTask.objects.get(pk=task.pk)
 | 
			
		||||
        new_task.status = status
 | 
			
		||||
        new_task.save()
 | 
			
		||||
 | 
			
		||||
@@ -365,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")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -393,7 +411,7 @@ class SysInfo(APIView):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class MeshExe(APIView):
 | 
			
		||||
    """ Sends the mesh exe to the installer """
 | 
			
		||||
    """Sends the mesh exe to the installer"""
 | 
			
		||||
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
        exe = "meshagent.exe" if request.data["arch"] == "64" else "meshagent-x86.exe"
 | 
			
		||||
@@ -464,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(
 | 
			
		||||
@@ -568,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")
 | 
			
		||||
 
 | 
			
		||||
@@ -29,17 +29,17 @@ class Policy(BaseAuditModel):
 | 
			
		||||
 | 
			
		||||
    def save(self, *args, **kwargs):
 | 
			
		||||
        from alerts.tasks import cache_agents_alert_template
 | 
			
		||||
        from automation.tasks import generate_agent_checks_from_policies_task
 | 
			
		||||
        from automation.tasks import generate_agent_checks_task
 | 
			
		||||
 | 
			
		||||
        # get old policy if exists
 | 
			
		||||
        old_policy = type(self).objects.get(pk=self.pk) if self.pk else None
 | 
			
		||||
        super(BaseAuditModel, self).save(*args, **kwargs)
 | 
			
		||||
        super(Policy, self).save(old_model=old_policy, *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
        # generate agent checks only if active and enforced were changed
 | 
			
		||||
        if old_policy:
 | 
			
		||||
            if old_policy.active != self.active or old_policy.enforced != self.enforced:
 | 
			
		||||
                generate_agent_checks_from_policies_task.delay(
 | 
			
		||||
                    policypk=self.pk,
 | 
			
		||||
                generate_agent_checks_task.delay(
 | 
			
		||||
                    policy=self.pk,
 | 
			
		||||
                    create_tasks=True,
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
@@ -50,9 +50,12 @@ 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, create_tasks=True)
 | 
			
		||||
        generate_agent_checks_task.delay(agents=agents, create_tasks=True)
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return self.name
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def is_default_server_policy(self):
 | 
			
		||||
@@ -62,9 +65,6 @@ class Policy(BaseAuditModel):
 | 
			
		||||
    def is_default_workstation_policy(self):
 | 
			
		||||
        return self.default_workstation_policy.exists()  # type: ignore
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return self.name
 | 
			
		||||
 | 
			
		||||
    def is_agent_excluded(self, agent):
 | 
			
		||||
        return (
 | 
			
		||||
            agent in self.excluded_agents.all()
 | 
			
		||||
@@ -94,20 +94,29 @@ class Policy(BaseAuditModel):
 | 
			
		||||
 | 
			
		||||
        filtered_agents_pks = Policy.objects.none()
 | 
			
		||||
 | 
			
		||||
        filtered_agents_pks |= Agent.objects.filter(
 | 
			
		||||
            site__in=[
 | 
			
		||||
                site
 | 
			
		||||
                for site in explicit_sites
 | 
			
		||||
                if site.client not in explicit_clients
 | 
			
		||||
                and site.client not in self.excluded_clients.all()
 | 
			
		||||
            ],
 | 
			
		||||
            monitoring_type=mon_type,
 | 
			
		||||
        ).values_list("pk", flat=True)
 | 
			
		||||
        filtered_agents_pks |= (
 | 
			
		||||
            Agent.objects.exclude(block_policy_inheritance=True)
 | 
			
		||||
            .filter(
 | 
			
		||||
                site__in=[
 | 
			
		||||
                    site
 | 
			
		||||
                    for site in explicit_sites
 | 
			
		||||
                    if site.client not in explicit_clients
 | 
			
		||||
                    and site.client not in self.excluded_clients.all()
 | 
			
		||||
                ],
 | 
			
		||||
                monitoring_type=mon_type,
 | 
			
		||||
            )
 | 
			
		||||
            .values_list("pk", flat=True)
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        filtered_agents_pks |= Agent.objects.filter(
 | 
			
		||||
            site__client__in=[client for client in explicit_clients],
 | 
			
		||||
            monitoring_type=mon_type,
 | 
			
		||||
        ).values_list("pk", flat=True)
 | 
			
		||||
        filtered_agents_pks |= (
 | 
			
		||||
            Agent.objects.exclude(block_policy_inheritance=True)
 | 
			
		||||
            .exclude(site__block_policy_inheritance=True)
 | 
			
		||||
            .filter(
 | 
			
		||||
                site__client__in=[client for client in explicit_clients],
 | 
			
		||||
                monitoring_type=mon_type,
 | 
			
		||||
            )
 | 
			
		||||
            .values_list("pk", flat=True)
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        return Agent.objects.filter(
 | 
			
		||||
            models.Q(pk__in=filtered_agents_pks)
 | 
			
		||||
@@ -117,15 +126,12 @@ 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):
 | 
			
		||||
        from autotasks.models import AutomatedTask
 | 
			
		||||
        from autotasks.tasks import delete_win_task_schedule
 | 
			
		||||
        from logs.models import PendingAction
 | 
			
		||||
 | 
			
		||||
        # List of all tasks to be applied
 | 
			
		||||
        tasks = list()
 | 
			
		||||
@@ -154,6 +160,17 @@ class Policy(BaseAuditModel):
 | 
			
		||||
            client_policy = client.workstation_policy
 | 
			
		||||
            site_policy = site.workstation_policy
 | 
			
		||||
 | 
			
		||||
        # check if client/site/agent is blocking inheritance and blank out policies
 | 
			
		||||
        if agent.block_policy_inheritance:
 | 
			
		||||
            site_policy = None
 | 
			
		||||
            client_policy = None
 | 
			
		||||
            default_policy = None
 | 
			
		||||
        elif site.block_policy_inheritance:
 | 
			
		||||
            client_policy = None
 | 
			
		||||
            default_policy = None
 | 
			
		||||
        elif client.block_policy_inheritance:
 | 
			
		||||
            default_policy = None
 | 
			
		||||
 | 
			
		||||
        if (
 | 
			
		||||
            agent_policy
 | 
			
		||||
            and agent_policy.active
 | 
			
		||||
@@ -200,26 +217,16 @@ class Policy(BaseAuditModel):
 | 
			
		||||
                if taskpk not in added_task_pks
 | 
			
		||||
            ]
 | 
			
		||||
        ):
 | 
			
		||||
            delete_win_task_schedule.delay(task.pk)
 | 
			
		||||
            if task.sync_status == "initial":
 | 
			
		||||
                task.delete()
 | 
			
		||||
            else:
 | 
			
		||||
                task.sync_status = "pendingdeletion"
 | 
			
		||||
                task.save()
 | 
			
		||||
 | 
			
		||||
        # handle matching tasks that haven't synced to agent yet or pending deletion due to agent being offline
 | 
			
		||||
        for action in agent.pendingactions.filter(action_type="taskaction").exclude(
 | 
			
		||||
            status="completed"
 | 
			
		||||
        ):
 | 
			
		||||
            task = AutomatedTask.objects.get(pk=action.details["task_id"])
 | 
			
		||||
            if (
 | 
			
		||||
                task.parent_task in agent_tasks_parent_pks
 | 
			
		||||
                and task.parent_task in added_task_pks
 | 
			
		||||
            ):
 | 
			
		||||
                agent.remove_matching_pending_task_actions(task.id)
 | 
			
		||||
 | 
			
		||||
                PendingAction(
 | 
			
		||||
                    agent=agent,
 | 
			
		||||
                    action_type="taskaction",
 | 
			
		||||
                    details={"action": "taskcreate", "task_id": task.id},
 | 
			
		||||
                ).save()
 | 
			
		||||
                task.sync_status = "notsynced"
 | 
			
		||||
                task.save(update_fields=["sync_status"])
 | 
			
		||||
        # change tasks from pendingdeletion to notsynced if policy was added or changed
 | 
			
		||||
        agent.autotasks.filter(sync_status="pendingdeletion").filter(
 | 
			
		||||
            parent_task__in=[taskpk for taskpk in added_task_pks]
 | 
			
		||||
        ).update(sync_status="notsynced")
 | 
			
		||||
 | 
			
		||||
        return [task for task in tasks if task.pk not in agent_tasks_parent_pks]
 | 
			
		||||
 | 
			
		||||
@@ -251,6 +258,17 @@ class Policy(BaseAuditModel):
 | 
			
		||||
            client_policy = client.workstation_policy
 | 
			
		||||
            site_policy = site.workstation_policy
 | 
			
		||||
 | 
			
		||||
        # check if client/site/agent is blocking inheritance and blank out policies
 | 
			
		||||
        if agent.block_policy_inheritance:
 | 
			
		||||
            site_policy = None
 | 
			
		||||
            client_policy = None
 | 
			
		||||
            default_policy = None
 | 
			
		||||
        elif site.block_policy_inheritance:
 | 
			
		||||
            client_policy = None
 | 
			
		||||
            default_policy = None
 | 
			
		||||
        elif client.block_policy_inheritance:
 | 
			
		||||
            default_policy = None
 | 
			
		||||
 | 
			
		||||
        # Used to hold the policies that will be applied and the order in which they are applied
 | 
			
		||||
        # Enforced policies are applied first
 | 
			
		||||
        enforced_checks = list()
 | 
			
		||||
@@ -412,11 +430,12 @@ class Policy(BaseAuditModel):
 | 
			
		||||
 | 
			
		||||
        # remove policy checks from agent that fell out of policy scope
 | 
			
		||||
        agent.agentchecks.filter(
 | 
			
		||||
            managed_by_policy=True,
 | 
			
		||||
            parent_check__in=[
 | 
			
		||||
                checkpk
 | 
			
		||||
                for checkpk in agent_checks_parent_pks
 | 
			
		||||
                if checkpk not in [check.pk for check in final_list]
 | 
			
		||||
            ]
 | 
			
		||||
            ],
 | 
			
		||||
        ).delete()
 | 
			
		||||
 | 
			
		||||
        return [
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										11
									
								
								api/tacticalrmm/automation/permissions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								api/tacticalrmm/automation/permissions.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
			
		||||
from rest_framework import permissions
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.permissions import _has_perm
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AutomationPolicyPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        if r.method == "GET":
 | 
			
		||||
            return True
 | 
			
		||||
 | 
			
		||||
        return _has_perm(r, "can_manage_automation_policies")
 | 
			
		||||
@@ -83,8 +83,15 @@ class PolicyCheckSerializer(ModelSerializer):
 | 
			
		||||
class AutoTasksFieldSerializer(ModelSerializer):
 | 
			
		||||
    assigned_check = PolicyCheckSerializer(read_only=True)
 | 
			
		||||
    script = ReadOnlyField(source="script.id")
 | 
			
		||||
    custom_field = ReadOnlyField(source="custom_field.id")
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = AutomatedTask
 | 
			
		||||
        fields = "__all__"
 | 
			
		||||
        depth = 1
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PolicyAuditSerializer(ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Policy
 | 
			
		||||
        fields = "__all__"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,169 +1,153 @@
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from automation.models import Policy
 | 
			
		||||
from autotasks.models import AutomatedTask
 | 
			
		||||
from checks.models import Check
 | 
			
		||||
from typing import Any, Dict, List, Union
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.celery import app
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
# generates policy checks on agents affected by a policy and optionally generate automated tasks
 | 
			
		||||
def generate_agent_checks_from_policies_task(policypk, create_tasks=False):
 | 
			
		||||
@app.task(retry_backoff=5, retry_jitter=True, retry_kwargs={"max_retries": 5})
 | 
			
		||||
def generate_agent_checks_task(
 | 
			
		||||
    policy: int = None,
 | 
			
		||||
    site: int = None,
 | 
			
		||||
    client: int = None,
 | 
			
		||||
    agents: List[int] = list(),
 | 
			
		||||
    all: bool = False,
 | 
			
		||||
    create_tasks: bool = False,
 | 
			
		||||
) -> Union[str, None]:
 | 
			
		||||
    from agents.models import Agent
 | 
			
		||||
    from automation.models import Policy
 | 
			
		||||
 | 
			
		||||
    policy = Policy.objects.get(pk=policypk)
 | 
			
		||||
    p = Policy.objects.get(pk=policy) if policy else None
 | 
			
		||||
 | 
			
		||||
    if policy.is_default_server_policy and policy.is_default_workstation_policy:
 | 
			
		||||
        agents = Agent.objects.prefetch_related("policy").only("pk", "monitoring_type")
 | 
			
		||||
    elif policy.is_default_server_policy:
 | 
			
		||||
        agents = Agent.objects.filter(monitoring_type="server").only(
 | 
			
		||||
            "pk", "monitoring_type"
 | 
			
		||||
        )
 | 
			
		||||
    elif policy.is_default_workstation_policy:
 | 
			
		||||
        agents = Agent.objects.filter(monitoring_type="workstation").only(
 | 
			
		||||
    # generate checks on all agents if all is specified or if policy is default server/workstation policy
 | 
			
		||||
    if (p and p.is_default_server_policy and p.is_default_workstation_policy) or all:
 | 
			
		||||
        a = Agent.objects.prefetch_related("policy").only("pk", "monitoring_type")
 | 
			
		||||
 | 
			
		||||
    # generate checks on all servers if policy is a default servers policy
 | 
			
		||||
    elif p and p.is_default_server_policy:
 | 
			
		||||
        a = Agent.objects.filter(monitoring_type="server").only("pk", "monitoring_type")
 | 
			
		||||
 | 
			
		||||
    # generate checks on all workstations if policy is a default workstations policy
 | 
			
		||||
    elif p and p.is_default_workstation_policy:
 | 
			
		||||
        a = Agent.objects.filter(monitoring_type="workstation").only(
 | 
			
		||||
            "pk", "monitoring_type"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    # generate checks on a list of supplied agents
 | 
			
		||||
    elif agents:
 | 
			
		||||
        a = Agent.objects.filter(pk__in=agents)
 | 
			
		||||
 | 
			
		||||
    # generate checks on agents affected by supplied policy
 | 
			
		||||
    elif policy:
 | 
			
		||||
        a = p.related_agents().only("pk")
 | 
			
		||||
 | 
			
		||||
    # generate checks that has specified site
 | 
			
		||||
    elif site:
 | 
			
		||||
        a = Agent.objects.filter(site_id=site)
 | 
			
		||||
 | 
			
		||||
    # generate checks that has specified client
 | 
			
		||||
    elif client:
 | 
			
		||||
        a = Agent.objects.filter(site__client_id=client)
 | 
			
		||||
    else:
 | 
			
		||||
        agents = policy.related_agents().only("pk")
 | 
			
		||||
        a = []
 | 
			
		||||
 | 
			
		||||
    for agent in agents:
 | 
			
		||||
    for agent in a:
 | 
			
		||||
        agent.generate_checks_from_policies()
 | 
			
		||||
        if create_tasks:
 | 
			
		||||
            agent.generate_tasks_from_policies()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
# generates policy checks on a list of agents and optionally generate automated tasks
 | 
			
		||||
def generate_agent_checks_task(agentpks, create_tasks=False):
 | 
			
		||||
    for agent in Agent.objects.filter(pk__in=agentpks):
 | 
			
		||||
        agent.generate_checks_from_policies()
 | 
			
		||||
 | 
			
		||||
        if create_tasks:
 | 
			
		||||
            agent.generate_tasks_from_policies()
 | 
			
		||||
    return "ok"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
# generates policy checks on agent servers or workstations within a certain client or site and optionally generate automated tasks
 | 
			
		||||
def generate_agent_checks_by_location_task(location, mon_type, create_tasks=False):
 | 
			
		||||
 | 
			
		||||
    for agent in Agent.objects.filter(**location).filter(monitoring_type=mon_type):
 | 
			
		||||
        agent.generate_checks_from_policies()
 | 
			
		||||
 | 
			
		||||
        if create_tasks:
 | 
			
		||||
            agent.generate_tasks_from_policies()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
# generates policy checks on all agent servers or workstations and optionally generate automated tasks
 | 
			
		||||
def generate_all_agent_checks_task(mon_type, create_tasks=False):
 | 
			
		||||
    for agent in Agent.objects.filter(monitoring_type=mon_type):
 | 
			
		||||
        agent.generate_checks_from_policies()
 | 
			
		||||
 | 
			
		||||
        if create_tasks:
 | 
			
		||||
            agent.generate_tasks_from_policies()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
# deletes a policy managed check from all agents
 | 
			
		||||
def delete_policy_check_task(checkpk):
 | 
			
		||||
 | 
			
		||||
    Check.objects.filter(parent_check=checkpk).delete()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
@app.task(
 | 
			
		||||
    acks_late=True, retry_backoff=5, retry_jitter=True, retry_kwargs={"max_retries": 5}
 | 
			
		||||
)
 | 
			
		||||
# updates policy managed check fields on agents
 | 
			
		||||
def update_policy_check_fields_task(checkpk):
 | 
			
		||||
def update_policy_check_fields_task(check: int) -> str:
 | 
			
		||||
    from checks.models import Check
 | 
			
		||||
 | 
			
		||||
    check = Check.objects.get(pk=checkpk)
 | 
			
		||||
    c: Check = Check.objects.get(pk=check)
 | 
			
		||||
    update_fields: Dict[Any, Any] = {}
 | 
			
		||||
 | 
			
		||||
    Check.objects.filter(parent_check=checkpk).update(
 | 
			
		||||
        warning_threshold=check.warning_threshold,
 | 
			
		||||
        error_threshold=check.error_threshold,
 | 
			
		||||
        alert_severity=check.alert_severity,
 | 
			
		||||
        name=check.name,
 | 
			
		||||
        run_interval=check.run_interval,
 | 
			
		||||
        disk=check.disk,
 | 
			
		||||
        fails_b4_alert=check.fails_b4_alert,
 | 
			
		||||
        ip=check.ip,
 | 
			
		||||
        script=check.script,
 | 
			
		||||
        script_args=check.script_args,
 | 
			
		||||
        info_return_codes=check.info_return_codes,
 | 
			
		||||
        warning_return_codes=check.warning_return_codes,
 | 
			
		||||
        timeout=check.timeout,
 | 
			
		||||
        pass_if_start_pending=check.pass_if_start_pending,
 | 
			
		||||
        pass_if_svc_not_exist=check.pass_if_svc_not_exist,
 | 
			
		||||
        restart_if_stopped=check.restart_if_stopped,
 | 
			
		||||
        log_name=check.log_name,
 | 
			
		||||
        event_id=check.event_id,
 | 
			
		||||
        event_id_is_wildcard=check.event_id_is_wildcard,
 | 
			
		||||
        event_type=check.event_type,
 | 
			
		||||
        event_source=check.event_source,
 | 
			
		||||
        event_message=check.event_message,
 | 
			
		||||
        fail_when=check.fail_when,
 | 
			
		||||
        search_last_days=check.search_last_days,
 | 
			
		||||
        number_of_events_b4_alert=check.number_of_events_b4_alert,
 | 
			
		||||
        email_alert=check.email_alert,
 | 
			
		||||
        text_alert=check.text_alert,
 | 
			
		||||
        dashboard_alert=check.dashboard_alert,
 | 
			
		||||
    )
 | 
			
		||||
    for field in c.policy_fields_to_copy:
 | 
			
		||||
        update_fields[field] = getattr(c, field)
 | 
			
		||||
 | 
			
		||||
    Check.objects.filter(parent_check=check).update(**update_fields)
 | 
			
		||||
 | 
			
		||||
    return "ok"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
@app.task(retry_backoff=5, retry_jitter=True, retry_kwargs={"max_retries": 5})
 | 
			
		||||
# generates policy tasks on agents affected by a policy
 | 
			
		||||
def generate_agent_tasks_from_policies_task(policypk):
 | 
			
		||||
def generate_agent_autotasks_task(policy: int = None) -> str:
 | 
			
		||||
    from agents.models import Agent
 | 
			
		||||
    from automation.models import Policy
 | 
			
		||||
 | 
			
		||||
    policy = Policy.objects.get(pk=policypk)
 | 
			
		||||
    p: Policy = Policy.objects.get(pk=policy)
 | 
			
		||||
 | 
			
		||||
    if policy.is_default_server_policy and policy.is_default_workstation_policy:
 | 
			
		||||
    if p and p.is_default_server_policy and p.is_default_workstation_policy:
 | 
			
		||||
        agents = Agent.objects.prefetch_related("policy").only("pk", "monitoring_type")
 | 
			
		||||
    elif policy.is_default_server_policy:
 | 
			
		||||
    elif p and p.is_default_server_policy:
 | 
			
		||||
        agents = Agent.objects.filter(monitoring_type="server").only(
 | 
			
		||||
            "pk", "monitoring_type"
 | 
			
		||||
        )
 | 
			
		||||
    elif policy.is_default_workstation_policy:
 | 
			
		||||
    elif p and p.is_default_workstation_policy:
 | 
			
		||||
        agents = Agent.objects.filter(monitoring_type="workstation").only(
 | 
			
		||||
            "pk", "monitoring_type"
 | 
			
		||||
        )
 | 
			
		||||
    else:
 | 
			
		||||
        agents = policy.related_agents().only("pk")
 | 
			
		||||
        agents = p.related_agents().only("pk")
 | 
			
		||||
 | 
			
		||||
    for agent in agents:
 | 
			
		||||
        agent.generate_tasks_from_policies()
 | 
			
		||||
 | 
			
		||||
    return "ok"
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
def delete_policy_autotask_task(taskpk):
 | 
			
		||||
 | 
			
		||||
@app.task(
 | 
			
		||||
    acks_late=True,
 | 
			
		||||
    retry_backoff=5,
 | 
			
		||||
    retry_jitter=True,
 | 
			
		||||
    retry_kwargs={"max_retries": 5},
 | 
			
		||||
)
 | 
			
		||||
def delete_policy_autotasks_task(task: int) -> str:
 | 
			
		||||
    from autotasks.models import AutomatedTask
 | 
			
		||||
    from autotasks.tasks import delete_win_task_schedule
 | 
			
		||||
 | 
			
		||||
    for task in AutomatedTask.objects.filter(parent_task=taskpk):
 | 
			
		||||
        delete_win_task_schedule.delay(task.pk)
 | 
			
		||||
    for t in AutomatedTask.objects.filter(parent_task=task):
 | 
			
		||||
        t.delete_task_on_agent()
 | 
			
		||||
 | 
			
		||||
    return "ok"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
def run_win_policy_autotask_task(task_pks):
 | 
			
		||||
    from autotasks.tasks import run_win_task
 | 
			
		||||
def run_win_policy_autotasks_task(task: int) -> str:
 | 
			
		||||
    from autotasks.models import AutomatedTask
 | 
			
		||||
 | 
			
		||||
    for task in task_pks:
 | 
			
		||||
        run_win_task.delay(task)
 | 
			
		||||
    for t in AutomatedTask.objects.filter(parent_task=task):
 | 
			
		||||
        t.run_win_task()
 | 
			
		||||
 | 
			
		||||
    return "ok"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
def update_policy_task_fields_task(taskpk, update_agent=False):
 | 
			
		||||
    from autotasks.tasks import enable_or_disable_win_task
 | 
			
		||||
@app.task(
 | 
			
		||||
    acks_late=True,
 | 
			
		||||
    retry_backoff=5,
 | 
			
		||||
    retry_jitter=True,
 | 
			
		||||
    retry_kwargs={"max_retries": 5},
 | 
			
		||||
)
 | 
			
		||||
def update_policy_autotasks_fields_task(task: int, update_agent: bool = False) -> str:
 | 
			
		||||
    from autotasks.models import AutomatedTask
 | 
			
		||||
 | 
			
		||||
    task = AutomatedTask.objects.get(pk=taskpk)
 | 
			
		||||
    t = AutomatedTask.objects.get(pk=task)
 | 
			
		||||
    update_fields: Dict[str, Any] = {}
 | 
			
		||||
 | 
			
		||||
    AutomatedTask.objects.filter(parent_task=taskpk).update(
 | 
			
		||||
        alert_severity=task.alert_severity,
 | 
			
		||||
        email_alert=task.email_alert,
 | 
			
		||||
        text_alert=task.text_alert,
 | 
			
		||||
        dashboard_alert=task.dashboard_alert,
 | 
			
		||||
        script=task.script,
 | 
			
		||||
        script_args=task.script_args,
 | 
			
		||||
        name=task.name,
 | 
			
		||||
        timeout=task.timeout,
 | 
			
		||||
        enabled=task.enabled,
 | 
			
		||||
    )
 | 
			
		||||
    for field in t.policy_fields_to_copy:
 | 
			
		||||
        update_fields[field] = getattr(t, field)
 | 
			
		||||
 | 
			
		||||
    AutomatedTask.objects.filter(parent_task=task).update(**update_fields)
 | 
			
		||||
 | 
			
		||||
    if update_agent:
 | 
			
		||||
        for task in AutomatedTask.objects.filter(parent_task=taskpk):
 | 
			
		||||
            enable_or_disable_win_task.delay(task.pk, task.enabled)
 | 
			
		||||
        for t in AutomatedTask.objects.filter(parent_task=task).exclude(
 | 
			
		||||
            sync_status="initial"
 | 
			
		||||
        ):
 | 
			
		||||
            t.modify_task_on_agent()
 | 
			
		||||
 | 
			
		||||
    return "ok"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,9 @@
 | 
			
		||||
from itertools import cycle
 | 
			
		||||
from unittest.mock import patch
 | 
			
		||||
 | 
			
		||||
from model_bakery import baker, seq
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from core.models import CoreSettings
 | 
			
		||||
from model_bakery import baker, seq
 | 
			
		||||
from tacticalrmm.test import TacticalTestCase
 | 
			
		||||
from winupdate.models import WinUpdatePolicy
 | 
			
		||||
 | 
			
		||||
@@ -52,7 +51,10 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
    def test_add_policy(self):
 | 
			
		||||
    @patch("autotasks.models.AutomatedTask.create_task_on_agent")
 | 
			
		||||
    def test_add_policy(self, create_task):
 | 
			
		||||
        from automation.models import Policy
 | 
			
		||||
 | 
			
		||||
        url = "/automation/policies/"
 | 
			
		||||
 | 
			
		||||
        data = {
 | 
			
		||||
@@ -71,8 +73,12 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        # create policy with tasks and checks
 | 
			
		||||
        policy = baker.make("automation.Policy")
 | 
			
		||||
        self.create_checks(policy=policy)
 | 
			
		||||
        baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3)
 | 
			
		||||
        checks = self.create_checks(policy=policy)
 | 
			
		||||
        tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3)
 | 
			
		||||
 | 
			
		||||
        # assign a task to a check
 | 
			
		||||
        tasks[0].assigned_check = checks[0]  # type: ignore
 | 
			
		||||
        tasks[0].save()  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # test copy tasks and checks to another policy
 | 
			
		||||
        data = {
 | 
			
		||||
@@ -85,13 +91,21 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        resp = self.client.post(f"/automation/policies/", data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertEqual(policy.autotasks.count(), 3)  # type: ignore
 | 
			
		||||
        self.assertEqual(policy.policychecks.count(), 7)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        copied_policy = Policy.objects.get(name=data["name"])
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(copied_policy.autotasks.count(), 3)  # type: ignore
 | 
			
		||||
        self.assertEqual(copied_policy.policychecks.count(), 7)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # make sure correct task was assign to the check
 | 
			
		||||
        self.assertEqual(copied_policy.autotasks.get(name=tasks[0].name).assigned_check.check_type, checks[0].check_type)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        create_task.assert_not_called()
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("post", url)
 | 
			
		||||
 | 
			
		||||
    @patch("automation.tasks.generate_agent_checks_from_policies_task.delay")
 | 
			
		||||
    def test_update_policy(self, generate_agent_checks_from_policies_task):
 | 
			
		||||
    @patch("automation.tasks.generate_agent_checks_task.delay")
 | 
			
		||||
    def test_update_policy(self, generate_agent_checks_task):
 | 
			
		||||
        # returns 404 for invalid policy pk
 | 
			
		||||
        resp = self.client.put("/automation/policies/500/", format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 404)
 | 
			
		||||
@@ -109,8 +123,8 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
        resp = self.client.put(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        # only called if active or enforced are updated
 | 
			
		||||
        generate_agent_checks_from_policies_task.assert_not_called()
 | 
			
		||||
        # only called if active, enforced, or excluded objects are updated
 | 
			
		||||
        generate_agent_checks_task.assert_not_called()
 | 
			
		||||
 | 
			
		||||
        data = {
 | 
			
		||||
            "name": "Test Policy Update",
 | 
			
		||||
@@ -121,8 +135,25 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        resp = self.client.put(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        generate_agent_checks_from_policies_task.assert_called_with(
 | 
			
		||||
            policypk=policy.pk, create_tasks=True  # type: ignore
 | 
			
		||||
        generate_agent_checks_task.assert_called_with(
 | 
			
		||||
            policy=policy.pk, create_tasks=True  # type: ignore
 | 
			
		||||
        )
 | 
			
		||||
        generate_agent_checks_task.reset_mock()
 | 
			
		||||
 | 
			
		||||
        # make sure policies are re-evaluated when excluded changes
 | 
			
		||||
        agents = baker.make_recipe("agents.agent", _quantity=2)
 | 
			
		||||
        clients = baker.make("clients.Client", _quantity=2)
 | 
			
		||||
        sites = baker.make("clients.Site", _quantity=2)
 | 
			
		||||
        data = {
 | 
			
		||||
            "excluded_agents": [agent.pk for agent in agents],  # type: ignore
 | 
			
		||||
            "excluded_sites": [site.pk for site in sites],  # type: ignore
 | 
			
		||||
            "excluded_clients": [client.pk for client in clients],  # type: ignore
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        resp = self.client.put(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        generate_agent_checks_task.assert_called_with(
 | 
			
		||||
            policy=policy.pk, create_tasks=True  # type: ignore
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("put", url)
 | 
			
		||||
@@ -145,7 +176,7 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        generate_agent_checks_task.assert_called_with(
 | 
			
		||||
            [agent.pk for agent in agents], create_tasks=True
 | 
			
		||||
            agents=[agent.pk for agent in agents], create_tasks=True
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("delete", url)
 | 
			
		||||
@@ -271,7 +302,7 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("patch", url)
 | 
			
		||||
 | 
			
		||||
    @patch("automation.tasks.run_win_policy_autotask_task.delay")
 | 
			
		||||
    @patch("automation.tasks.run_win_policy_autotasks_task.delay")
 | 
			
		||||
    def test_run_win_task(self, mock_task):
 | 
			
		||||
 | 
			
		||||
        # create managed policy tasks
 | 
			
		||||
@@ -281,11 +312,12 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
            parent_task=1,
 | 
			
		||||
            _quantity=6,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        url = "/automation/runwintask/1/"
 | 
			
		||||
        resp = self.client.put(url, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        mock_task.assert_called_once_with([task.pk for task in tasks])  # type: ignore
 | 
			
		||||
        mock_task.assert_called()  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("put", url)
 | 
			
		||||
 | 
			
		||||
@@ -426,7 +458,7 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("delete", url)
 | 
			
		||||
 | 
			
		||||
    @patch("automation.tasks.generate_agent_checks_from_policies_task.delay")
 | 
			
		||||
    @patch("automation.tasks.generate_agent_checks_task.delay")
 | 
			
		||||
    def test_sync_policy(self, generate_checks):
 | 
			
		||||
        url = "/automation/sync/"
 | 
			
		||||
 | 
			
		||||
@@ -441,7 +473,7 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        resp = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        generate_checks.assert_called_with(policy.pk, create_tasks=True)  # type: ignore
 | 
			
		||||
        generate_checks.assert_called_with(policy=policy.pk, create_tasks=True)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("post", url)
 | 
			
		||||
 | 
			
		||||
@@ -497,7 +529,7 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
        self.assertEquals(len(resp.data["agents"]), 10)  # type: ignore
 | 
			
		||||
 | 
			
		||||
    def test_generating_agent_policy_checks(self):
 | 
			
		||||
        from .tasks import generate_agent_checks_from_policies_task
 | 
			
		||||
        from .tasks import generate_agent_checks_task
 | 
			
		||||
 | 
			
		||||
        # setup data
 | 
			
		||||
        policy = baker.make("automation.Policy", active=True)
 | 
			
		||||
@@ -505,7 +537,7 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
        agent = baker.make_recipe("agents.agent", policy=policy)
 | 
			
		||||
 | 
			
		||||
        # test policy assigned to agent
 | 
			
		||||
        generate_agent_checks_from_policies_task(policy.id)  # type: ignore
 | 
			
		||||
        generate_agent_checks_task(policy=policy.id)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # make sure all checks were created. should be 7
 | 
			
		||||
        agent_checks = Agent.objects.get(pk=agent.id).agentchecks.all()
 | 
			
		||||
@@ -545,7 +577,7 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
                self.assertEqual(check.event_type, checks[6].event_type)
 | 
			
		||||
 | 
			
		||||
    def test_generating_agent_policy_checks_with_enforced(self):
 | 
			
		||||
        from .tasks import generate_agent_checks_from_policies_task
 | 
			
		||||
        from .tasks import generate_agent_checks_task
 | 
			
		||||
 | 
			
		||||
        # setup data
 | 
			
		||||
        policy = baker.make("automation.Policy", active=True, enforced=True)
 | 
			
		||||
@@ -555,7 +587,7 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
        agent = baker.make_recipe("agents.agent", site=site, policy=policy)
 | 
			
		||||
        self.create_checks(agent=agent, script=script)
 | 
			
		||||
 | 
			
		||||
        generate_agent_checks_from_policies_task(policy.id, create_tasks=True)  # type: ignore
 | 
			
		||||
        generate_agent_checks_task(policy=policy.id, create_tasks=True)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # make sure each agent check says overriden_by_policy
 | 
			
		||||
        self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 14)
 | 
			
		||||
@@ -566,13 +598,12 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
            7,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    @patch("automation.tasks.generate_agent_checks_by_location_task.delay")
 | 
			
		||||
    @patch("autotasks.models.AutomatedTask.create_task_on_agent")
 | 
			
		||||
    @patch("automation.tasks.generate_agent_checks_task.delay")
 | 
			
		||||
    def test_generating_agent_policy_checks_by_location(
 | 
			
		||||
        self, generate_agent_checks_by_location_task
 | 
			
		||||
        self, generate_agent_checks_mock, create_task
 | 
			
		||||
    ):
 | 
			
		||||
        from automation.tasks import (
 | 
			
		||||
            generate_agent_checks_by_location_task as generate_agent_checks,
 | 
			
		||||
        )
 | 
			
		||||
        from automation.tasks import generate_agent_checks_task
 | 
			
		||||
 | 
			
		||||
        # setup data
 | 
			
		||||
        policy = baker.make("automation.Policy", active=True)
 | 
			
		||||
@@ -596,16 +627,14 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
        workstation_agent.client.save()
 | 
			
		||||
 | 
			
		||||
        # should trigger task in save method on core
 | 
			
		||||
        generate_agent_checks_by_location_task.assert_called_with(
 | 
			
		||||
            location={"site__client_id": workstation_agent.client.pk},
 | 
			
		||||
            mon_type="workstation",
 | 
			
		||||
        generate_agent_checks_mock.assert_called_with(
 | 
			
		||||
            client=workstation_agent.client.pk,
 | 
			
		||||
            create_tasks=True,
 | 
			
		||||
        )
 | 
			
		||||
        generate_agent_checks_by_location_task.reset_mock()
 | 
			
		||||
        generate_agent_checks_mock.reset_mock()
 | 
			
		||||
 | 
			
		||||
        generate_agent_checks(
 | 
			
		||||
            location={"site__client_id": workstation_agent.client.pk},
 | 
			
		||||
            mon_type="workstation",
 | 
			
		||||
        generate_agent_checks_task(
 | 
			
		||||
            client=workstation_agent.client.pk,
 | 
			
		||||
            create_tasks=True,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@@ -620,16 +649,14 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
        workstation_agent.client.save()
 | 
			
		||||
 | 
			
		||||
        # should trigger task in save method on core
 | 
			
		||||
        generate_agent_checks_by_location_task.assert_called_with(
 | 
			
		||||
            location={"site__client_id": workstation_agent.client.pk},
 | 
			
		||||
            mon_type="workstation",
 | 
			
		||||
        generate_agent_checks_mock.assert_called_with(
 | 
			
		||||
            client=workstation_agent.client.pk,
 | 
			
		||||
            create_tasks=True,
 | 
			
		||||
        )
 | 
			
		||||
        generate_agent_checks_by_location_task.reset_mock()
 | 
			
		||||
        generate_agent_checks_mock.reset_mock()
 | 
			
		||||
 | 
			
		||||
        generate_agent_checks(
 | 
			
		||||
            location={"site__client_id": workstation_agent.client.pk},
 | 
			
		||||
            mon_type="workstation",
 | 
			
		||||
        generate_agent_checks_task(
 | 
			
		||||
            client=workstation_agent.client.pk,
 | 
			
		||||
            create_tasks=True,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@@ -644,16 +671,14 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
        server_agent.client.save()
 | 
			
		||||
 | 
			
		||||
        # should trigger task in save method on core
 | 
			
		||||
        generate_agent_checks_by_location_task.assert_called_with(
 | 
			
		||||
            location={"site__client_id": server_agent.client.pk},
 | 
			
		||||
            mon_type="server",
 | 
			
		||||
        generate_agent_checks_mock.assert_called_with(
 | 
			
		||||
            client=server_agent.client.pk,
 | 
			
		||||
            create_tasks=True,
 | 
			
		||||
        )
 | 
			
		||||
        generate_agent_checks_by_location_task.reset_mock()
 | 
			
		||||
        generate_agent_checks_mock.reset_mock()
 | 
			
		||||
 | 
			
		||||
        generate_agent_checks(
 | 
			
		||||
            location={"site__client_id": server_agent.client.pk},
 | 
			
		||||
            mon_type="server",
 | 
			
		||||
        generate_agent_checks_task(
 | 
			
		||||
            client=server_agent.client.pk,
 | 
			
		||||
            create_tasks=True,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@@ -668,16 +693,14 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
        server_agent.client.save()
 | 
			
		||||
 | 
			
		||||
        # should trigger task in save method on core
 | 
			
		||||
        generate_agent_checks_by_location_task.assert_called_with(
 | 
			
		||||
            location={"site__client_id": server_agent.client.pk},
 | 
			
		||||
            mon_type="server",
 | 
			
		||||
        generate_agent_checks_mock.assert_called_with(
 | 
			
		||||
            client=server_agent.client.pk,
 | 
			
		||||
            create_tasks=True,
 | 
			
		||||
        )
 | 
			
		||||
        generate_agent_checks_by_location_task.reset_mock()
 | 
			
		||||
        generate_agent_checks_mock.reset_mock()
 | 
			
		||||
 | 
			
		||||
        generate_agent_checks(
 | 
			
		||||
            location={"site__client_id": server_agent.client.pk},
 | 
			
		||||
            mon_type="server",
 | 
			
		||||
        generate_agent_checks_task(
 | 
			
		||||
            client=server_agent.client.pk,
 | 
			
		||||
            create_tasks=True,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@@ -692,16 +715,14 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
        workstation_agent.site.save()
 | 
			
		||||
 | 
			
		||||
        # should trigger task in save method on core
 | 
			
		||||
        generate_agent_checks_by_location_task.assert_called_with(
 | 
			
		||||
            location={"site_id": workstation_agent.site.pk},
 | 
			
		||||
            mon_type="workstation",
 | 
			
		||||
        generate_agent_checks_mock.assert_called_with(
 | 
			
		||||
            site=workstation_agent.site.pk,
 | 
			
		||||
            create_tasks=True,
 | 
			
		||||
        )
 | 
			
		||||
        generate_agent_checks_by_location_task.reset_mock()
 | 
			
		||||
        generate_agent_checks_mock.reset_mock()
 | 
			
		||||
 | 
			
		||||
        generate_agent_checks(
 | 
			
		||||
            location={"site_id": workstation_agent.site.pk},
 | 
			
		||||
            mon_type="workstation",
 | 
			
		||||
        generate_agent_checks_task(
 | 
			
		||||
            site=workstation_agent.site.pk,
 | 
			
		||||
            create_tasks=True,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@@ -716,16 +737,14 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
        workstation_agent.site.save()
 | 
			
		||||
 | 
			
		||||
        # should trigger task in save method on core
 | 
			
		||||
        generate_agent_checks_by_location_task.assert_called_with(
 | 
			
		||||
            location={"site_id": workstation_agent.site.pk},
 | 
			
		||||
            mon_type="workstation",
 | 
			
		||||
        generate_agent_checks_mock.assert_called_with(
 | 
			
		||||
            site=workstation_agent.site.pk,
 | 
			
		||||
            create_tasks=True,
 | 
			
		||||
        )
 | 
			
		||||
        generate_agent_checks_by_location_task.reset_mock()
 | 
			
		||||
        generate_agent_checks_mock.reset_mock()
 | 
			
		||||
 | 
			
		||||
        generate_agent_checks(
 | 
			
		||||
            location={"site_id": workstation_agent.site.pk},
 | 
			
		||||
            mon_type="workstation",
 | 
			
		||||
        generate_agent_checks_task(
 | 
			
		||||
            site=workstation_agent.site.pk,
 | 
			
		||||
            create_tasks=True,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@@ -740,16 +759,14 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
        server_agent.site.save()
 | 
			
		||||
 | 
			
		||||
        # should trigger task in save method on core
 | 
			
		||||
        generate_agent_checks_by_location_task.assert_called_with(
 | 
			
		||||
            location={"site_id": server_agent.site.pk},
 | 
			
		||||
            mon_type="server",
 | 
			
		||||
        generate_agent_checks_mock.assert_called_with(
 | 
			
		||||
            site=server_agent.site.pk,
 | 
			
		||||
            create_tasks=True,
 | 
			
		||||
        )
 | 
			
		||||
        generate_agent_checks_by_location_task.reset_mock()
 | 
			
		||||
        generate_agent_checks_mock.reset_mock()
 | 
			
		||||
 | 
			
		||||
        generate_agent_checks(
 | 
			
		||||
            location={"site_id": server_agent.site.pk},
 | 
			
		||||
            mon_type="server",
 | 
			
		||||
        generate_agent_checks_task(
 | 
			
		||||
            site=server_agent.site.pk,
 | 
			
		||||
            create_tasks=True,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@@ -764,16 +781,14 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
        server_agent.site.save()
 | 
			
		||||
 | 
			
		||||
        # should trigger task in save method on core
 | 
			
		||||
        generate_agent_checks_by_location_task.assert_called_with(
 | 
			
		||||
            location={"site_id": server_agent.site.pk},
 | 
			
		||||
            mon_type="server",
 | 
			
		||||
        generate_agent_checks_mock.assert_called_with(
 | 
			
		||||
            site=server_agent.site.pk,
 | 
			
		||||
            create_tasks=True,
 | 
			
		||||
        )
 | 
			
		||||
        generate_agent_checks_by_location_task.reset_mock()
 | 
			
		||||
        generate_agent_checks_mock.reset_mock()
 | 
			
		||||
 | 
			
		||||
        generate_agent_checks(
 | 
			
		||||
            location={"site_id": server_agent.site.pk},
 | 
			
		||||
            mon_type="server",
 | 
			
		||||
        generate_agent_checks_task(
 | 
			
		||||
            site=server_agent.site.pk,
 | 
			
		||||
            create_tasks=True,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@@ -783,13 +798,11 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
            Agent.objects.get(pk=workstation_agent.id).agentchecks.count(), 0
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    @patch("automation.tasks.generate_all_agent_checks_task.delay")
 | 
			
		||||
    def test_generating_policy_checks_for_all_agents(
 | 
			
		||||
        self, generate_all_agent_checks_task
 | 
			
		||||
    ):
 | 
			
		||||
    @patch("automation.tasks.generate_agent_checks_task.delay")
 | 
			
		||||
    def test_generating_policy_checks_for_all_agents(self, generate_agent_checks_mock):
 | 
			
		||||
        from core.models import CoreSettings
 | 
			
		||||
 | 
			
		||||
        from .tasks import generate_all_agent_checks_task as generate_all_checks
 | 
			
		||||
        from .tasks import generate_agent_checks_task
 | 
			
		||||
 | 
			
		||||
        # setup data
 | 
			
		||||
        policy = baker.make("automation.Policy", active=True)
 | 
			
		||||
@@ -801,11 +814,9 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
        core.server_policy = policy
 | 
			
		||||
        core.save()
 | 
			
		||||
 | 
			
		||||
        generate_all_agent_checks_task.assert_called_with(
 | 
			
		||||
            mon_type="server", create_tasks=True
 | 
			
		||||
        )
 | 
			
		||||
        generate_all_agent_checks_task.reset_mock()
 | 
			
		||||
        generate_all_checks(mon_type="server", create_tasks=True)
 | 
			
		||||
        generate_agent_checks_mock.assert_called_with(all=True, create_tasks=True)
 | 
			
		||||
        generate_agent_checks_mock.reset_mock()
 | 
			
		||||
        generate_agent_checks_task(all=True, create_tasks=True)
 | 
			
		||||
 | 
			
		||||
        # all servers should have 7 checks
 | 
			
		||||
        for agent in server_agents:
 | 
			
		||||
@@ -818,15 +829,9 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
        core.workstation_policy = policy
 | 
			
		||||
        core.save()
 | 
			
		||||
 | 
			
		||||
        generate_all_agent_checks_task.assert_any_call(
 | 
			
		||||
            mon_type="workstation", create_tasks=True
 | 
			
		||||
        )
 | 
			
		||||
        generate_all_agent_checks_task.assert_any_call(
 | 
			
		||||
            mon_type="server", create_tasks=True
 | 
			
		||||
        )
 | 
			
		||||
        generate_all_agent_checks_task.reset_mock()
 | 
			
		||||
        generate_all_checks(mon_type="server", create_tasks=True)
 | 
			
		||||
        generate_all_checks(mon_type="workstation", create_tasks=True)
 | 
			
		||||
        generate_agent_checks_mock.assert_any_call(all=True, create_tasks=True)
 | 
			
		||||
        generate_agent_checks_mock.reset_mock()
 | 
			
		||||
        generate_agent_checks_task(all=True, create_tasks=True)
 | 
			
		||||
 | 
			
		||||
        # all workstations should have 7 checks
 | 
			
		||||
        for agent in server_agents:
 | 
			
		||||
@@ -838,11 +843,9 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
        core.workstation_policy = None
 | 
			
		||||
        core.save()
 | 
			
		||||
 | 
			
		||||
        generate_all_agent_checks_task.assert_called_with(
 | 
			
		||||
            mon_type="workstation", create_tasks=True
 | 
			
		||||
        )
 | 
			
		||||
        generate_all_agent_checks_task.reset_mock()
 | 
			
		||||
        generate_all_checks(mon_type="workstation", create_tasks=True)
 | 
			
		||||
        generate_agent_checks_mock.assert_called_with(all=True, create_tasks=True)
 | 
			
		||||
        generate_agent_checks_mock.reset_mock()
 | 
			
		||||
        generate_agent_checks_task(all=True, create_tasks=True)
 | 
			
		||||
 | 
			
		||||
        # nothing should have the checks
 | 
			
		||||
        for agent in server_agents:
 | 
			
		||||
@@ -851,31 +854,8 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
        for agent in workstation_agents:
 | 
			
		||||
            self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 0)
 | 
			
		||||
 | 
			
		||||
    def test_delete_policy_check(self):
 | 
			
		||||
        from .models import Policy
 | 
			
		||||
        from .tasks import delete_policy_check_task
 | 
			
		||||
 | 
			
		||||
        policy = baker.make("automation.Policy", active=True)
 | 
			
		||||
        self.create_checks(policy=policy)
 | 
			
		||||
        agent = baker.make_recipe("agents.server_agent", policy=policy)
 | 
			
		||||
 | 
			
		||||
        # make sure agent has 7 checks
 | 
			
		||||
        self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 7)
 | 
			
		||||
 | 
			
		||||
        # pick a policy check and delete it from the agent
 | 
			
		||||
        policy_check_id = Policy.objects.get(pk=policy.id).policychecks.first().id  # type: ignore
 | 
			
		||||
 | 
			
		||||
        delete_policy_check_task(policy_check_id)
 | 
			
		||||
 | 
			
		||||
        # make sure policy check doesn't exist on agent
 | 
			
		||||
        self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 6)
 | 
			
		||||
        self.assertFalse(
 | 
			
		||||
            Agent.objects.get(pk=agent.id)
 | 
			
		||||
            .agentchecks.filter(parent_check=policy_check_id)
 | 
			
		||||
            .exists()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def update_policy_check_fields(self):
 | 
			
		||||
    @patch("autotasks.models.AutomatedTask.create_task_on_agent")
 | 
			
		||||
    def update_policy_check_fields(self, create_task):
 | 
			
		||||
        from .models import Policy
 | 
			
		||||
        from .tasks import update_policy_check_fields_task
 | 
			
		||||
 | 
			
		||||
@@ -905,8 +885,9 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
            "12.12.12.12",
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_generate_agent_tasks(self):
 | 
			
		||||
        from .tasks import generate_agent_tasks_from_policies_task
 | 
			
		||||
    @patch("autotasks.models.AutomatedTask.create_task_on_agent")
 | 
			
		||||
    def test_generate_agent_tasks(self, create_task):
 | 
			
		||||
        from .tasks import generate_agent_autotasks_task
 | 
			
		||||
 | 
			
		||||
        # create test data
 | 
			
		||||
        policy = baker.make("automation.Policy", active=True)
 | 
			
		||||
@@ -915,7 +896,7 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
        )
 | 
			
		||||
        agent = baker.make_recipe("agents.server_agent", policy=policy)
 | 
			
		||||
 | 
			
		||||
        generate_agent_tasks_from_policies_task(policy.id)  # type: ignore
 | 
			
		||||
        generate_agent_autotasks_task(policy=policy.id)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        agent_tasks = Agent.objects.get(pk=agent.id).autotasks.all()
 | 
			
		||||
 | 
			
		||||
@@ -934,56 +915,70 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
                self.assertEqual(task.parent_task, tasks[2].id)  # type: ignore
 | 
			
		||||
                self.assertEqual(task.name, tasks[2].name)  # type: ignore
 | 
			
		||||
 | 
			
		||||
    @patch("autotasks.tasks.delete_win_task_schedule.delay")
 | 
			
		||||
    def test_delete_policy_tasks(self, delete_win_task_schedule):
 | 
			
		||||
        from .tasks import delete_policy_autotask_task
 | 
			
		||||
    @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, generate_agent_checks_task
 | 
			
		||||
 | 
			
		||||
        policy = baker.make("automation.Policy", active=True)
 | 
			
		||||
        tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3)
 | 
			
		||||
        agent = baker.make_recipe("agents.server_agent", policy=policy)
 | 
			
		||||
 | 
			
		||||
        delete_policy_autotask_task(tasks[0].id)  # type: ignore
 | 
			
		||||
        generate_agent_checks_task(agents=[agent.pk], create_tasks=True)
 | 
			
		||||
 | 
			
		||||
        delete_win_task_schedule.assert_called_with(
 | 
			
		||||
            agent.autotasks.get(parent_task=tasks[0].id).id  # type: ignore
 | 
			
		||||
        delete_policy_autotasks_task(task=tasks[0].id)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        delete_task_on_agent.assert_called()
 | 
			
		||||
 | 
			
		||||
    @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, generate_agent_checks_task
 | 
			
		||||
 | 
			
		||||
        policy = baker.make("automation.Policy", active=True)
 | 
			
		||||
        tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3)
 | 
			
		||||
        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
 | 
			
		||||
 | 
			
		||||
        run_win_task.assert_called_once()
 | 
			
		||||
 | 
			
		||||
    @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,
 | 
			
		||||
            generate_agent_checks_task,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    @patch("autotasks.tasks.run_win_task.delay")
 | 
			
		||||
    def test_run_policy_task(self, run_win_task):
 | 
			
		||||
        from .tasks import run_win_policy_autotask_task
 | 
			
		||||
 | 
			
		||||
        tasks = baker.make("autotasks.AutomatedTask", _quantity=3)
 | 
			
		||||
 | 
			
		||||
        run_win_policy_autotask_task([task.id for task in tasks])  # type: ignore
 | 
			
		||||
 | 
			
		||||
        run_win_task.side_effect = [task.id for task in tasks]  # type: ignore
 | 
			
		||||
        self.assertEqual(run_win_task.call_count, 3)
 | 
			
		||||
        for task in tasks:  # type: ignore
 | 
			
		||||
            run_win_task.assert_any_call(task.id)  # type: ignore
 | 
			
		||||
 | 
			
		||||
    @patch("autotasks.tasks.enable_or_disable_win_task.delay")
 | 
			
		||||
    def test_update_policy_tasks(self, enable_or_disable_win_task):
 | 
			
		||||
        from .tasks import update_policy_task_fields_task
 | 
			
		||||
 | 
			
		||||
        # setup data
 | 
			
		||||
        policy = baker.make("automation.Policy", active=True)
 | 
			
		||||
        tasks = baker.make(
 | 
			
		||||
            "autotasks.AutomatedTask", enabled=True, policy=policy, _quantity=3
 | 
			
		||||
            "autotasks.AutomatedTask",
 | 
			
		||||
            enabled=True,
 | 
			
		||||
            policy=policy,
 | 
			
		||||
            _quantity=3,
 | 
			
		||||
        )
 | 
			
		||||
        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
 | 
			
		||||
 | 
			
		||||
        update_policy_task_fields_task(tasks[0].id)  # type: ignore
 | 
			
		||||
        enable_or_disable_win_task.assert_not_called()
 | 
			
		||||
        update_policy_autotasks_fields_task(task=tasks[0].id)  # type: ignore
 | 
			
		||||
        modify_task_on_agent.assert_not_called()
 | 
			
		||||
 | 
			
		||||
        self.assertFalse(agent.autotasks.get(parent_task=tasks[0].id).enabled)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        update_policy_task_fields_task(tasks[0].id, update_agent=True)  # type: ignore
 | 
			
		||||
        enable_or_disable_win_task.assert_called_with(
 | 
			
		||||
            agent.autotasks.get(parent_task=tasks[0].id).id, False  # type: ignore
 | 
			
		||||
        )
 | 
			
		||||
        update_policy_autotasks_fields_task(task=tasks[0].id, update_agent=True)  # type: ignore
 | 
			
		||||
        modify_task_on_agent.assert_not_called()
 | 
			
		||||
 | 
			
		||||
        agent.autotasks.update(sync_status="synced")
 | 
			
		||||
        update_policy_autotasks_fields_task(task=tasks[0].id, update_agent=True)  # type: ignore
 | 
			
		||||
        modify_task_on_agent.assert_called_once()
 | 
			
		||||
 | 
			
		||||
    @patch("agents.models.Agent.generate_tasks_from_policies")
 | 
			
		||||
    @patch("agents.models.Agent.generate_checks_from_policies")
 | 
			
		||||
@@ -996,17 +991,21 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
        generate_checks.reset_mock()
 | 
			
		||||
        generate_tasks.reset_mock()
 | 
			
		||||
 | 
			
		||||
        generate_agent_checks_task([agent.pk for agent in agents])
 | 
			
		||||
        generate_agent_checks_task(agents=[agent.pk for agent in agents])
 | 
			
		||||
        self.assertEquals(generate_checks.call_count, 5)
 | 
			
		||||
        generate_tasks.assert_not_called()
 | 
			
		||||
        generate_checks.reset_mock()
 | 
			
		||||
 | 
			
		||||
        generate_agent_checks_task([agent.pk for agent in agents], create_tasks=True)
 | 
			
		||||
        generate_agent_checks_task(
 | 
			
		||||
            agents=[agent.pk for agent in agents], create_tasks=True
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEquals(generate_checks.call_count, 5)
 | 
			
		||||
        self.assertEquals(generate_checks.call_count, 5)
 | 
			
		||||
 | 
			
		||||
    @patch("autotasks.tasks.delete_win_task_schedule.delay")
 | 
			
		||||
    def test_policy_exclusions(self, delete_task):
 | 
			
		||||
    @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)
 | 
			
		||||
@@ -1015,6 +1014,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
 | 
			
		||||
@@ -1028,8 +1029,6 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(policy.related_agents().count(), 0)  # type: ignore
 | 
			
		||||
        self.assertEqual(agent.agentchecks.count(), 0)  # type: ignore
 | 
			
		||||
        delete_task.assert_called()
 | 
			
		||||
        delete_task.reset_mock()
 | 
			
		||||
 | 
			
		||||
        # delete agent tasks
 | 
			
		||||
        agent.autotasks.all().delete()
 | 
			
		||||
@@ -1051,8 +1050,6 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(policy.related_agents().count(), 0)  # type: ignore
 | 
			
		||||
        self.assertEqual(agent.agentchecks.count(), 0)  # type: ignore
 | 
			
		||||
        delete_task.assert_called()
 | 
			
		||||
        delete_task.reset_mock()
 | 
			
		||||
 | 
			
		||||
        # delete agent tasks and reset
 | 
			
		||||
        agent.autotasks.all().delete()
 | 
			
		||||
@@ -1074,8 +1071,6 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(policy.related_agents().count(), 0)  # type: ignore
 | 
			
		||||
        self.assertEqual(agent.agentchecks.count(), 0)  # type: ignore
 | 
			
		||||
        delete_task.assert_called()
 | 
			
		||||
        delete_task.reset_mock()
 | 
			
		||||
 | 
			
		||||
        # delete agent tasks and reset
 | 
			
		||||
        agent.autotasks.all().delete()
 | 
			
		||||
@@ -1103,11 +1098,82 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(policy.related_agents().count(), 0)  # type: ignore
 | 
			
		||||
        self.assertEqual(agent.agentchecks.count(), 0)  # type: ignore
 | 
			
		||||
        delete_task.assert_called()
 | 
			
		||||
        delete_task.reset_mock()
 | 
			
		||||
 | 
			
		||||
    def test_removing_duplicate_pending_task_actions(self):
 | 
			
		||||
        pass
 | 
			
		||||
    @patch("autotasks.models.AutomatedTask.create_task_on_agent")
 | 
			
		||||
    def test_policy_inheritance_blocking(self, create_task):
 | 
			
		||||
        # setup data
 | 
			
		||||
        policy = baker.make("automation.Policy", active=True)
 | 
			
		||||
        baker.make_recipe("checks.memory_check", policy=policy)
 | 
			
		||||
        baker.make("autotasks.AutomatedTask", policy=policy)
 | 
			
		||||
        agent = baker.make_recipe("agents.agent", monitoring_type="server")
 | 
			
		||||
 | 
			
		||||
    def test_creating_checks_with_assigned_tasks(self):
 | 
			
		||||
        pass
 | 
			
		||||
        core = CoreSettings.objects.first()
 | 
			
		||||
        core.server_policy = policy
 | 
			
		||||
        core.save()
 | 
			
		||||
 | 
			
		||||
        agent.generate_checks_from_policies()
 | 
			
		||||
        agent.generate_tasks_from_policies()
 | 
			
		||||
 | 
			
		||||
        # should get policies from default policy
 | 
			
		||||
        self.assertTrue(agent.autotasks.all())
 | 
			
		||||
        self.assertTrue(agent.agentchecks.all())
 | 
			
		||||
 | 
			
		||||
        # test client blocking inheritance
 | 
			
		||||
        agent.site.client.block_policy_inheritance = True
 | 
			
		||||
        agent.site.client.save()
 | 
			
		||||
 | 
			
		||||
        agent.generate_checks_from_policies()
 | 
			
		||||
        agent.generate_tasks_from_policies()
 | 
			
		||||
 | 
			
		||||
        self.assertFalse(agent.autotasks.all())
 | 
			
		||||
        self.assertFalse(agent.agentchecks.all())
 | 
			
		||||
 | 
			
		||||
        agent.site.client.server_policy = policy
 | 
			
		||||
        agent.site.client.save()
 | 
			
		||||
 | 
			
		||||
        agent.generate_checks_from_policies()
 | 
			
		||||
        agent.generate_tasks_from_policies()
 | 
			
		||||
 | 
			
		||||
        # should get policies from client policy
 | 
			
		||||
        self.assertTrue(agent.autotasks.all())
 | 
			
		||||
        self.assertTrue(agent.agentchecks.all())
 | 
			
		||||
 | 
			
		||||
        # test site blocking inheritance
 | 
			
		||||
        agent.site.block_policy_inheritance = True
 | 
			
		||||
        agent.site.save()
 | 
			
		||||
 | 
			
		||||
        agent.generate_checks_from_policies()
 | 
			
		||||
        agent.generate_tasks_from_policies()
 | 
			
		||||
 | 
			
		||||
        self.assertFalse(agent.autotasks.all())
 | 
			
		||||
        self.assertFalse(agent.agentchecks.all())
 | 
			
		||||
 | 
			
		||||
        agent.site.server_policy = policy
 | 
			
		||||
        agent.site.save()
 | 
			
		||||
 | 
			
		||||
        agent.generate_checks_from_policies()
 | 
			
		||||
        agent.generate_tasks_from_policies()
 | 
			
		||||
 | 
			
		||||
        # should get policies from site policy
 | 
			
		||||
        self.assertTrue(agent.autotasks.all())
 | 
			
		||||
        self.assertTrue(agent.agentchecks.all())
 | 
			
		||||
 | 
			
		||||
        # test agent blocking inheritance
 | 
			
		||||
        agent.block_policy_inheritance = True
 | 
			
		||||
        agent.save()
 | 
			
		||||
 | 
			
		||||
        agent.generate_checks_from_policies()
 | 
			
		||||
        agent.generate_tasks_from_policies()
 | 
			
		||||
 | 
			
		||||
        self.assertFalse(agent.autotasks.all())
 | 
			
		||||
        self.assertFalse(agent.agentchecks.all())
 | 
			
		||||
 | 
			
		||||
        agent.policy = policy
 | 
			
		||||
        agent.save()
 | 
			
		||||
 | 
			
		||||
        agent.generate_checks_from_policies()
 | 
			
		||||
        agent.generate_tasks_from_policies()
 | 
			
		||||
 | 
			
		||||
        # should get policies from agent policy
 | 
			
		||||
        self.assertTrue(agent.autotasks.all())
 | 
			
		||||
        self.assertTrue(agent.agentchecks.all())
 | 
			
		||||
 
 | 
			
		||||
@@ -1,18 +1,19 @@
 | 
			
		||||
from django.shortcuts import get_object_or_404
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
from rest_framework.views import APIView
 | 
			
		||||
 | 
			
		||||
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 tacticalrmm.utils import notify_error
 | 
			
		||||
from winupdate.models import WinUpdatePolicy
 | 
			
		||||
from winupdate.serializers import WinUpdatePolicySerializer
 | 
			
		||||
 | 
			
		||||
from .models import Policy
 | 
			
		||||
from .permissions import AutomationPolicyPerms
 | 
			
		||||
from .serializers import (
 | 
			
		||||
    AutoTasksFieldSerializer,
 | 
			
		||||
    PolicyCheckSerializer,
 | 
			
		||||
@@ -22,10 +23,11 @@ from .serializers import (
 | 
			
		||||
    PolicyTableSerializer,
 | 
			
		||||
    PolicyTaskStatusSerializer,
 | 
			
		||||
)
 | 
			
		||||
from .tasks import run_win_policy_autotask_task
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetAddPolicies(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, AutomationPolicyPerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request):
 | 
			
		||||
        policies = Policy.objects.all()
 | 
			
		||||
 | 
			
		||||
@@ -53,18 +55,30 @@ class GetAddPolicies(APIView):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetUpdateDeletePolicy(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, AutomationPolicyPerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request, pk):
 | 
			
		||||
        policy = get_object_or_404(Policy, pk=pk)
 | 
			
		||||
 | 
			
		||||
        return Response(PolicySerializer(policy).data)
 | 
			
		||||
 | 
			
		||||
    def put(self, request, pk):
 | 
			
		||||
        from .tasks import generate_agent_checks_task
 | 
			
		||||
 | 
			
		||||
        policy = get_object_or_404(Policy, pk=pk)
 | 
			
		||||
 | 
			
		||||
        serializer = PolicySerializer(instance=policy, data=request.data, partial=True)
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        serializer.save()
 | 
			
		||||
 | 
			
		||||
        # check for excluding objects and in the request and if present generate policies
 | 
			
		||||
        if (
 | 
			
		||||
            "excluded_sites" in request.data.keys()
 | 
			
		||||
            or "excluded_clients" in request.data.keys()
 | 
			
		||||
            or "excluded_agents" in request.data.keys()
 | 
			
		||||
        ):
 | 
			
		||||
            generate_agent_checks_task.delay(policy=pk, create_tasks=True)
 | 
			
		||||
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
    def delete(self, request, pk):
 | 
			
		||||
@@ -76,10 +90,10 @@ class GetUpdateDeletePolicy(APIView):
 | 
			
		||||
class PolicySync(APIView):
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
        if "policy" in request.data.keys():
 | 
			
		||||
            from automation.tasks import generate_agent_checks_from_policies_task
 | 
			
		||||
            from automation.tasks import generate_agent_checks_task
 | 
			
		||||
 | 
			
		||||
            generate_agent_checks_from_policies_task.delay(
 | 
			
		||||
                request.data["policy"], create_tasks=True
 | 
			
		||||
            generate_agent_checks_task.delay(
 | 
			
		||||
                policy=request.data["policy"], create_tasks=True
 | 
			
		||||
            )
 | 
			
		||||
            return Response("ok")
 | 
			
		||||
 | 
			
		||||
@@ -88,7 +102,7 @@ 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)
 | 
			
		||||
@@ -101,12 +115,15 @@ class PolicyAutoTask(APIView):
 | 
			
		||||
 | 
			
		||||
    # bulk run win tasks associated with policy
 | 
			
		||||
    def put(self, request, task):
 | 
			
		||||
        tasks = AutomatedTask.objects.filter(parent_task=task)
 | 
			
		||||
        run_win_policy_autotask_task.delay([task.id for task in tasks])
 | 
			
		||||
        from .tasks import run_win_policy_autotasks_task
 | 
			
		||||
 | 
			
		||||
        run_win_policy_autotasks_task.delay(task=task)
 | 
			
		||||
        return Response("Affected agent tasks will run shortly")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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)
 | 
			
		||||
@@ -179,7 +196,7 @@ class GetRelated(APIView):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UpdatePatchPolicy(APIView):
 | 
			
		||||
 | 
			
		||||
    permission_classes = [IsAuthenticated, AutomationPolicyPerms]
 | 
			
		||||
    # create new patch policy
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
        policy = get_object_or_404(Policy, pk=request.data["policy"])
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,31 @@
 | 
			
		||||
# Generated by Django 3.1.7 on 2021-04-04 00:32
 | 
			
		||||
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('core', '0019_globalkvstore'),
 | 
			
		||||
        ('scripts', '0007_script_args'),
 | 
			
		||||
        ('autotasks', '0018_automatedtask_run_asap_after_missed'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='automatedtask',
 | 
			
		||||
            name='custom_field',
 | 
			
		||||
            field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='autotask', to='core.customfield'),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='automatedtask',
 | 
			
		||||
            name='retvalue',
 | 
			
		||||
            field=models.TextField(blank=True, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='automatedtask',
 | 
			
		||||
            name='script',
 | 
			
		||||
            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='autoscript', to='scripts.script'),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -0,0 +1,18 @@
 | 
			
		||||
# Generated by Django 3.1.7 on 2021-04-21 02:26
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('autotasks', '0019_auto_20210404_0032'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='automatedtask',
 | 
			
		||||
            name='sync_status',
 | 
			
		||||
            field=models.CharField(choices=[('synced', 'Synced With Agent'), ('notsynced', 'Waiting On Agent Checkin'), ('pendingdeletion', 'Pending Deletion on Agent'), ('initial', 'Initial Task Sync')], default='initial', max_length=100),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -0,0 +1,20 @@
 | 
			
		||||
# Generated by Django 3.1.7 on 2021-04-27 14:11
 | 
			
		||||
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('core', '0021_customfield_hide_in_ui'),
 | 
			
		||||
        ('autotasks', '0020_auto_20210421_0226'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='automatedtask',
 | 
			
		||||
            name='custom_field',
 | 
			
		||||
            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='autotasks', to='core.customfield'),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -0,0 +1,18 @@
 | 
			
		||||
# Generated by Django 3.2.1 on 2021-05-29 03:26
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('autotasks', '0021_alter_automatedtask_custom_field'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='automatedtask',
 | 
			
		||||
            name='collector_all_output',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -1,20 +1,20 @@
 | 
			
		||||
import asyncio
 | 
			
		||||
import datetime as dt
 | 
			
		||||
import random
 | 
			
		||||
import string
 | 
			
		||||
from typing import List
 | 
			
		||||
 | 
			
		||||
import pytz
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from alerts.models import SEVERITY_CHOICES
 | 
			
		||||
from django.contrib.postgres.fields import ArrayField
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.db.models.fields import DateTimeField
 | 
			
		||||
from loguru import logger
 | 
			
		||||
 | 
			
		||||
from alerts.models import SEVERITY_CHOICES
 | 
			
		||||
from logs.models import BaseAuditModel
 | 
			
		||||
from django.db.utils import DatabaseError
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from logs.models import BaseAuditModel, DebugLog
 | 
			
		||||
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"),
 | 
			
		||||
@@ -36,6 +36,7 @@ SYNC_STATUS_CHOICES = [
 | 
			
		||||
    ("synced", "Synced With Agent"),
 | 
			
		||||
    ("notsynced", "Waiting On Agent Checkin"),
 | 
			
		||||
    ("pendingdeletion", "Pending Deletion on Agent"),
 | 
			
		||||
    ("initial", "Initial Task Sync"),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
TASK_STATUS_CHOICES = [
 | 
			
		||||
@@ -60,12 +61,19 @@ class AutomatedTask(BaseAuditModel):
 | 
			
		||||
        blank=True,
 | 
			
		||||
        on_delete=models.CASCADE,
 | 
			
		||||
    )
 | 
			
		||||
    custom_field = models.ForeignKey(
 | 
			
		||||
        "core.CustomField",
 | 
			
		||||
        related_name="autotasks",
 | 
			
		||||
        null=True,
 | 
			
		||||
        blank=True,
 | 
			
		||||
        on_delete=models.SET_NULL,
 | 
			
		||||
    )
 | 
			
		||||
    script = models.ForeignKey(
 | 
			
		||||
        "scripts.Script",
 | 
			
		||||
        null=True,
 | 
			
		||||
        blank=True,
 | 
			
		||||
        related_name="autoscript",
 | 
			
		||||
        on_delete=models.CASCADE,
 | 
			
		||||
        on_delete=models.SET_NULL,
 | 
			
		||||
    )
 | 
			
		||||
    script_args = ArrayField(
 | 
			
		||||
        models.CharField(max_length=255, null=True, blank=True),
 | 
			
		||||
@@ -93,6 +101,7 @@ class AutomatedTask(BaseAuditModel):
 | 
			
		||||
    task_type = models.CharField(
 | 
			
		||||
        max_length=100, choices=TASK_TYPE_CHOICES, default="manual"
 | 
			
		||||
    )
 | 
			
		||||
    collector_all_output = models.BooleanField(default=False)
 | 
			
		||||
    run_time_date = DateTimeField(null=True, blank=True)
 | 
			
		||||
    remove_if_not_scheduled = models.BooleanField(default=False)
 | 
			
		||||
    run_asap_after_missed = models.BooleanField(default=False)  # added in agent v1.4.7
 | 
			
		||||
@@ -100,6 +109,7 @@ class AutomatedTask(BaseAuditModel):
 | 
			
		||||
    parent_task = models.PositiveIntegerField(null=True, blank=True)
 | 
			
		||||
    win_task_name = models.CharField(max_length=255, null=True, blank=True)
 | 
			
		||||
    timeout = models.PositiveIntegerField(default=120)
 | 
			
		||||
    retvalue = models.TextField(null=True, blank=True)
 | 
			
		||||
    retcode = models.IntegerField(null=True, blank=True)
 | 
			
		||||
    stdout = models.TextField(null=True, blank=True)
 | 
			
		||||
    stderr = models.TextField(null=True, blank=True)
 | 
			
		||||
@@ -110,7 +120,7 @@ class AutomatedTask(BaseAuditModel):
 | 
			
		||||
        max_length=30, choices=TASK_STATUS_CHOICES, default="pending"
 | 
			
		||||
    )
 | 
			
		||||
    sync_status = models.CharField(
 | 
			
		||||
        max_length=100, choices=SYNC_STATUS_CHOICES, default="notsynced"
 | 
			
		||||
        max_length=100, choices=SYNC_STATUS_CHOICES, default="initial"
 | 
			
		||||
    )
 | 
			
		||||
    alert_severity = models.CharField(
 | 
			
		||||
        max_length=30, choices=SEVERITY_CHOICES, default="info"
 | 
			
		||||
@@ -147,6 +157,32 @@ class AutomatedTask(BaseAuditModel):
 | 
			
		||||
 | 
			
		||||
        return self.last_run
 | 
			
		||||
 | 
			
		||||
    # These fields will be duplicated on the agent tasks that are managed by a policy
 | 
			
		||||
    @property
 | 
			
		||||
    def policy_fields_to_copy(self) -> List[str]:
 | 
			
		||||
        return [
 | 
			
		||||
            "alert_severity",
 | 
			
		||||
            "email_alert",
 | 
			
		||||
            "text_alert",
 | 
			
		||||
            "dashboard_alert",
 | 
			
		||||
            "script",
 | 
			
		||||
            "script_args",
 | 
			
		||||
            "assigned_check",
 | 
			
		||||
            "name",
 | 
			
		||||
            "run_time_days",
 | 
			
		||||
            "run_time_minute",
 | 
			
		||||
            "run_time_bit_weekdays",
 | 
			
		||||
            "run_time_date",
 | 
			
		||||
            "task_type",
 | 
			
		||||
            "win_task_name",
 | 
			
		||||
            "timeout",
 | 
			
		||||
            "enabled",
 | 
			
		||||
            "remove_if_not_scheduled",
 | 
			
		||||
            "run_asap_after_missed",
 | 
			
		||||
            "custom_field",
 | 
			
		||||
            "collector_all_output",
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def generate_task_name():
 | 
			
		||||
        chars = string.ascii_letters
 | 
			
		||||
@@ -155,73 +191,241 @@ 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):
 | 
			
		||||
        from .tasks import create_win_task_schedule
 | 
			
		||||
    def create_policy_task(self, agent=None, policy=None, assigned_check=None):
 | 
			
		||||
 | 
			
		||||
        # added to allow new policy tasks to be assigned to check only when the agent check exists already
 | 
			
		||||
        if (
 | 
			
		||||
            self.assigned_check
 | 
			
		||||
            and agent
 | 
			
		||||
            and agent.agentchecks.filter(parent_check=self.assigned_check.id).exists()
 | 
			
		||||
        ):
 | 
			
		||||
            assigned_check = agent.agentchecks.get(parent_check=self.assigned_check.id)
 | 
			
		||||
 | 
			
		||||
        # if policy is present, then this task is being copied to another policy
 | 
			
		||||
        # if agent is present, then this task is being created on an agent from a policy
 | 
			
		||||
        # exit if neither are set or if both are set
 | 
			
		||||
        if not agent and not policy or agent and policy:
 | 
			
		||||
        # also exit if assigned_check is set because this task will be created when the check is
 | 
			
		||||
        if (
 | 
			
		||||
            (not agent and not policy)
 | 
			
		||||
            or (agent and policy)
 | 
			
		||||
            or (self.assigned_check and not assigned_check)
 | 
			
		||||
        ):
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        assigned_check = None
 | 
			
		||||
 | 
			
		||||
        # get correct assigned check to task if set
 | 
			
		||||
        if agent and self.assigned_check:
 | 
			
		||||
            # check if there is a matching check on the agent
 | 
			
		||||
            if agent.agentchecks.filter(parent_check=self.assigned_check.pk).exists():
 | 
			
		||||
                assigned_check = agent.agentchecks.filter(
 | 
			
		||||
                    parent_check=self.assigned_check.pk
 | 
			
		||||
                ).first()
 | 
			
		||||
            # check was overriden by agent and we need to use that agents check
 | 
			
		||||
            else:
 | 
			
		||||
                if agent.agentchecks.filter(
 | 
			
		||||
                    check_type=self.assigned_check.check_type, overriden_by_policy=True
 | 
			
		||||
                ).exists():
 | 
			
		||||
                    assigned_check = agent.agentchecks.filter(
 | 
			
		||||
                        check_type=self.assigned_check.check_type,
 | 
			
		||||
                        overriden_by_policy=True,
 | 
			
		||||
                    ).first()
 | 
			
		||||
        elif policy and self.assigned_check:
 | 
			
		||||
            if policy.policychecks.filter(name=self.assigned_check.name).exists():
 | 
			
		||||
                assigned_check = policy.policychecks.filter(
 | 
			
		||||
                    name=self.assigned_check.name
 | 
			
		||||
                ).first()
 | 
			
		||||
            else:
 | 
			
		||||
                assigned_check = policy.policychecks.filter(
 | 
			
		||||
                    check_type=self.assigned_check.check_type
 | 
			
		||||
                ).first()
 | 
			
		||||
 | 
			
		||||
        task = AutomatedTask.objects.create(
 | 
			
		||||
            agent=agent,
 | 
			
		||||
            policy=policy,
 | 
			
		||||
            managed_by_policy=bool(agent),
 | 
			
		||||
            parent_task=(self.pk if agent else None),
 | 
			
		||||
            alert_severity=self.alert_severity,
 | 
			
		||||
            email_alert=self.email_alert,
 | 
			
		||||
            text_alert=self.text_alert,
 | 
			
		||||
            dashboard_alert=self.dashboard_alert,
 | 
			
		||||
            script=self.script,
 | 
			
		||||
            script_args=self.script_args,
 | 
			
		||||
            assigned_check=assigned_check,
 | 
			
		||||
            name=self.name,
 | 
			
		||||
            run_time_days=self.run_time_days,
 | 
			
		||||
            run_time_minute=self.run_time_minute,
 | 
			
		||||
            run_time_bit_weekdays=self.run_time_bit_weekdays,
 | 
			
		||||
            run_time_date=self.run_time_date,
 | 
			
		||||
            task_type=self.task_type,
 | 
			
		||||
            win_task_name=self.win_task_name,
 | 
			
		||||
            timeout=self.timeout,
 | 
			
		||||
            enabled=self.enabled,
 | 
			
		||||
            remove_if_not_scheduled=self.remove_if_not_scheduled,
 | 
			
		||||
            run_asap_after_missed=self.run_asap_after_missed,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        create_win_task_schedule.delay(task.pk)
 | 
			
		||||
        for field in self.policy_fields_to_copy:
 | 
			
		||||
            if field != "assigned_check":
 | 
			
		||||
                setattr(task, field, getattr(self, field))
 | 
			
		||||
 | 
			
		||||
        task.save()
 | 
			
		||||
 | 
			
		||||
        if agent:
 | 
			
		||||
            task.create_task_on_agent()
 | 
			
		||||
 | 
			
		||||
    def create_task_on_agent(self):
 | 
			
		||||
        from agents.models import Agent
 | 
			
		||||
 | 
			
		||||
        agent = (
 | 
			
		||||
            Agent.objects.filter(pk=self.agent.pk)
 | 
			
		||||
            .only("pk", "version", "hostname", "agent_id")
 | 
			
		||||
            .first()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        if self.task_type == "scheduled":
 | 
			
		||||
            nats_data = {
 | 
			
		||||
                "func": "schedtask",
 | 
			
		||||
                "schedtaskpayload": {
 | 
			
		||||
                    "type": "rmm",
 | 
			
		||||
                    "trigger": "weekly",
 | 
			
		||||
                    "weekdays": self.run_time_bit_weekdays,
 | 
			
		||||
                    "pk": self.pk,
 | 
			
		||||
                    "name": self.win_task_name,
 | 
			
		||||
                    "hour": dt.datetime.strptime(self.run_time_minute, "%H:%M").hour,
 | 
			
		||||
                    "min": dt.datetime.strptime(self.run_time_minute, "%H:%M").minute,
 | 
			
		||||
                },
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        elif self.task_type == "runonce":
 | 
			
		||||
            # check if scheduled time is in the past
 | 
			
		||||
            agent_tz = pytz.timezone(agent.timezone)  # type: ignore
 | 
			
		||||
            task_time_utc = self.run_time_date.replace(tzinfo=agent_tz).astimezone(
 | 
			
		||||
                pytz.utc
 | 
			
		||||
            )
 | 
			
		||||
            now = djangotime.now()
 | 
			
		||||
            if task_time_utc < now:
 | 
			
		||||
                self.run_time_date = now.astimezone(agent_tz).replace(
 | 
			
		||||
                    tzinfo=pytz.utc
 | 
			
		||||
                ) + djangotime.timedelta(minutes=5)
 | 
			
		||||
                self.save(update_fields=["run_time_date"])
 | 
			
		||||
 | 
			
		||||
            nats_data = {
 | 
			
		||||
                "func": "schedtask",
 | 
			
		||||
                "schedtaskpayload": {
 | 
			
		||||
                    "type": "rmm",
 | 
			
		||||
                    "trigger": "once",
 | 
			
		||||
                    "pk": self.pk,
 | 
			
		||||
                    "name": self.win_task_name,
 | 
			
		||||
                    "year": int(dt.datetime.strftime(self.run_time_date, "%Y")),
 | 
			
		||||
                    "month": dt.datetime.strftime(self.run_time_date, "%B"),
 | 
			
		||||
                    "day": int(dt.datetime.strftime(self.run_time_date, "%d")),
 | 
			
		||||
                    "hour": int(dt.datetime.strftime(self.run_time_date, "%H")),
 | 
			
		||||
                    "min": int(dt.datetime.strftime(self.run_time_date, "%M")),
 | 
			
		||||
                },
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            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
 | 
			
		||||
 | 
			
		||||
            if self.remove_if_not_scheduled:
 | 
			
		||||
                nats_data["schedtaskpayload"]["deleteafter"] = True
 | 
			
		||||
 | 
			
		||||
        elif self.task_type == "checkfailure" or self.task_type == "manual":
 | 
			
		||||
            nats_data = {
 | 
			
		||||
                "func": "schedtask",
 | 
			
		||||
                "schedtaskpayload": {
 | 
			
		||||
                    "type": "rmm",
 | 
			
		||||
                    "trigger": "manual",
 | 
			
		||||
                    "pk": self.pk,
 | 
			
		||||
                    "name": self.win_task_name,
 | 
			
		||||
                },
 | 
			
		||||
            }
 | 
			
		||||
        else:
 | 
			
		||||
            return "error"
 | 
			
		||||
 | 
			
		||||
        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"])
 | 
			
		||||
            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"])
 | 
			
		||||
            DebugLog.info(
 | 
			
		||||
                agent=agent,
 | 
			
		||||
                log_type="agent_issues",
 | 
			
		||||
                message=f"{agent.hostname} task {self.name} was successfully created",  # type: ignore
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        return "ok"
 | 
			
		||||
 | 
			
		||||
    def modify_task_on_agent(self):
 | 
			
		||||
        from agents.models import Agent
 | 
			
		||||
 | 
			
		||||
        agent = (
 | 
			
		||||
            Agent.objects.filter(pk=self.agent.pk)
 | 
			
		||||
            .only("pk", "version", "hostname", "agent_id")
 | 
			
		||||
            .first()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        nats_data = {
 | 
			
		||||
            "func": "enableschedtask",
 | 
			
		||||
            "schedtaskpayload": {
 | 
			
		||||
                "name": self.win_task_name,
 | 
			
		||||
                "enabled": self.enabled,
 | 
			
		||||
            },
 | 
			
		||||
        }
 | 
			
		||||
        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"])
 | 
			
		||||
            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"])
 | 
			
		||||
            DebugLog.info(
 | 
			
		||||
                agent=agent,
 | 
			
		||||
                log_type="agent_issues",
 | 
			
		||||
                message=f"{agent.hostname} task {self.name} was successfully modified",  # type: ignore
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        return "ok"
 | 
			
		||||
 | 
			
		||||
    def delete_task_on_agent(self):
 | 
			
		||||
        from agents.models import Agent
 | 
			
		||||
 | 
			
		||||
        agent = (
 | 
			
		||||
            Agent.objects.filter(pk=self.agent.pk)
 | 
			
		||||
            .only("pk", "version", "hostname", "agent_id")
 | 
			
		||||
            .first()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        nats_data = {
 | 
			
		||||
            "func": "delschedtask",
 | 
			
		||||
            "schedtaskpayload": {"name": self.win_task_name},
 | 
			
		||||
        }
 | 
			
		||||
        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"
 | 
			
		||||
 | 
			
		||||
            try:
 | 
			
		||||
                self.save(update_fields=["sync_status"])
 | 
			
		||||
            except DatabaseError:
 | 
			
		||||
                pass
 | 
			
		||||
 | 
			
		||||
            DebugLog.warning(
 | 
			
		||||
                agent=agent,
 | 
			
		||||
                log_type="agent_issues",
 | 
			
		||||
                message=f"{agent.hostname} task {self.name} will be deleted on next checkin",  # type: ignore
 | 
			
		||||
            )
 | 
			
		||||
            return "timeout"
 | 
			
		||||
        else:
 | 
			
		||||
            self.delete()
 | 
			
		||||
            DebugLog.info(
 | 
			
		||||
                agent=agent,
 | 
			
		||||
                log_type="agent_issues",
 | 
			
		||||
                message=f"{agent.hostname}({agent.pk}) task {self.name} was deleted",  # type: ignore
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        return "ok"
 | 
			
		||||
 | 
			
		||||
    def run_win_task(self):
 | 
			
		||||
        from agents.models import Agent
 | 
			
		||||
 | 
			
		||||
        agent = (
 | 
			
		||||
            Agent.objects.filter(pk=self.agent.pk)
 | 
			
		||||
            .only("pk", "version", "hostname", "agent_id")
 | 
			
		||||
            .first()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        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 (
 | 
			
		||||
@@ -242,9 +446,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"
 | 
			
		||||
 | 
			
		||||
@@ -253,16 +457,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"
 | 
			
		||||
 | 
			
		||||
@@ -271,7 +474,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
 | 
			
		||||
@@ -283,7 +486,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
 | 
			
		||||
@@ -294,4 +497,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
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										16
									
								
								api/tacticalrmm/autotasks/permissions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								api/tacticalrmm/autotasks/permissions.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
			
		||||
from rest_framework import permissions
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.permissions import _has_perm
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ManageAutoTaskPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        if r.method == "GET":
 | 
			
		||||
            return True
 | 
			
		||||
 | 
			
		||||
        return _has_perm(r, "can_manage_autotasks")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RunAutoTaskPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        return _has_perm(r, "can_run_autotasks")
 | 
			
		||||
@@ -68,6 +68,12 @@ class TaskRunnerGetSerializer(serializers.ModelSerializer):
 | 
			
		||||
 | 
			
		||||
class TaskGOGetSerializer(serializers.ModelSerializer):
 | 
			
		||||
    script = ScriptCheckSerializer(read_only=True)
 | 
			
		||||
    script_args = serializers.SerializerMethodField()
 | 
			
		||||
 | 
			
		||||
    def get_script_args(self, obj):
 | 
			
		||||
        return Script.parse_script_args(
 | 
			
		||||
            agent=obj.agent, shell=obj.script.shell, args=obj.script_args
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = AutomatedTask
 | 
			
		||||
@@ -78,3 +84,9 @@ class TaskRunnerPatchSerializer(serializers.ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = AutomatedTask
 | 
			
		||||
        fields = "__all__"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TaskAuditSerializer(serializers.ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = AutomatedTask
 | 
			
		||||
        fields = "__all__"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,210 +1,47 @@
 | 
			
		||||
import asyncio
 | 
			
		||||
import datetime as dt
 | 
			
		||||
from logging import log
 | 
			
		||||
import random
 | 
			
		||||
from time import sleep
 | 
			
		||||
from typing import Union
 | 
			
		||||
 | 
			
		||||
import pytz
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from loguru import logger
 | 
			
		||||
from packaging import version as pyver
 | 
			
		||||
 | 
			
		||||
from logs.models import PendingAction
 | 
			
		||||
from autotasks.models import AutomatedTask
 | 
			
		||||
from logs.models import DebugLog
 | 
			
		||||
from tacticalrmm.celery import app
 | 
			
		||||
 | 
			
		||||
from .models import AutomatedTask
 | 
			
		||||
 | 
			
		||||
logger.configure(**settings.LOG_CONFIG)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
def create_win_task_schedule(pk, pending_action=False):
 | 
			
		||||
def create_win_task_schedule(pk):
 | 
			
		||||
    task = AutomatedTask.objects.get(pk=pk)
 | 
			
		||||
 | 
			
		||||
    if task.task_type == "scheduled":
 | 
			
		||||
        nats_data = {
 | 
			
		||||
            "func": "schedtask",
 | 
			
		||||
            "schedtaskpayload": {
 | 
			
		||||
                "type": "rmm",
 | 
			
		||||
                "trigger": "weekly",
 | 
			
		||||
                "weekdays": task.run_time_bit_weekdays,
 | 
			
		||||
                "pk": task.pk,
 | 
			
		||||
                "name": task.win_task_name,
 | 
			
		||||
                "hour": dt.datetime.strptime(task.run_time_minute, "%H:%M").hour,
 | 
			
		||||
                "min": dt.datetime.strptime(task.run_time_minute, "%H:%M").minute,
 | 
			
		||||
            },
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    elif task.task_type == "runonce":
 | 
			
		||||
        # check if scheduled time is in the past
 | 
			
		||||
        agent_tz = pytz.timezone(task.agent.timezone)
 | 
			
		||||
        task_time_utc = task.run_time_date.replace(tzinfo=agent_tz).astimezone(pytz.utc)
 | 
			
		||||
        now = djangotime.now()
 | 
			
		||||
        if task_time_utc < now:
 | 
			
		||||
            task.run_time_date = now.astimezone(agent_tz).replace(
 | 
			
		||||
                tzinfo=pytz.utc
 | 
			
		||||
            ) + djangotime.timedelta(minutes=5)
 | 
			
		||||
            task.save(update_fields=["run_time_date"])
 | 
			
		||||
 | 
			
		||||
        nats_data = {
 | 
			
		||||
            "func": "schedtask",
 | 
			
		||||
            "schedtaskpayload": {
 | 
			
		||||
                "type": "rmm",
 | 
			
		||||
                "trigger": "once",
 | 
			
		||||
                "pk": task.pk,
 | 
			
		||||
                "name": task.win_task_name,
 | 
			
		||||
                "year": int(dt.datetime.strftime(task.run_time_date, "%Y")),
 | 
			
		||||
                "month": dt.datetime.strftime(task.run_time_date, "%B"),
 | 
			
		||||
                "day": int(dt.datetime.strftime(task.run_time_date, "%d")),
 | 
			
		||||
                "hour": int(dt.datetime.strftime(task.run_time_date, "%H")),
 | 
			
		||||
                "min": int(dt.datetime.strftime(task.run_time_date, "%M")),
 | 
			
		||||
            },
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if task.run_asap_after_missed and pyver.parse(
 | 
			
		||||
            task.agent.version
 | 
			
		||||
        ) >= pyver.parse("1.4.7"):
 | 
			
		||||
            nats_data["schedtaskpayload"]["run_asap_after_missed"] = True
 | 
			
		||||
 | 
			
		||||
        if task.remove_if_not_scheduled:
 | 
			
		||||
            nats_data["schedtaskpayload"]["deleteafter"] = True
 | 
			
		||||
 | 
			
		||||
    elif task.task_type == "checkfailure" or task.task_type == "manual":
 | 
			
		||||
        nats_data = {
 | 
			
		||||
            "func": "schedtask",
 | 
			
		||||
            "schedtaskpayload": {
 | 
			
		||||
                "type": "rmm",
 | 
			
		||||
                "trigger": "manual",
 | 
			
		||||
                "pk": task.pk,
 | 
			
		||||
                "name": task.win_task_name,
 | 
			
		||||
            },
 | 
			
		||||
        }
 | 
			
		||||
    else:
 | 
			
		||||
        return "error"
 | 
			
		||||
 | 
			
		||||
    r = asyncio.run(task.agent.nats_cmd(nats_data, timeout=10))
 | 
			
		||||
 | 
			
		||||
    if r != "ok":
 | 
			
		||||
        # don't create pending action if this task was initiated by a pending action
 | 
			
		||||
        if not pending_action:
 | 
			
		||||
 | 
			
		||||
            # complete any other pending actions on agent with same task_id
 | 
			
		||||
            task.agent.remove_matching_pending_task_actions(task.id)
 | 
			
		||||
 | 
			
		||||
            PendingAction(
 | 
			
		||||
                agent=task.agent,
 | 
			
		||||
                action_type="taskaction",
 | 
			
		||||
                details={"action": "taskcreate", "task_id": task.id},
 | 
			
		||||
            ).save()
 | 
			
		||||
            task.sync_status = "notsynced"
 | 
			
		||||
            task.save(update_fields=["sync_status"])
 | 
			
		||||
 | 
			
		||||
        logger.error(
 | 
			
		||||
            f"Unable to create scheduled task {task.win_task_name} on {task.agent.hostname}. It will be created when the agent checks in."
 | 
			
		||||
        )
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    # clear pending action since it was successful
 | 
			
		||||
    if pending_action:
 | 
			
		||||
        pendingaction = PendingAction.objects.get(pk=pending_action)
 | 
			
		||||
        pendingaction.status = "completed"
 | 
			
		||||
        pendingaction.save(update_fields=["status"])
 | 
			
		||||
 | 
			
		||||
    task.sync_status = "synced"
 | 
			
		||||
    task.save(update_fields=["sync_status"])
 | 
			
		||||
 | 
			
		||||
    logger.info(f"{task.agent.hostname} task {task.name} was successfully created")
 | 
			
		||||
    task.create_task_on_agent()
 | 
			
		||||
 | 
			
		||||
    return "ok"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
def enable_or_disable_win_task(pk, action, pending_action=False):
 | 
			
		||||
def enable_or_disable_win_task(pk):
 | 
			
		||||
    task = AutomatedTask.objects.get(pk=pk)
 | 
			
		||||
 | 
			
		||||
    nats_data = {
 | 
			
		||||
        "func": "enableschedtask",
 | 
			
		||||
        "schedtaskpayload": {
 | 
			
		||||
            "name": task.win_task_name,
 | 
			
		||||
            "enabled": action,
 | 
			
		||||
        },
 | 
			
		||||
    }
 | 
			
		||||
    r = asyncio.run(task.agent.nats_cmd(nats_data))
 | 
			
		||||
 | 
			
		||||
    if r != "ok":
 | 
			
		||||
        # don't create pending action if this task was initiated by a pending action
 | 
			
		||||
        if not pending_action:
 | 
			
		||||
            PendingAction(
 | 
			
		||||
                agent=task.agent,
 | 
			
		||||
                action_type="taskaction",
 | 
			
		||||
                details={
 | 
			
		||||
                    "action": "tasktoggle",
 | 
			
		||||
                    "value": action,
 | 
			
		||||
                    "task_id": task.id,
 | 
			
		||||
                },
 | 
			
		||||
            ).save()
 | 
			
		||||
            task.sync_status = "notsynced"
 | 
			
		||||
            task.save(update_fields=["sync_status"])
 | 
			
		||||
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    # clear pending action since it was successful
 | 
			
		||||
    if pending_action:
 | 
			
		||||
        pendingaction = PendingAction.objects.get(pk=pending_action)
 | 
			
		||||
        pendingaction.status = "completed"
 | 
			
		||||
        pendingaction.save(update_fields=["status"])
 | 
			
		||||
 | 
			
		||||
    task.sync_status = "synced"
 | 
			
		||||
    task.save(update_fields=["sync_status"])
 | 
			
		||||
    task.modify_task_on_agent()
 | 
			
		||||
 | 
			
		||||
    return "ok"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
def delete_win_task_schedule(pk, pending_action=False):
 | 
			
		||||
def delete_win_task_schedule(pk):
 | 
			
		||||
    task = AutomatedTask.objects.get(pk=pk)
 | 
			
		||||
 | 
			
		||||
    nats_data = {
 | 
			
		||||
        "func": "delschedtask",
 | 
			
		||||
        "schedtaskpayload": {"name": task.win_task_name},
 | 
			
		||||
    }
 | 
			
		||||
    r = asyncio.run(task.agent.nats_cmd(nats_data, timeout=10))
 | 
			
		||||
 | 
			
		||||
    if r != "ok" and "The system cannot find the file specified" not in r:
 | 
			
		||||
        # don't create pending action if this task was initiated by a pending action
 | 
			
		||||
        if not pending_action:
 | 
			
		||||
 | 
			
		||||
            # complete any other pending actions on agent with same task_id
 | 
			
		||||
            task.agent.remove_matching_pending_task_actions(task.id)
 | 
			
		||||
 | 
			
		||||
            PendingAction(
 | 
			
		||||
                agent=task.agent,
 | 
			
		||||
                action_type="taskaction",
 | 
			
		||||
                details={"action": "taskdelete", "task_id": task.id},
 | 
			
		||||
            ).save()
 | 
			
		||||
            task.sync_status = "pendingdeletion"
 | 
			
		||||
            task.save(update_fields=["sync_status"])
 | 
			
		||||
 | 
			
		||||
        return "timeout"
 | 
			
		||||
 | 
			
		||||
    # complete pending action since it was successful
 | 
			
		||||
    if pending_action:
 | 
			
		||||
        pendingaction = PendingAction.objects.get(pk=pending_action)
 | 
			
		||||
        pendingaction.status = "completed"
 | 
			
		||||
        pendingaction.save(update_fields=["status"])
 | 
			
		||||
 | 
			
		||||
    # complete any other pending actions on agent with same task_id
 | 
			
		||||
    task.agent.remove_matching_pending_task_actions(task.id)
 | 
			
		||||
 | 
			
		||||
    task.delete()
 | 
			
		||||
    task.delete_task_on_agent()
 | 
			
		||||
    return "ok"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
def run_win_task(pk):
 | 
			
		||||
    task = AutomatedTask.objects.get(pk=pk)
 | 
			
		||||
    asyncio.run(task.agent.nats_cmd({"func": "runtask", "taskpk": task.pk}, wait=False))
 | 
			
		||||
    task.run_win_task()
 | 
			
		||||
    return "ok"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -214,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))
 | 
			
		||||
@@ -244,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
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,6 @@ from unittest.mock import call, patch
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from model_bakery import baker
 | 
			
		||||
 | 
			
		||||
from logs.models import PendingAction
 | 
			
		||||
from tacticalrmm.test import TacticalTestCase
 | 
			
		||||
 | 
			
		||||
from .models import AutomatedTask
 | 
			
		||||
@@ -17,10 +16,10 @@ class TestAutotaskViews(TacticalTestCase):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
        self.setup_coresettings()
 | 
			
		||||
 | 
			
		||||
    @patch("automation.tasks.generate_agent_tasks_from_policies_task.delay")
 | 
			
		||||
    @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_tasks_from_policies_task
 | 
			
		||||
        self, create_win_task_schedule, generate_agent_autotasks_task
 | 
			
		||||
    ):
 | 
			
		||||
        url = "/tasks/automatedtasks/"
 | 
			
		||||
 | 
			
		||||
@@ -84,13 +83,13 @@ class TestAutotaskViews(TacticalTestCase):
 | 
			
		||||
                "task_type": "manual",
 | 
			
		||||
                "assigned_check": None,
 | 
			
		||||
            },
 | 
			
		||||
            "policy": policy.id,
 | 
			
		||||
            "policy": policy.id,  # type: ignore
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        resp = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        generate_agent_tasks_from_policies_task.assert_called_with(policy.id)
 | 
			
		||||
        generate_agent_autotasks_task.assert_called_with(policy=policy.id)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("post", url)
 | 
			
		||||
 | 
			
		||||
@@ -106,14 +105,14 @@ class TestAutotaskViews(TacticalTestCase):
 | 
			
		||||
        serializer = AutoTaskSerializer(agent)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertEqual(resp.data, serializer.data)
 | 
			
		||||
        self.assertEqual(resp.data, serializer.data)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
    @patch("autotasks.tasks.enable_or_disable_win_task.delay")
 | 
			
		||||
    @patch("automation.tasks.update_policy_task_fields_task.delay")
 | 
			
		||||
    @patch("automation.tasks.update_policy_autotasks_fields_task.delay")
 | 
			
		||||
    def test_update_autotask(
 | 
			
		||||
        self, update_policy_task_fields_task, enable_or_disable_win_task
 | 
			
		||||
        self, update_policy_autotasks_fields_task, enable_or_disable_win_task
 | 
			
		||||
    ):
 | 
			
		||||
        # setup data
 | 
			
		||||
        agent = baker.make_recipe("agents.agent")
 | 
			
		||||
@@ -125,32 +124,32 @@ class TestAutotaskViews(TacticalTestCase):
 | 
			
		||||
        resp = self.client.patch("/tasks/500/automatedtasks/", format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 404)
 | 
			
		||||
 | 
			
		||||
        url = f"/tasks/{agent_task.id}/automatedtasks/"
 | 
			
		||||
        url = f"/tasks/{agent_task.id}/automatedtasks/"  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # test editing agent task
 | 
			
		||||
        data = {"enableordisable": False}
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        enable_or_disable_win_task.assert_called_with(pk=agent_task.id, action=False)
 | 
			
		||||
        enable_or_disable_win_task.assert_called_with(pk=agent_task.id)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        url = f"/tasks/{policy_task.id}/automatedtasks/"
 | 
			
		||||
        url = f"/tasks/{policy_task.id}/automatedtasks/"  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # test editing policy task
 | 
			
		||||
        data = {"enableordisable": True}
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        update_policy_task_fields_task.assert_called_with(
 | 
			
		||||
            policy_task.id, update_agent=True
 | 
			
		||||
        update_policy_autotasks_fields_task.assert_called_with(
 | 
			
		||||
            task=policy_task.id, update_agent=True  # type: ignore
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("patch", url)
 | 
			
		||||
 | 
			
		||||
    @patch("autotasks.tasks.delete_win_task_schedule.delay")
 | 
			
		||||
    @patch("automation.tasks.delete_policy_autotask_task.delay")
 | 
			
		||||
    @patch("automation.tasks.delete_policy_autotasks_task.delay")
 | 
			
		||||
    def test_delete_autotask(
 | 
			
		||||
        self, delete_policy_autotask_task, delete_win_task_schedule
 | 
			
		||||
        self, delete_policy_autotasks_task, delete_win_task_schedule
 | 
			
		||||
    ):
 | 
			
		||||
        # setup data
 | 
			
		||||
        agent = baker.make_recipe("agents.agent")
 | 
			
		||||
@@ -163,21 +162,22 @@ class TestAutotaskViews(TacticalTestCase):
 | 
			
		||||
        self.assertEqual(resp.status_code, 404)
 | 
			
		||||
 | 
			
		||||
        # test delete agent task
 | 
			
		||||
        url = f"/tasks/{agent_task.id}/automatedtasks/"
 | 
			
		||||
        url = f"/tasks/{agent_task.id}/automatedtasks/"  # 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)
 | 
			
		||||
        delete_win_task_schedule.assert_called_with(pk=agent_task.id)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # test delete policy task
 | 
			
		||||
        url = f"/tasks/{policy_task.id}/automatedtasks/"
 | 
			
		||||
        url = f"/tasks/{policy_task.id}/automatedtasks/"  # type: ignore
 | 
			
		||||
        resp = self.client.delete(url, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        delete_policy_autotask_task.assert_called_with(policy_task.id)
 | 
			
		||||
        self.assertFalse(AutomatedTask.objects.filter(pk=policy_task.id))  # type: ignore
 | 
			
		||||
        delete_policy_autotasks_task.assert_called_with(task=policy_task.id)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("delete", url)
 | 
			
		||||
 | 
			
		||||
    @patch("agents.models.Agent.nats_cmd")
 | 
			
		||||
    def test_run_autotask(self, nats_cmd):
 | 
			
		||||
    @patch("autotasks.tasks.run_win_task.delay")
 | 
			
		||||
    def test_run_autotask(self, run_win_task):
 | 
			
		||||
        # setup data
 | 
			
		||||
        agent = baker.make_recipe("agents.agent", version="1.1.0")
 | 
			
		||||
        task = baker.make("autotasks.AutomatedTask", agent=agent)
 | 
			
		||||
@@ -187,11 +187,10 @@ class TestAutotaskViews(TacticalTestCase):
 | 
			
		||||
        self.assertEqual(resp.status_code, 404)
 | 
			
		||||
 | 
			
		||||
        # test run agent task
 | 
			
		||||
        url = f"/tasks/runwintask/{task.id}/"
 | 
			
		||||
        url = f"/tasks/runwintask/{task.id}/"  # type: ignore
 | 
			
		||||
        resp = self.client.get(url, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        nats_cmd.assert_called_with({"func": "runtask", "taskpk": task.id}, wait=False)
 | 
			
		||||
        nats_cmd.reset_mock()
 | 
			
		||||
        run_win_task.assert_called()
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
@@ -284,9 +283,9 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
 | 
			
		||||
            run_time_bit_weekdays=127,
 | 
			
		||||
            run_time_minute="21:55",
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(self.task1.sync_status, "notsynced")
 | 
			
		||||
        self.assertEqual(self.task1.sync_status, "initial")
 | 
			
		||||
        nats_cmd.return_value = "ok"
 | 
			
		||||
        ret = create_win_task_schedule.s(pk=self.task1.pk, pending_action=False).apply()
 | 
			
		||||
        ret = create_win_task_schedule.s(pk=self.task1.pk).apply()
 | 
			
		||||
        self.assertEqual(nats_cmd.call_count, 1)
 | 
			
		||||
        nats_cmd.assert_called_with(
 | 
			
		||||
            {
 | 
			
		||||
@@ -301,29 +300,16 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
 | 
			
		||||
                    "min": 55,
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
            timeout=10,
 | 
			
		||||
            timeout=5,
 | 
			
		||||
        )
 | 
			
		||||
        self.task1 = AutomatedTask.objects.get(pk=self.task1.pk)
 | 
			
		||||
        self.assertEqual(self.task1.sync_status, "synced")
 | 
			
		||||
 | 
			
		||||
        nats_cmd.return_value = "timeout"
 | 
			
		||||
        ret = create_win_task_schedule.s(pk=self.task1.pk, pending_action=False).apply()
 | 
			
		||||
        ret = create_win_task_schedule.s(pk=self.task1.pk).apply()
 | 
			
		||||
        self.assertEqual(ret.status, "SUCCESS")
 | 
			
		||||
        self.task1 = AutomatedTask.objects.get(pk=self.task1.pk)
 | 
			
		||||
        self.assertEqual(self.task1.sync_status, "notsynced")
 | 
			
		||||
 | 
			
		||||
        # test pending action
 | 
			
		||||
        self.pending_action = PendingAction.objects.create(
 | 
			
		||||
            agent=self.agent, action_type="taskaction"
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(self.pending_action.status, "pending")
 | 
			
		||||
        nats_cmd.return_value = "ok"
 | 
			
		||||
        ret = create_win_task_schedule.s(
 | 
			
		||||
            pk=self.task1.pk, pending_action=self.pending_action.pk
 | 
			
		||||
        ).apply()
 | 
			
		||||
        self.assertEqual(ret.status, "SUCCESS")
 | 
			
		||||
        self.pending_action = PendingAction.objects.get(pk=self.pending_action.pk)
 | 
			
		||||
        self.assertEqual(self.pending_action.status, "completed")
 | 
			
		||||
        self.assertEqual(self.task1.sync_status, "initial")
 | 
			
		||||
 | 
			
		||||
        # test runonce with future date
 | 
			
		||||
        nats_cmd.reset_mock()
 | 
			
		||||
@@ -337,7 +323,7 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
 | 
			
		||||
            run_time_date=run_time_date,
 | 
			
		||||
        )
 | 
			
		||||
        nats_cmd.return_value = "ok"
 | 
			
		||||
        ret = create_win_task_schedule.s(pk=self.task2.pk, pending_action=False).apply()
 | 
			
		||||
        ret = create_win_task_schedule.s(pk=self.task2.pk).apply()
 | 
			
		||||
        nats_cmd.assert_called_with(
 | 
			
		||||
            {
 | 
			
		||||
                "func": "schedtask",
 | 
			
		||||
@@ -353,7 +339,7 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
 | 
			
		||||
                    "min": int(dt.datetime.strftime(self.task2.run_time_date, "%M")),
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
            timeout=10,
 | 
			
		||||
            timeout=5,
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(ret.status, "SUCCESS")
 | 
			
		||||
 | 
			
		||||
@@ -369,7 +355,7 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
 | 
			
		||||
            run_time_date=run_time_date,
 | 
			
		||||
        )
 | 
			
		||||
        nats_cmd.return_value = "ok"
 | 
			
		||||
        ret = create_win_task_schedule.s(pk=self.task3.pk, pending_action=False).apply()
 | 
			
		||||
        ret = create_win_task_schedule.s(pk=self.task3.pk).apply()
 | 
			
		||||
        self.task3 = AutomatedTask.objects.get(pk=self.task3.pk)
 | 
			
		||||
        self.assertEqual(ret.status, "SUCCESS")
 | 
			
		||||
 | 
			
		||||
@@ -385,7 +371,7 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
 | 
			
		||||
            assigned_check=self.check,
 | 
			
		||||
        )
 | 
			
		||||
        nats_cmd.return_value = "ok"
 | 
			
		||||
        ret = create_win_task_schedule.s(pk=self.task4.pk, pending_action=False).apply()
 | 
			
		||||
        ret = create_win_task_schedule.s(pk=self.task4.pk).apply()
 | 
			
		||||
        nats_cmd.assert_called_with(
 | 
			
		||||
            {
 | 
			
		||||
                "func": "schedtask",
 | 
			
		||||
@@ -396,7 +382,7 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
 | 
			
		||||
                    "name": task_name,
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
            timeout=10,
 | 
			
		||||
            timeout=5,
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(ret.status, "SUCCESS")
 | 
			
		||||
 | 
			
		||||
@@ -410,7 +396,7 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
 | 
			
		||||
            task_type="manual",
 | 
			
		||||
        )
 | 
			
		||||
        nats_cmd.return_value = "ok"
 | 
			
		||||
        ret = create_win_task_schedule.s(pk=self.task5.pk, pending_action=False).apply()
 | 
			
		||||
        ret = create_win_task_schedule.s(pk=self.task5.pk).apply()
 | 
			
		||||
        nats_cmd.assert_called_with(
 | 
			
		||||
            {
 | 
			
		||||
                "func": "schedtask",
 | 
			
		||||
@@ -421,6 +407,6 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
 | 
			
		||||
                    "name": task_name,
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
            timeout=10,
 | 
			
		||||
            timeout=5,
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(ret.status, "SUCCESS")
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
import asyncio
 | 
			
		||||
 | 
			
		||||
from django.shortcuts import get_object_or_404
 | 
			
		||||
from rest_framework.decorators import api_view
 | 
			
		||||
from rest_framework.decorators import api_view, permission_classes
 | 
			
		||||
from rest_framework.permissions import IsAuthenticated
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
from rest_framework.views import APIView
 | 
			
		||||
 | 
			
		||||
@@ -11,18 +10,17 @@ from scripts.models import Script
 | 
			
		||||
from tacticalrmm.utils import get_bit_days, get_default_timezone, notify_error
 | 
			
		||||
 | 
			
		||||
from .models import AutomatedTask
 | 
			
		||||
from .permissions import ManageAutoTaskPerms, RunAutoTaskPerms
 | 
			
		||||
from .serializers import AutoTaskSerializer, TaskSerializer
 | 
			
		||||
from .tasks import (
 | 
			
		||||
    create_win_task_schedule,
 | 
			
		||||
    delete_win_task_schedule,
 | 
			
		||||
    enable_or_disable_win_task,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AddAutoTask(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, ManageAutoTaskPerms]
 | 
			
		||||
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
        from automation.models import Policy
 | 
			
		||||
        from automation.tasks import generate_agent_tasks_from_policies_task
 | 
			
		||||
        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"])
 | 
			
		||||
@@ -47,7 +45,7 @@ class AddAutoTask(APIView):
 | 
			
		||||
        del data["autotask"]["run_time_days"]
 | 
			
		||||
        serializer = TaskSerializer(data=data["autotask"], partial=True, context=parent)
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        obj = serializer.save(
 | 
			
		||||
        task = serializer.save(
 | 
			
		||||
            **parent,
 | 
			
		||||
            script=script,
 | 
			
		||||
            win_task_name=AutomatedTask.generate_task_name(),
 | 
			
		||||
@@ -55,16 +53,18 @@ class AddAutoTask(APIView):
 | 
			
		||||
            run_time_bit_weekdays=bit_weekdays,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        if not "policy" in data:
 | 
			
		||||
            create_win_task_schedule.delay(pk=obj.pk)
 | 
			
		||||
        if task.agent:
 | 
			
		||||
            create_win_task_schedule.delay(pk=task.pk)
 | 
			
		||||
 | 
			
		||||
        if "policy" in data:
 | 
			
		||||
            generate_agent_tasks_from_policies_task.delay(data["policy"])
 | 
			
		||||
        elif task.policy:
 | 
			
		||||
            generate_agent_autotasks_task.delay(policy=task.policy.pk)
 | 
			
		||||
 | 
			
		||||
        return Response("Task will be created shortly!")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AutoTask(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, ManageAutoTaskPerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request, pk):
 | 
			
		||||
 | 
			
		||||
        agent = get_object_or_404(Agent, pk=pk)
 | 
			
		||||
@@ -75,7 +75,7 @@ class AutoTask(APIView):
 | 
			
		||||
        return Response(AutoTaskSerializer(agent, context=ctx).data)
 | 
			
		||||
 | 
			
		||||
    def put(self, request, pk):
 | 
			
		||||
        from automation.tasks import update_policy_task_fields_task
 | 
			
		||||
        from automation.tasks import update_policy_autotasks_fields_task
 | 
			
		||||
 | 
			
		||||
        task = get_object_or_404(AutomatedTask, pk=pk)
 | 
			
		||||
 | 
			
		||||
@@ -84,46 +84,54 @@ class AutoTask(APIView):
 | 
			
		||||
        serializer.save()
 | 
			
		||||
 | 
			
		||||
        if task.policy:
 | 
			
		||||
            update_policy_task_fields_task.delay(task.pk)
 | 
			
		||||
            update_policy_autotasks_fields_task.delay(task=task.pk)
 | 
			
		||||
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
    def patch(self, request, pk):
 | 
			
		||||
        from automation.tasks import update_policy_task_fields_task
 | 
			
		||||
        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"]
 | 
			
		||||
 | 
			
		||||
            if not task.policy:
 | 
			
		||||
                enable_or_disable_win_task.delay(pk=task.pk, action=action)
 | 
			
		||||
 | 
			
		||||
            else:
 | 
			
		||||
                update_policy_task_fields_task.delay(task.pk, update_agent=True)
 | 
			
		||||
 | 
			
		||||
            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")
 | 
			
		||||
 | 
			
		||||
    def delete(self, request, pk):
 | 
			
		||||
        from automation.tasks import delete_policy_autotask_task
 | 
			
		||||
        from automation.tasks import delete_policy_autotasks_task
 | 
			
		||||
        from autotasks.tasks import delete_win_task_schedule
 | 
			
		||||
 | 
			
		||||
        task = get_object_or_404(AutomatedTask, pk=pk)
 | 
			
		||||
 | 
			
		||||
        if not task.policy:
 | 
			
		||||
        if task.agent:
 | 
			
		||||
            delete_win_task_schedule.delay(pk=task.pk)
 | 
			
		||||
 | 
			
		||||
        if task.policy:
 | 
			
		||||
            delete_policy_autotask_task.delay(task.pk)
 | 
			
		||||
        elif task.policy:
 | 
			
		||||
            delete_policy_autotasks_task.delay(task=task.pk)
 | 
			
		||||
            task.delete()
 | 
			
		||||
 | 
			
		||||
        return Response(f"{task.name} will be deleted shortly")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view()
 | 
			
		||||
@permission_classes([IsAuthenticated, RunAutoTaskPerms])
 | 
			
		||||
def run_task(request, pk):
 | 
			
		||||
    from autotasks.tasks import run_win_task
 | 
			
		||||
 | 
			
		||||
    task = get_object_or_404(AutomatedTask, pk=pk)
 | 
			
		||||
    asyncio.run(task.agent.nats_cmd({"func": "runtask", "taskpk": task.pk}, wait=False))
 | 
			
		||||
    run_win_task.delay(pk=pk)
 | 
			
		||||
    return Response(f"{task.name} will now be run on {task.agent.hostname}")
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										22
									
								
								api/tacticalrmm/checks/migrations/0024_auto_20210606_1632.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								api/tacticalrmm/checks/migrations/0024_auto_20210606_1632.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
			
		||||
# Generated by Django 3.2.1 on 2021-06-06 16:32
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('checks', '0023_check_run_interval'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='checkhistory',
 | 
			
		||||
            name='check_history',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='checkhistory',
 | 
			
		||||
            name='check_id',
 | 
			
		||||
            field=models.PositiveIntegerField(default=0),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -1,4 +1,3 @@
 | 
			
		||||
import asyncio
 | 
			
		||||
import json
 | 
			
		||||
import os
 | 
			
		||||
import string
 | 
			
		||||
@@ -6,20 +5,14 @@ from statistics import mean
 | 
			
		||||
from typing import Any
 | 
			
		||||
 | 
			
		||||
import pytz
 | 
			
		||||
from alerts.models import SEVERITY_CHOICES
 | 
			
		||||
from core.models import CoreSettings
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.contrib.postgres.fields import ArrayField
 | 
			
		||||
from django.core.validators import MaxValueValidator, MinValueValidator
 | 
			
		||||
from django.db import models
 | 
			
		||||
from loguru import logger
 | 
			
		||||
 | 
			
		||||
from alerts.models import SEVERITY_CHOICES
 | 
			
		||||
from core.models import CoreSettings
 | 
			
		||||
from logs.models import BaseAuditModel
 | 
			
		||||
 | 
			
		||||
from .utils import bytes2human
 | 
			
		||||
 | 
			
		||||
logger.configure(**settings.LOG_CONFIG)
 | 
			
		||||
 | 
			
		||||
CHECK_TYPE_CHOICES = [
 | 
			
		||||
    ("diskspace", "Disk Space Check"),
 | 
			
		||||
    ("ping", "Ping Check"),
 | 
			
		||||
@@ -263,6 +256,42 @@ class Check(BaseAuditModel):
 | 
			
		||||
            "modified_time",
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def policy_fields_to_copy(self) -> list[str]:
 | 
			
		||||
        return [
 | 
			
		||||
            "warning_threshold",
 | 
			
		||||
            "error_threshold",
 | 
			
		||||
            "alert_severity",
 | 
			
		||||
            "name",
 | 
			
		||||
            "run_interval",
 | 
			
		||||
            "disk",
 | 
			
		||||
            "fails_b4_alert",
 | 
			
		||||
            "ip",
 | 
			
		||||
            "script",
 | 
			
		||||
            "script_args",
 | 
			
		||||
            "info_return_codes",
 | 
			
		||||
            "warning_return_codes",
 | 
			
		||||
            "timeout",
 | 
			
		||||
            "svc_name",
 | 
			
		||||
            "svc_display_name",
 | 
			
		||||
            "svc_policy_mode",
 | 
			
		||||
            "pass_if_start_pending",
 | 
			
		||||
            "pass_if_svc_not_exist",
 | 
			
		||||
            "restart_if_stopped",
 | 
			
		||||
            "log_name",
 | 
			
		||||
            "event_id",
 | 
			
		||||
            "event_id_is_wildcard",
 | 
			
		||||
            "event_type",
 | 
			
		||||
            "event_source",
 | 
			
		||||
            "event_message",
 | 
			
		||||
            "fail_when",
 | 
			
		||||
            "search_last_days",
 | 
			
		||||
            "number_of_events_b4_alert",
 | 
			
		||||
            "email_alert",
 | 
			
		||||
            "text_alert",
 | 
			
		||||
            "dashboard_alert",
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
    def should_create_alert(self, alert_template=None):
 | 
			
		||||
 | 
			
		||||
        return (
 | 
			
		||||
@@ -280,9 +309,9 @@ class Check(BaseAuditModel):
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def add_check_history(self, value: int, more_info: Any = None) -> None:
 | 
			
		||||
        CheckHistory.objects.create(check_history=self, y=value, results=more_info)
 | 
			
		||||
        CheckHistory.objects.create(check_id=self.pk, y=value, results=more_info)
 | 
			
		||||
 | 
			
		||||
    def handle_checkv2(self, data):
 | 
			
		||||
    def handle_check(self, data):
 | 
			
		||||
        from alerts.models import Alert
 | 
			
		||||
 | 
			
		||||
        # cpuload or mem checks
 | 
			
		||||
@@ -313,9 +342,6 @@ class Check(BaseAuditModel):
 | 
			
		||||
        elif self.check_type == "diskspace":
 | 
			
		||||
            if data["exists"]:
 | 
			
		||||
                percent_used = round(data["percent_used"])
 | 
			
		||||
                total = bytes2human(data["total"])
 | 
			
		||||
                free = bytes2human(data["free"])
 | 
			
		||||
 | 
			
		||||
                if self.error_threshold and (100 - percent_used) < self.error_threshold:
 | 
			
		||||
                    self.status = "failing"
 | 
			
		||||
                    self.alert_severity = "error"
 | 
			
		||||
@@ -329,7 +355,7 @@ class Check(BaseAuditModel):
 | 
			
		||||
                else:
 | 
			
		||||
                    self.status = "passing"
 | 
			
		||||
 | 
			
		||||
                self.more_info = f"Total: {total}B, Free: {free}B"
 | 
			
		||||
                self.more_info = data["more_info"]
 | 
			
		||||
 | 
			
		||||
                # add check history
 | 
			
		||||
                self.add_check_history(100 - percent_used)
 | 
			
		||||
@@ -345,12 +371,7 @@ class Check(BaseAuditModel):
 | 
			
		||||
            self.stdout = data["stdout"]
 | 
			
		||||
            self.stderr = data["stderr"]
 | 
			
		||||
            self.retcode = data["retcode"]
 | 
			
		||||
            try:
 | 
			
		||||
                # python agent
 | 
			
		||||
                self.execution_time = "{:.4f}".format(data["stop"] - data["start"])
 | 
			
		||||
            except:
 | 
			
		||||
                # golang agent
 | 
			
		||||
                self.execution_time = "{:.4f}".format(data["runtime"])
 | 
			
		||||
            self.execution_time = "{:.4f}".format(data["runtime"])
 | 
			
		||||
 | 
			
		||||
            if data["retcode"] in self.info_return_codes:
 | 
			
		||||
                self.alert_severity = "info"
 | 
			
		||||
@@ -386,18 +407,8 @@ class Check(BaseAuditModel):
 | 
			
		||||
 | 
			
		||||
        # ping checks
 | 
			
		||||
        elif self.check_type == "ping":
 | 
			
		||||
            success = ["Reply", "bytes", "time", "TTL"]
 | 
			
		||||
            output = data["output"]
 | 
			
		||||
 | 
			
		||||
            if data["has_stdout"]:
 | 
			
		||||
                if all(x in output for x in success):
 | 
			
		||||
                    self.status = "passing"
 | 
			
		||||
                else:
 | 
			
		||||
                    self.status = "failing"
 | 
			
		||||
            elif data["has_stderr"]:
 | 
			
		||||
                self.status = "failing"
 | 
			
		||||
 | 
			
		||||
            self.more_info = output
 | 
			
		||||
            self.status = data["status"]
 | 
			
		||||
            self.more_info = data["output"]
 | 
			
		||||
            self.save(update_fields=["more_info"])
 | 
			
		||||
 | 
			
		||||
            self.add_check_history(
 | 
			
		||||
@@ -406,41 +417,8 @@ class Check(BaseAuditModel):
 | 
			
		||||
 | 
			
		||||
        # windows service checks
 | 
			
		||||
        elif self.check_type == "winsvc":
 | 
			
		||||
            svc_stat = data["status"]
 | 
			
		||||
            self.more_info = f"Status {svc_stat.upper()}"
 | 
			
		||||
 | 
			
		||||
            if data["exists"]:
 | 
			
		||||
                if svc_stat == "running":
 | 
			
		||||
                    self.status = "passing"
 | 
			
		||||
                elif svc_stat == "start_pending" and self.pass_if_start_pending:
 | 
			
		||||
                    self.status = "passing"
 | 
			
		||||
                else:
 | 
			
		||||
                    if self.agent and self.restart_if_stopped:
 | 
			
		||||
                        nats_data = {
 | 
			
		||||
                            "func": "winsvcaction",
 | 
			
		||||
                            "payload": {"name": self.svc_name, "action": "start"},
 | 
			
		||||
                        }
 | 
			
		||||
                        r = asyncio.run(self.agent.nats_cmd(nats_data, timeout=32))
 | 
			
		||||
                        if r == "timeout" or r == "natsdown":
 | 
			
		||||
                            self.status = "failing"
 | 
			
		||||
                        elif not r["success"] and r["errormsg"]:
 | 
			
		||||
                            self.status = "failing"
 | 
			
		||||
                        elif r["success"]:
 | 
			
		||||
                            self.status = "passing"
 | 
			
		||||
                            self.more_info = f"Status RUNNING"
 | 
			
		||||
                        else:
 | 
			
		||||
                            self.status = "failing"
 | 
			
		||||
                    else:
 | 
			
		||||
                        self.status = "failing"
 | 
			
		||||
 | 
			
		||||
            else:
 | 
			
		||||
                if self.pass_if_svc_not_exist:
 | 
			
		||||
                    self.status = "passing"
 | 
			
		||||
                else:
 | 
			
		||||
                    self.status = "failing"
 | 
			
		||||
 | 
			
		||||
                self.more_info = f"Service {self.svc_name} does not exist"
 | 
			
		||||
 | 
			
		||||
            self.status = data["status"]
 | 
			
		||||
            self.more_info = data["more_info"]
 | 
			
		||||
            self.save(update_fields=["more_info"])
 | 
			
		||||
 | 
			
		||||
            self.add_check_history(
 | 
			
		||||
@@ -448,49 +426,7 @@ class Check(BaseAuditModel):
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        elif self.check_type == "eventlog":
 | 
			
		||||
            log = []
 | 
			
		||||
            is_wildcard = self.event_id_is_wildcard
 | 
			
		||||
            eventType = self.event_type
 | 
			
		||||
            eventID = self.event_id
 | 
			
		||||
            source = self.event_source
 | 
			
		||||
            message = self.event_message
 | 
			
		||||
            r = data["log"]
 | 
			
		||||
 | 
			
		||||
            for i in r:
 | 
			
		||||
                if i["eventType"] == eventType:
 | 
			
		||||
                    if not is_wildcard and not int(i["eventID"]) == eventID:
 | 
			
		||||
                        continue
 | 
			
		||||
 | 
			
		||||
                    if not source and not message:
 | 
			
		||||
                        if is_wildcard:
 | 
			
		||||
                            log.append(i)
 | 
			
		||||
                        elif int(i["eventID"]) == eventID:
 | 
			
		||||
                            log.append(i)
 | 
			
		||||
                        continue
 | 
			
		||||
 | 
			
		||||
                    if source and message:
 | 
			
		||||
                        if is_wildcard:
 | 
			
		||||
                            if source in i["source"] and message in i["message"]:
 | 
			
		||||
                                log.append(i)
 | 
			
		||||
 | 
			
		||||
                        elif int(i["eventID"]) == eventID:
 | 
			
		||||
                            if source in i["source"] and message in i["message"]:
 | 
			
		||||
                                log.append(i)
 | 
			
		||||
 | 
			
		||||
                        continue
 | 
			
		||||
 | 
			
		||||
                    if source and source in i["source"]:
 | 
			
		||||
                        if is_wildcard:
 | 
			
		||||
                            log.append(i)
 | 
			
		||||
                        elif int(i["eventID"]) == eventID:
 | 
			
		||||
                            log.append(i)
 | 
			
		||||
 | 
			
		||||
                    if message and message in i["message"]:
 | 
			
		||||
                        if is_wildcard:
 | 
			
		||||
                            log.append(i)
 | 
			
		||||
                        elif int(i["eventID"]) == eventID:
 | 
			
		||||
                            log.append(i)
 | 
			
		||||
 | 
			
		||||
            log = data["log"]
 | 
			
		||||
            if self.fail_when == "contains":
 | 
			
		||||
                if log and len(log) >= self.number_of_events_b4_alert:
 | 
			
		||||
                    self.status = "failing"
 | 
			
		||||
@@ -527,12 +463,17 @@ class Check(BaseAuditModel):
 | 
			
		||||
 | 
			
		||||
        return self.status
 | 
			
		||||
 | 
			
		||||
    def handle_assigned_task(self) -> None:
 | 
			
		||||
        for task in self.assignedtask.all():  # type: ignore
 | 
			
		||||
            if task.enabled:
 | 
			
		||||
                task.run_win_task()
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def serialize(check):
 | 
			
		||||
        # serializes the check and returns json
 | 
			
		||||
        from .serializers import CheckSerializer
 | 
			
		||||
        from .serializers import CheckAuditSerializer
 | 
			
		||||
 | 
			
		||||
        return CheckSerializer(check).data
 | 
			
		||||
        return CheckAuditSerializer(check).data
 | 
			
		||||
 | 
			
		||||
    # for policy diskchecks
 | 
			
		||||
    @staticmethod
 | 
			
		||||
@@ -551,49 +492,31 @@ class Check(BaseAuditModel):
 | 
			
		||||
 | 
			
		||||
    def create_policy_check(self, agent=None, policy=None):
 | 
			
		||||
 | 
			
		||||
        if not agent and not policy or agent and policy:
 | 
			
		||||
        if (not agent and not policy) or (agent and policy):
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        Check.objects.create(
 | 
			
		||||
        check = Check.objects.create(
 | 
			
		||||
            agent=agent,
 | 
			
		||||
            policy=policy,
 | 
			
		||||
            managed_by_policy=bool(agent),
 | 
			
		||||
            parent_check=(self.pk if agent else None),
 | 
			
		||||
            name=self.name,
 | 
			
		||||
            alert_severity=self.alert_severity,
 | 
			
		||||
            check_type=self.check_type,
 | 
			
		||||
            email_alert=self.email_alert,
 | 
			
		||||
            dashboard_alert=self.dashboard_alert,
 | 
			
		||||
            text_alert=self.text_alert,
 | 
			
		||||
            fails_b4_alert=self.fails_b4_alert,
 | 
			
		||||
            extra_details=self.extra_details,
 | 
			
		||||
            run_interval=self.run_interval,
 | 
			
		||||
            error_threshold=self.error_threshold,
 | 
			
		||||
            warning_threshold=self.warning_threshold,
 | 
			
		||||
            disk=self.disk,
 | 
			
		||||
            ip=self.ip,
 | 
			
		||||
            script=self.script,
 | 
			
		||||
            script_args=self.script_args,
 | 
			
		||||
            timeout=self.timeout,
 | 
			
		||||
            info_return_codes=self.info_return_codes,
 | 
			
		||||
            warning_return_codes=self.warning_return_codes,
 | 
			
		||||
            svc_name=self.svc_name,
 | 
			
		||||
            svc_display_name=self.svc_display_name,
 | 
			
		||||
            pass_if_start_pending=self.pass_if_start_pending,
 | 
			
		||||
            pass_if_svc_not_exist=self.pass_if_svc_not_exist,
 | 
			
		||||
            restart_if_stopped=self.restart_if_stopped,
 | 
			
		||||
            svc_policy_mode=self.svc_policy_mode,
 | 
			
		||||
            log_name=self.log_name,
 | 
			
		||||
            event_id=self.event_id,
 | 
			
		||||
            event_id_is_wildcard=self.event_id_is_wildcard,
 | 
			
		||||
            event_type=self.event_type,
 | 
			
		||||
            event_source=self.event_source,
 | 
			
		||||
            event_message=self.event_message,
 | 
			
		||||
            fail_when=self.fail_when,
 | 
			
		||||
            search_last_days=self.search_last_days,
 | 
			
		||||
            number_of_events_b4_alert=self.number_of_events_b4_alert,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        for task in self.assignedtask.all():  # type: ignore
 | 
			
		||||
            if policy or (
 | 
			
		||||
                agent and not agent.autotasks.filter(parent_task=task.pk).exists()
 | 
			
		||||
            ):
 | 
			
		||||
                task.create_policy_task(
 | 
			
		||||
                    agent=agent, policy=policy, assigned_check=check
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
        for field in self.policy_fields_to_copy:
 | 
			
		||||
            setattr(check, field, getattr(self, field))
 | 
			
		||||
 | 
			
		||||
        check.save()
 | 
			
		||||
 | 
			
		||||
    def is_duplicate(self, check):
 | 
			
		||||
        if self.check_type == "diskspace":
 | 
			
		||||
            return self.disk == check.disk
 | 
			
		||||
@@ -633,12 +556,15 @@ class Check(BaseAuditModel):
 | 
			
		||||
            if self.error_threshold:
 | 
			
		||||
                text += f" Error Threshold: {self.error_threshold}%"
 | 
			
		||||
 | 
			
		||||
            percent_used = [
 | 
			
		||||
                d["percent"] for d in self.agent.disks if d["device"] == self.disk
 | 
			
		||||
            ][0]
 | 
			
		||||
            percent_free = 100 - percent_used
 | 
			
		||||
            try:
 | 
			
		||||
                percent_used = [
 | 
			
		||||
                    d["percent"] for d in self.agent.disks if d["device"] == self.disk
 | 
			
		||||
                ][0]
 | 
			
		||||
                percent_free = 100 - percent_used
 | 
			
		||||
 | 
			
		||||
            body = subject + f" - Free: {percent_free}%, {text}"
 | 
			
		||||
                body = subject + f" - Free: {percent_free}%, {text}"
 | 
			
		||||
            except:
 | 
			
		||||
                body = subject + f" - Disk {self.disk} does not exist"
 | 
			
		||||
 | 
			
		||||
        elif self.check_type == "script":
 | 
			
		||||
 | 
			
		||||
@@ -667,16 +593,7 @@ class Check(BaseAuditModel):
 | 
			
		||||
                body = subject + f" - Average memory usage: {avg}%, {text}"
 | 
			
		||||
 | 
			
		||||
        elif self.check_type == "winsvc":
 | 
			
		||||
 | 
			
		||||
            try:
 | 
			
		||||
                status = list(
 | 
			
		||||
                    filter(lambda x: x["name"] == self.svc_name, self.agent.services)
 | 
			
		||||
                )[0]["status"]
 | 
			
		||||
            # catch services that don't exist if policy check
 | 
			
		||||
            except:
 | 
			
		||||
                status = "Unknown"
 | 
			
		||||
 | 
			
		||||
            body = subject + f" - Status: {status.upper()}"
 | 
			
		||||
            body = subject + f" - Status: {self.more_info}"
 | 
			
		||||
 | 
			
		||||
        elif self.check_type == "eventlog":
 | 
			
		||||
 | 
			
		||||
@@ -719,11 +636,15 @@ class Check(BaseAuditModel):
 | 
			
		||||
            if self.error_threshold:
 | 
			
		||||
                text += f" Error Threshold: {self.error_threshold}%"
 | 
			
		||||
 | 
			
		||||
            percent_used = [
 | 
			
		||||
                d["percent"] for d in self.agent.disks if d["device"] == self.disk
 | 
			
		||||
            ][0]
 | 
			
		||||
            percent_free = 100 - percent_used
 | 
			
		||||
            body = subject + f" - Free: {percent_free}%, {text}"
 | 
			
		||||
            try:
 | 
			
		||||
                percent_used = [
 | 
			
		||||
                    d["percent"] for d in self.agent.disks if d["device"] == self.disk
 | 
			
		||||
                ][0]
 | 
			
		||||
                percent_free = 100 - percent_used
 | 
			
		||||
                body = subject + f" - Free: {percent_free}%, {text}"
 | 
			
		||||
            except:
 | 
			
		||||
                body = subject + f" - Disk {self.disk} does not exist"
 | 
			
		||||
 | 
			
		||||
        elif self.check_type == "script":
 | 
			
		||||
            body = subject + f" - Return code: {self.retcode}"
 | 
			
		||||
        elif self.check_type == "ping":
 | 
			
		||||
@@ -741,10 +662,7 @@ class Check(BaseAuditModel):
 | 
			
		||||
            elif self.check_type == "memory":
 | 
			
		||||
                body = subject + f" - Average memory usage: {avg}%, {text}"
 | 
			
		||||
        elif self.check_type == "winsvc":
 | 
			
		||||
            status = list(
 | 
			
		||||
                filter(lambda x: x["name"] == self.svc_name, self.agent.services)
 | 
			
		||||
            )[0]["status"]
 | 
			
		||||
            body = subject + f" - Status: {status.upper()}"
 | 
			
		||||
            body = subject + f" - Status: {self.more_info}"
 | 
			
		||||
        elif self.check_type == "eventlog":
 | 
			
		||||
            body = subject
 | 
			
		||||
 | 
			
		||||
@@ -766,14 +684,10 @@ class Check(BaseAuditModel):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CheckHistory(models.Model):
 | 
			
		||||
    check_history = models.ForeignKey(
 | 
			
		||||
        Check,
 | 
			
		||||
        related_name="check_history",
 | 
			
		||||
        on_delete=models.CASCADE,
 | 
			
		||||
    )
 | 
			
		||||
    check_id = models.PositiveIntegerField(default=0)
 | 
			
		||||
    x = models.DateTimeField(auto_now_add=True)
 | 
			
		||||
    y = models.PositiveIntegerField(null=True, blank=True, default=None)
 | 
			
		||||
    results = models.JSONField(null=True, blank=True)
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return self.check_history.readable_desc
 | 
			
		||||
        return self.x
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										16
									
								
								api/tacticalrmm/checks/permissions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								api/tacticalrmm/checks/permissions.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
			
		||||
from rest_framework import permissions
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.permissions import _has_perm
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ManageChecksPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        if r.method == "GET":
 | 
			
		||||
            return True
 | 
			
		||||
 | 
			
		||||
        return _has_perm(r, "can_manage_checks")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RunChecksPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        return _has_perm(r, "can_run_checks")
 | 
			
		||||
@@ -6,6 +6,7 @@ from autotasks.models import AutomatedTask
 | 
			
		||||
from scripts.serializers import ScriptCheckSerializer, ScriptSerializer
 | 
			
		||||
 | 
			
		||||
from .models import Check, CheckHistory
 | 
			
		||||
from scripts.models import Script
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AssignedTaskField(serializers.ModelSerializer):
 | 
			
		||||
@@ -158,13 +159,16 @@ class AssignedTaskCheckRunnerField(serializers.ModelSerializer):
 | 
			
		||||
 | 
			
		||||
class CheckRunnerGetSerializer(serializers.ModelSerializer):
 | 
			
		||||
    # only send data needed for agent to run a check
 | 
			
		||||
    assigned_tasks = serializers.SerializerMethodField()
 | 
			
		||||
    script = ScriptCheckSerializer(read_only=True)
 | 
			
		||||
    script_args = serializers.SerializerMethodField()
 | 
			
		||||
 | 
			
		||||
    def get_assigned_tasks(self, obj):
 | 
			
		||||
        if obj.assignedtask.exists():
 | 
			
		||||
            tasks = obj.assignedtask.all()
 | 
			
		||||
            return AssignedTaskCheckRunnerField(tasks, many=True).data
 | 
			
		||||
    def get_script_args(self, obj):
 | 
			
		||||
        if obj.check_type != "script":
 | 
			
		||||
            return []
 | 
			
		||||
 | 
			
		||||
        return Script.parse_script_args(
 | 
			
		||||
            agent=obj.agent, shell=obj.script.shell, args=obj.script_args
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Check
 | 
			
		||||
@@ -193,6 +197,7 @@ class CheckRunnerGetSerializer(serializers.ModelSerializer):
 | 
			
		||||
            "modified_by",
 | 
			
		||||
            "modified_time",
 | 
			
		||||
            "history",
 | 
			
		||||
            "dashboard_alert",
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -215,3 +220,9 @@ class CheckHistorySerializer(serializers.ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = CheckHistory
 | 
			
		||||
        fields = ("x", "y", "results")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CheckAuditSerializer(serializers.ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Check
 | 
			
		||||
        fields = "__all__"
 | 
			
		||||
 
 | 
			
		||||
@@ -14,6 +14,22 @@ class TestCheckViews(TacticalTestCase):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
        self.setup_coresettings()
 | 
			
		||||
 | 
			
		||||
    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")
 | 
			
		||||
        self.assertEqual(resp.status_code, 404)
 | 
			
		||||
 | 
			
		||||
        url = f"/checks/{check.pk}/check/"
 | 
			
		||||
 | 
			
		||||
        resp = self.client.delete(url, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertFalse(agent.agentchecks.all())
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("delete", url)
 | 
			
		||||
 | 
			
		||||
    def test_get_disk_check(self):
 | 
			
		||||
        # setup data
 | 
			
		||||
        disk_check = baker.make_recipe("checks.diskspace_check")
 | 
			
		||||
@@ -347,10 +363,10 @@ class TestCheckViews(TacticalTestCase):
 | 
			
		||||
        # setup data
 | 
			
		||||
        agent = baker.make_recipe("agents.agent")
 | 
			
		||||
        check = baker.make_recipe("checks.diskspace_check", agent=agent)
 | 
			
		||||
        baker.make("checks.CheckHistory", check_history=check, _quantity=30)
 | 
			
		||||
        baker.make("checks.CheckHistory", check_id=check.id, _quantity=30)
 | 
			
		||||
        check_history_data = baker.make(
 | 
			
		||||
            "checks.CheckHistory",
 | 
			
		||||
            check_history=check,
 | 
			
		||||
            check_id=check.id,
 | 
			
		||||
            _quantity=30,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@@ -384,17 +400,17 @@ class TestCheckTasks(TacticalTestCase):
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
        self.setup_coresettings()
 | 
			
		||||
        self.agent = baker.make_recipe("agents.agent")
 | 
			
		||||
        self.agent = baker.make_recipe("agents.agent", version="1.5.7")
 | 
			
		||||
 | 
			
		||||
    def test_prune_check_history(self):
 | 
			
		||||
        from .tasks import prune_check_history
 | 
			
		||||
 | 
			
		||||
        # setup data
 | 
			
		||||
        check = baker.make_recipe("checks.diskspace_check")
 | 
			
		||||
        baker.make("checks.CheckHistory", check_history=check, _quantity=30)
 | 
			
		||||
        baker.make("checks.CheckHistory", check_id=check.id, _quantity=30)
 | 
			
		||||
        check_history_data = baker.make(
 | 
			
		||||
            "checks.CheckHistory",
 | 
			
		||||
            check_history=check,
 | 
			
		||||
            check_id=check.id,
 | 
			
		||||
            _quantity=30,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@@ -510,6 +526,7 @@ class TestCheckTasks(TacticalTestCase):
 | 
			
		||||
            "percent_used": 85,
 | 
			
		||||
            "total": 500,
 | 
			
		||||
            "free": 400,
 | 
			
		||||
            "more_info": "More info",
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
@@ -527,6 +544,7 @@ class TestCheckTasks(TacticalTestCase):
 | 
			
		||||
            "percent_used": 95,
 | 
			
		||||
            "total": 500,
 | 
			
		||||
            "free": 400,
 | 
			
		||||
            "more_info": "More info",
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
@@ -557,6 +575,7 @@ class TestCheckTasks(TacticalTestCase):
 | 
			
		||||
            "percent_used": 95,
 | 
			
		||||
            "total": 500,
 | 
			
		||||
            "free": 400,
 | 
			
		||||
            "more_info": "More info",
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
@@ -576,6 +595,7 @@ class TestCheckTasks(TacticalTestCase):
 | 
			
		||||
            "percent_used": 95,
 | 
			
		||||
            "total": 500,
 | 
			
		||||
            "free": 400,
 | 
			
		||||
            "more_info": "More info",
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
@@ -592,6 +612,7 @@ class TestCheckTasks(TacticalTestCase):
 | 
			
		||||
            "percent_used": 50,
 | 
			
		||||
            "total": 500,
 | 
			
		||||
            "free": 400,
 | 
			
		||||
            "more_info": "More info",
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
@@ -775,12 +796,7 @@ class TestCheckTasks(TacticalTestCase):
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # test failing info
 | 
			
		||||
        data = {
 | 
			
		||||
            "id": ping.id,
 | 
			
		||||
            "output": "Reply from 192.168.1.27: Destination host unreachable",
 | 
			
		||||
            "has_stdout": True,
 | 
			
		||||
            "has_stderr": False,
 | 
			
		||||
        }
 | 
			
		||||
        data = {"id": ping.id, "status": "failing", "output": "reply from a.com"}
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
@@ -790,13 +806,6 @@ class TestCheckTasks(TacticalTestCase):
 | 
			
		||||
        self.assertEqual(new_check.alert_severity, "info")
 | 
			
		||||
 | 
			
		||||
        # test failing warning
 | 
			
		||||
        data = {
 | 
			
		||||
            "id": ping.id,
 | 
			
		||||
            "output": "Reply from 192.168.1.27: Destination host unreachable",
 | 
			
		||||
            "has_stdout": True,
 | 
			
		||||
            "has_stderr": False,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        ping.alert_severity = "warning"
 | 
			
		||||
        ping.save()
 | 
			
		||||
 | 
			
		||||
@@ -808,13 +817,6 @@ class TestCheckTasks(TacticalTestCase):
 | 
			
		||||
        self.assertEqual(new_check.alert_severity, "warning")
 | 
			
		||||
 | 
			
		||||
        # test failing error
 | 
			
		||||
        data = {
 | 
			
		||||
            "id": ping.id,
 | 
			
		||||
            "output": "Reply from 192.168.1.27: Destination host unreachable",
 | 
			
		||||
            "has_stdout": True,
 | 
			
		||||
            "has_stderr": False,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        ping.alert_severity = "error"
 | 
			
		||||
        ping.save()
 | 
			
		||||
 | 
			
		||||
@@ -826,13 +828,6 @@ class TestCheckTasks(TacticalTestCase):
 | 
			
		||||
        self.assertEqual(new_check.alert_severity, "error")
 | 
			
		||||
 | 
			
		||||
        # test failing error
 | 
			
		||||
        data = {
 | 
			
		||||
            "id": ping.id,
 | 
			
		||||
            "output": "some output",
 | 
			
		||||
            "has_stdout": False,
 | 
			
		||||
            "has_stderr": True,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
@@ -841,12 +836,7 @@ class TestCheckTasks(TacticalTestCase):
 | 
			
		||||
        self.assertEqual(new_check.alert_severity, "error")
 | 
			
		||||
 | 
			
		||||
        # test passing
 | 
			
		||||
        data = {
 | 
			
		||||
            "id": ping.id,
 | 
			
		||||
            "output": "Reply from 192.168.1.1: bytes=32 time<1ms TTL=64",
 | 
			
		||||
            "has_stdout": True,
 | 
			
		||||
            "has_stderr": False,
 | 
			
		||||
        }
 | 
			
		||||
        data = {"id": ping.id, "status": "passing", "output": "reply from a.com"}
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
@@ -865,7 +855,7 @@ class TestCheckTasks(TacticalTestCase):
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # test passing running
 | 
			
		||||
        data = {"id": winsvc.id, "exists": True, "status": "running"}
 | 
			
		||||
        data = {"id": winsvc.id, "status": "passing", "more_info": "ok"}
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
@@ -873,20 +863,8 @@ class TestCheckTasks(TacticalTestCase):
 | 
			
		||||
        new_check = Check.objects.get(pk=winsvc.id)
 | 
			
		||||
        self.assertEqual(new_check.status, "passing")
 | 
			
		||||
 | 
			
		||||
        # test passing start pending
 | 
			
		||||
        winsvc.pass_if_start_pending = True
 | 
			
		||||
        winsvc.save()
 | 
			
		||||
 | 
			
		||||
        data = {"id": winsvc.id, "exists": True, "status": "start_pending"}
 | 
			
		||||
 | 
			
		||||
        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")
 | 
			
		||||
 | 
			
		||||
        # test failing no start
 | 
			
		||||
        data = {"id": winsvc.id, "exists": True, "status": "not running"}
 | 
			
		||||
        # test failing
 | 
			
		||||
        data = {"id": winsvc.id, "status": "failing", "more_info": "ok"}
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
@@ -895,7 +873,7 @@ class TestCheckTasks(TacticalTestCase):
 | 
			
		||||
        self.assertEqual(new_check.status, "failing")
 | 
			
		||||
        self.assertEqual(new_check.alert_severity, "info")
 | 
			
		||||
 | 
			
		||||
        # test failing and attempt start
 | 
			
		||||
        """ # test failing and attempt start
 | 
			
		||||
        winsvc.restart_if_stopped = True
 | 
			
		||||
        winsvc.alert_severity = "warning"
 | 
			
		||||
        winsvc.save()
 | 
			
		||||
@@ -960,9 +938,9 @@ class TestCheckTasks(TacticalTestCase):
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=winsvc.id)
 | 
			
		||||
        self.assertEqual(new_check.status, "passing")
 | 
			
		||||
        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/"
 | 
			
		||||
@@ -1164,4 +1142,4 @@ class TestCheckTasks(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=eventlog.id)
 | 
			
		||||
 | 
			
		||||
        self.assertEquals(new_check.status, "passing")
 | 
			
		||||
        self.assertEquals(new_check.status, "passing") """
 | 
			
		||||
 
 | 
			
		||||
@@ -8,5 +8,5 @@ urlpatterns = [
 | 
			
		||||
    path("<pk>/loadchecks/", views.load_checks),
 | 
			
		||||
    path("getalldisks/", views.get_disks_for_policies),
 | 
			
		||||
    path("runchecks/<pk>/", views.run_checks),
 | 
			
		||||
    path("history/<int:checkpk>/", views.CheckHistory.as_view()),
 | 
			
		||||
    path("history/<int:checkpk>/", views.GetCheckHistory.as_view()),
 | 
			
		||||
]
 | 
			
		||||
 
 | 
			
		||||
@@ -5,26 +5,27 @@ from django.db.models import Q
 | 
			
		||||
from django.shortcuts import get_object_or_404
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from packaging import version as pyver
 | 
			
		||||
from rest_framework.decorators import api_view
 | 
			
		||||
from rest_framework.decorators import api_view, permission_classes
 | 
			
		||||
from rest_framework.permissions import IsAuthenticated
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
from rest_framework.views import APIView
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from automation.models import Policy
 | 
			
		||||
from automation.tasks import (
 | 
			
		||||
    delete_policy_check_task,
 | 
			
		||||
    generate_agent_checks_from_policies_task,
 | 
			
		||||
    update_policy_check_fields_task,
 | 
			
		||||
)
 | 
			
		||||
from scripts.models import Script
 | 
			
		||||
from tacticalrmm.utils import notify_error
 | 
			
		||||
 | 
			
		||||
from .models import Check
 | 
			
		||||
from .models import Check, CheckHistory
 | 
			
		||||
from .permissions import ManageChecksPerms, RunChecksPerms
 | 
			
		||||
from .serializers import CheckHistorySerializer, CheckSerializer
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AddCheck(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, ManageChecksPerms]
 | 
			
		||||
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
        from automation.tasks import generate_agent_checks_task
 | 
			
		||||
 | 
			
		||||
        policy = None
 | 
			
		||||
        agent = None
 | 
			
		||||
 | 
			
		||||
@@ -53,40 +54,49 @@ class AddCheck(APIView):
 | 
			
		||||
            data=request.data["check"], partial=True, context=parent
 | 
			
		||||
        )
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        obj = serializer.save(**parent, script=script)
 | 
			
		||||
        new_check = serializer.save(**parent, script=script)
 | 
			
		||||
 | 
			
		||||
        # Generate policy Checks
 | 
			
		||||
        if policy:
 | 
			
		||||
            generate_agent_checks_from_policies_task.delay(policypk=policy.pk)
 | 
			
		||||
            generate_agent_checks_task.delay(policy=policy.pk)
 | 
			
		||||
        elif agent:
 | 
			
		||||
            checks = agent.agentchecks.filter(  # type: ignore
 | 
			
		||||
                check_type=obj.check_type, managed_by_policy=True
 | 
			
		||||
                check_type=new_check.check_type, managed_by_policy=True
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            # Should only be one
 | 
			
		||||
            duplicate_check = [check for check in checks if check.is_duplicate(obj)]
 | 
			
		||||
            duplicate_check = [
 | 
			
		||||
                check for check in checks if check.is_duplicate(new_check)
 | 
			
		||||
            ]
 | 
			
		||||
 | 
			
		||||
            if duplicate_check:
 | 
			
		||||
                policy = Check.objects.get(pk=duplicate_check[0].parent_check).policy
 | 
			
		||||
                if policy.enforced:
 | 
			
		||||
                    obj.overriden_by_policy = True
 | 
			
		||||
                    obj.save()
 | 
			
		||||
                    new_check.overriden_by_policy = True
 | 
			
		||||
                    new_check.save()
 | 
			
		||||
                else:
 | 
			
		||||
                    duplicate_check[0].delete()
 | 
			
		||||
 | 
			
		||||
        return Response(f"{obj.readable_desc} was added!")
 | 
			
		||||
        return Response(f"{new_check.readable_desc} was added!")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetUpdateDeleteCheck(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, ManageChecksPerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request, pk):
 | 
			
		||||
        check = get_object_or_404(Check, pk=pk)
 | 
			
		||||
        return Response(CheckSerializer(check).data)
 | 
			
		||||
 | 
			
		||||
    def patch(self, request, pk):
 | 
			
		||||
        from automation.tasks import update_policy_check_fields_task
 | 
			
		||||
 | 
			
		||||
        check = get_object_or_404(Check, pk=pk)
 | 
			
		||||
 | 
			
		||||
        # remove fields that should not be changed when editing a check from the frontend
 | 
			
		||||
        if "check_alert" not in request.data.keys():
 | 
			
		||||
        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]
 | 
			
		||||
 | 
			
		||||
        # set event id to 0 if wildcard because it needs to be an integer field for db
 | 
			
		||||
@@ -102,31 +112,32 @@ class GetUpdateDeleteCheck(APIView):
 | 
			
		||||
 | 
			
		||||
        serializer = CheckSerializer(instance=check, data=request.data, partial=True)
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        obj = serializer.save()
 | 
			
		||||
        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()
 | 
			
		||||
 | 
			
		||||
        # Update policy check fields
 | 
			
		||||
        if check.policy:
 | 
			
		||||
            update_policy_check_fields_task(checkpk=pk)
 | 
			
		||||
            update_policy_check_fields_task.delay(check=check.pk)
 | 
			
		||||
 | 
			
		||||
        return Response(f"{obj.readable_desc} was edited!")
 | 
			
		||||
        return Response(f"{check.readable_desc} was edited!")
 | 
			
		||||
 | 
			
		||||
    def delete(self, request, pk):
 | 
			
		||||
        check = get_object_or_404(Check, pk=pk)
 | 
			
		||||
        from automation.tasks import generate_agent_checks_task
 | 
			
		||||
 | 
			
		||||
        check_pk = check.pk
 | 
			
		||||
        policy_pk = None
 | 
			
		||||
        if check.policy:
 | 
			
		||||
            policy_pk = check.policy.pk
 | 
			
		||||
        check = get_object_or_404(Check, pk=pk)
 | 
			
		||||
 | 
			
		||||
        check.delete()
 | 
			
		||||
 | 
			
		||||
        # Policy check deleted
 | 
			
		||||
        if check.policy:
 | 
			
		||||
            delete_policy_check_task.delay(checkpk=check_pk)
 | 
			
		||||
            Check.objects.filter(managed_by_policy=True, parent_check=pk).delete()
 | 
			
		||||
 | 
			
		||||
            # Re-evaluate agent checks is policy was enforced
 | 
			
		||||
            if check.policy.enforced:
 | 
			
		||||
                generate_agent_checks_from_policies_task.delay(policypk=policy_pk)
 | 
			
		||||
                generate_agent_checks_task.delay(policy=check.policy)
 | 
			
		||||
 | 
			
		||||
        # Agent check deleted
 | 
			
		||||
        elif check.agent:
 | 
			
		||||
@@ -135,7 +146,7 @@ class GetUpdateDeleteCheck(APIView):
 | 
			
		||||
        return Response(f"{check.readable_desc} was deleted!")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CheckHistory(APIView):
 | 
			
		||||
class GetCheckHistory(APIView):
 | 
			
		||||
    def patch(self, request, checkpk):
 | 
			
		||||
        check = get_object_or_404(Check, pk=checkpk)
 | 
			
		||||
 | 
			
		||||
@@ -149,7 +160,7 @@ class CheckHistory(APIView):
 | 
			
		||||
                    - djangotime.timedelta(days=request.data["timeFilter"]),
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
        check_history = check.check_history.filter(timeFilter).order_by("-x")  # type: ignore
 | 
			
		||||
        check_history = CheckHistory.objects.filter(check_id=checkpk).filter(timeFilter).order_by("-x")  # type: ignore
 | 
			
		||||
 | 
			
		||||
        return Response(
 | 
			
		||||
            CheckHistorySerializer(
 | 
			
		||||
@@ -159,6 +170,7 @@ class CheckHistory(APIView):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view()
 | 
			
		||||
@permission_classes([IsAuthenticated, RunChecksPerms])
 | 
			
		||||
def run_checks(request, pk):
 | 
			
		||||
    agent = get_object_or_404(Agent, pk=pk)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,9 @@
 | 
			
		||||
from django.contrib import admin
 | 
			
		||||
 | 
			
		||||
from .models import Client, Deployment, Site
 | 
			
		||||
from .models import Client, ClientCustomField, Deployment, Site, SiteCustomField
 | 
			
		||||
 | 
			
		||||
admin.site.register(Client)
 | 
			
		||||
admin.site.register(Site)
 | 
			
		||||
admin.site.register(Deployment)
 | 
			
		||||
admin.site.register(ClientCustomField)
 | 
			
		||||
admin.site.register(SiteCustomField)
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,33 @@
 | 
			
		||||
# Generated by Django 3.1.7 on 2021-03-17 14:45
 | 
			
		||||
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('core', '0014_customfield'),
 | 
			
		||||
        ('clients', '0009_auto_20210212_1408'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='SiteCustomField',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
			
		||||
                ('value', models.TextField(blank=True, null=True)),
 | 
			
		||||
                ('field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='site_fields', to='core.customfield')),
 | 
			
		||||
                ('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='custom_fields', to='clients.site')),
 | 
			
		||||
            ],
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='ClientCustomField',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
			
		||||
                ('value', models.TextField(blank=True, null=True)),
 | 
			
		||||
                ('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='custom_fields', to='clients.client')),
 | 
			
		||||
                ('field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='client_fields', to='core.customfield')),
 | 
			
		||||
            ],
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -0,0 +1,17 @@
 | 
			
		||||
# Generated by Django 3.1.7 on 2021-03-21 15:11
 | 
			
		||||
 | 
			
		||||
from django.db import migrations
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('clients', '0010_clientcustomfield_sitecustomfield'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterUniqueTogether(
 | 
			
		||||
            name='site',
 | 
			
		||||
            unique_together={('client', 'name')},
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -0,0 +1,18 @@
 | 
			
		||||
# Generated by Django 3.1.7 on 2021-03-26 06:52
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('clients', '0011_auto_20210321_1511'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='deployment',
 | 
			
		||||
            name='created',
 | 
			
		||||
            field=models.DateTimeField(auto_now_add=True, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -0,0 +1,24 @@
 | 
			
		||||
# Generated by Django 3.1.7 on 2021-03-29 02:51
 | 
			
		||||
 | 
			
		||||
import django.contrib.postgres.fields
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('clients', '0012_deployment_created'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='clientcustomfield',
 | 
			
		||||
            name='multiple_value',
 | 
			
		||||
            field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(blank=True, null=True), blank=True, default=list, null=True, size=None),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='sitecustomfield',
 | 
			
		||||
            name='multiple_value',
 | 
			
		||||
            field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(blank=True, null=True), blank=True, default=list, null=True, size=None),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -0,0 +1,23 @@
 | 
			
		||||
# Generated by Django 3.1.7 on 2021-03-29 03:01
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('clients', '0013_auto_20210329_0251'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='clientcustomfield',
 | 
			
		||||
            name='checkbox_value',
 | 
			
		||||
            field=models.BooleanField(blank=True, default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='sitecustomfield',
 | 
			
		||||
            name='checkbox_value',
 | 
			
		||||
            field=models.BooleanField(blank=True, default=False),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -0,0 +1,27 @@
 | 
			
		||||
# Generated by Django 3.1.7 on 2021-03-29 17:09
 | 
			
		||||
 | 
			
		||||
from django.db import migrations
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('clients', '0014_auto_20210329_0301'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.RenameField(
 | 
			
		||||
            model_name='clientcustomfield',
 | 
			
		||||
            old_name='checkbox_value',
 | 
			
		||||
            new_name='bool_value',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RenameField(
 | 
			
		||||
            model_name='clientcustomfield',
 | 
			
		||||
            old_name='value',
 | 
			
		||||
            new_name='string_value',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='sitecustomfield',
 | 
			
		||||
            name='value',
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -0,0 +1,23 @@
 | 
			
		||||
# Generated by Django 3.1.7 on 2021-03-29 18:27
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('clients', '0015_auto_20210329_1709'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.RenameField(
 | 
			
		||||
            model_name='sitecustomfield',
 | 
			
		||||
            old_name='checkbox_value',
 | 
			
		||||
            new_name='bool_value',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='sitecustomfield',
 | 
			
		||||
            name='string_value',
 | 
			
		||||
            field=models.TextField(blank=True, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -0,0 +1,23 @@
 | 
			
		||||
# Generated by Django 3.1.7 on 2021-04-17 01:25
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('clients', '0016_auto_20210329_1827'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='client',
 | 
			
		||||
            name='block_policy_inheritance',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='site',
 | 
			
		||||
            name='block_policy_inheritance',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
import uuid
 | 
			
		||||
 | 
			
		||||
from django.contrib.postgres.fields import ArrayField
 | 
			
		||||
from django.db import models
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
@@ -8,6 +9,7 @@ from logs.models import BaseAuditModel
 | 
			
		||||
 | 
			
		||||
class Client(BaseAuditModel):
 | 
			
		||||
    name = models.CharField(max_length=255, unique=True)
 | 
			
		||||
    block_policy_inheritance = models.BooleanField(default=False)
 | 
			
		||||
    workstation_policy = models.ForeignKey(
 | 
			
		||||
        "automation.Policy",
 | 
			
		||||
        related_name="workstation_clients",
 | 
			
		||||
@@ -31,32 +33,34 @@ 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_by_location_task
 | 
			
		||||
        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 server polcies have changed and initiate task to reapply policies if so
 | 
			
		||||
        if old_client and old_client.server_policy != self.server_policy:
 | 
			
		||||
            generate_agent_checks_by_location_task.delay(
 | 
			
		||||
                location={"site__client_id": self.pk},
 | 
			
		||||
                mon_type="server",
 | 
			
		||||
                create_tasks=True,
 | 
			
		||||
            )
 | 
			
		||||
        # check if polcies have changed and initiate task to reapply policies if so
 | 
			
		||||
        if old_client:
 | 
			
		||||
            if (
 | 
			
		||||
                (old_client.server_policy != self.server_policy)
 | 
			
		||||
                or (old_client.workstation_policy != self.workstation_policy)
 | 
			
		||||
                or (
 | 
			
		||||
                    old_client.block_policy_inheritance != self.block_policy_inheritance
 | 
			
		||||
                )
 | 
			
		||||
            ):
 | 
			
		||||
                generate_agent_checks_task.delay(
 | 
			
		||||
                    client=self.pk,
 | 
			
		||||
                    create_tasks=True,
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
        # check if workstation polcies have changed and initiate task to reapply policies if so
 | 
			
		||||
        if old_client and old_client.workstation_policy != self.workstation_policy:
 | 
			
		||||
            generate_agent_checks_by_location_task.delay(
 | 
			
		||||
                location={"site__client_id": self.pk},
 | 
			
		||||
                mon_type="workstation",
 | 
			
		||||
                create_tasks=True,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        if old_client and old_client.alert_template != self.alert_template:
 | 
			
		||||
            cache_agents_alert_template.delay()
 | 
			
		||||
            if old_client.alert_template != self.alert_template:
 | 
			
		||||
                cache_agents_alert_template.delay()
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        ordering = ("name",)
 | 
			
		||||
@@ -64,6 +68,10 @@ class Client(BaseAuditModel):
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return self.name
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def agent_count(self) -> int:
 | 
			
		||||
        return Agent.objects.filter(site__client=self).count()
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def has_maintenanace_mode_agents(self):
 | 
			
		||||
        return (
 | 
			
		||||
@@ -82,31 +90,49 @@ class Client(BaseAuditModel):
 | 
			
		||||
                "offline_time",
 | 
			
		||||
            )
 | 
			
		||||
            .filter(site__client=self)
 | 
			
		||||
            .prefetch_related("agentchecks")
 | 
			
		||||
            .prefetch_related("agentchecks", "autotasks")
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        failing = 0
 | 
			
		||||
        data = {"error": False, "warning": False}
 | 
			
		||||
 | 
			
		||||
        for agent in agents:
 | 
			
		||||
            if agent.checks["has_failing_checks"]:
 | 
			
		||||
                failing += 1
 | 
			
		||||
            if agent.maintenance_mode:
 | 
			
		||||
                break
 | 
			
		||||
 | 
			
		||||
            if agent.overdue_email_alert or agent.overdue_text_alert:
 | 
			
		||||
                if agent.status == "overdue":
 | 
			
		||||
                    failing += 1
 | 
			
		||||
                    data["error"] = True
 | 
			
		||||
                    break
 | 
			
		||||
 | 
			
		||||
        return failing > 0
 | 
			
		||||
            if agent.checks["has_failing_checks"]:
 | 
			
		||||
 | 
			
		||||
                if agent.checks["warning"]:
 | 
			
		||||
                    data["warning"] = True
 | 
			
		||||
 | 
			
		||||
                if agent.checks["failing"]:
 | 
			
		||||
                    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):
 | 
			
		||||
    client = models.ForeignKey(Client, related_name="sites", on_delete=models.CASCADE)
 | 
			
		||||
    name = models.CharField(max_length=255)
 | 
			
		||||
    block_policy_inheritance = models.BooleanField(default=False)
 | 
			
		||||
    workstation_policy = models.ForeignKey(
 | 
			
		||||
        "automation.Policy",
 | 
			
		||||
        related_name="workstation_sites",
 | 
			
		||||
@@ -130,39 +156,41 @@ 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_by_location_task
 | 
			
		||||
        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 server polcies have changed and initiate task to reapply policies if so
 | 
			
		||||
        if old_site and old_site.server_policy != self.server_policy:
 | 
			
		||||
            generate_agent_checks_by_location_task.delay(
 | 
			
		||||
                location={"site_id": self.pk},
 | 
			
		||||
                mon_type="server",
 | 
			
		||||
                create_tasks=True,
 | 
			
		||||
            )
 | 
			
		||||
        # check if polcies have changed and initiate task to reapply policies if so
 | 
			
		||||
        if old_site:
 | 
			
		||||
            if (
 | 
			
		||||
                (old_site.server_policy != self.server_policy)
 | 
			
		||||
                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)
 | 
			
		||||
 | 
			
		||||
        # check if workstation polcies have changed and initiate task to reapply policies if so
 | 
			
		||||
        if old_site and old_site.workstation_policy != self.workstation_policy:
 | 
			
		||||
            generate_agent_checks_by_location_task.delay(
 | 
			
		||||
                location={"site_id": self.pk},
 | 
			
		||||
                mon_type="workstation",
 | 
			
		||||
                create_tasks=True,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        if old_site and 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",)
 | 
			
		||||
        unique_together = (("client", "name"),)
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return self.name
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def agent_count(self) -> int:
 | 
			
		||||
        return Agent.objects.filter(site=self).count()
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def has_maintenanace_mode_agents(self):
 | 
			
		||||
        return Agent.objects.filter(site=self, maintenance_mode=True).count() > 0
 | 
			
		||||
@@ -179,26 +207,42 @@ class Site(BaseAuditModel):
 | 
			
		||||
                "offline_time",
 | 
			
		||||
            )
 | 
			
		||||
            .filter(site=self)
 | 
			
		||||
            .prefetch_related("agentchecks")
 | 
			
		||||
            .prefetch_related("agentchecks", "autotasks")
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        failing = 0
 | 
			
		||||
        data = {"error": False, "warning": False}
 | 
			
		||||
 | 
			
		||||
        for agent in agents:
 | 
			
		||||
            if agent.checks["has_failing_checks"]:
 | 
			
		||||
                failing += 1
 | 
			
		||||
            if agent.maintenance_mode:
 | 
			
		||||
                break
 | 
			
		||||
 | 
			
		||||
            if agent.overdue_email_alert or agent.overdue_text_alert:
 | 
			
		||||
                if agent.status == "overdue":
 | 
			
		||||
                    failing += 1
 | 
			
		||||
                    data["error"] = True
 | 
			
		||||
                    break
 | 
			
		||||
 | 
			
		||||
        return failing > 0
 | 
			
		||||
            if agent.checks["has_failing_checks"]:
 | 
			
		||||
                if agent.checks["warning"]:
 | 
			
		||||
                    data["warning"] = True
 | 
			
		||||
 | 
			
		||||
                if agent.checks["failing"]:
 | 
			
		||||
                    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 = [
 | 
			
		||||
@@ -225,6 +269,7 @@ class Deployment(models.Model):
 | 
			
		||||
    )
 | 
			
		||||
    arch = models.CharField(max_length=255, choices=ARCH_CHOICES, default="64")
 | 
			
		||||
    expiry = models.DateTimeField(null=True, blank=True)
 | 
			
		||||
    created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
 | 
			
		||||
    auth_token = models.ForeignKey(
 | 
			
		||||
        "knox.AuthToken", related_name="deploytokens", on_delete=models.CASCADE
 | 
			
		||||
    )
 | 
			
		||||
@@ -233,3 +278,105 @@ class Deployment(models.Model):
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return f"{self.client} - {self.site} - {self.mon_type}"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ClientCustomField(models.Model):
 | 
			
		||||
    client = models.ForeignKey(
 | 
			
		||||
        Client,
 | 
			
		||||
        related_name="custom_fields",
 | 
			
		||||
        on_delete=models.CASCADE,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    field = models.ForeignKey(
 | 
			
		||||
        "core.CustomField",
 | 
			
		||||
        related_name="client_fields",
 | 
			
		||||
        on_delete=models.CASCADE,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    string_value = models.TextField(null=True, blank=True)
 | 
			
		||||
    bool_value = models.BooleanField(blank=True, default=False)
 | 
			
		||||
    multiple_value = ArrayField(
 | 
			
		||||
        models.TextField(null=True, blank=True),
 | 
			
		||||
        null=True,
 | 
			
		||||
        blank=True,
 | 
			
		||||
        default=list,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return self.field.name
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def value(self):
 | 
			
		||||
        if self.field.type == "multiple":
 | 
			
		||||
            return self.multiple_value
 | 
			
		||||
        elif self.field.type == "checkbox":
 | 
			
		||||
            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()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SiteCustomField(models.Model):
 | 
			
		||||
    site = models.ForeignKey(
 | 
			
		||||
        Site,
 | 
			
		||||
        related_name="custom_fields",
 | 
			
		||||
        on_delete=models.CASCADE,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    field = models.ForeignKey(
 | 
			
		||||
        "core.CustomField",
 | 
			
		||||
        related_name="site_fields",
 | 
			
		||||
        on_delete=models.CASCADE,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    string_value = models.TextField(null=True, blank=True)
 | 
			
		||||
    bool_value = models.BooleanField(blank=True, default=False)
 | 
			
		||||
    multiple_value = ArrayField(
 | 
			
		||||
        models.TextField(null=True, blank=True),
 | 
			
		||||
        null=True,
 | 
			
		||||
        blank=True,
 | 
			
		||||
        default=list,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return self.field.name
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def value(self):
 | 
			
		||||
        if self.field.type == "multiple":
 | 
			
		||||
            return self.multiple_value
 | 
			
		||||
        elif self.field.type == "checkbox":
 | 
			
		||||
            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()
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										27
									
								
								api/tacticalrmm/clients/permissions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								api/tacticalrmm/clients/permissions.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,27 @@
 | 
			
		||||
from rest_framework import permissions
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.permissions import _has_perm
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ManageClientsPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        if r.method == "GET":
 | 
			
		||||
            return True
 | 
			
		||||
 | 
			
		||||
        return _has_perm(r, "can_manage_clients")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ManageSitesPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        if r.method == "GET":
 | 
			
		||||
            return True
 | 
			
		||||
 | 
			
		||||
        return _has_perm(r, "can_manage_sites")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ManageDeploymentPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view):
 | 
			
		||||
        if r.method == "GET":
 | 
			
		||||
            return True
 | 
			
		||||
 | 
			
		||||
        return _has_perm(r, "can_manage_deployments")
 | 
			
		||||
@@ -1,42 +1,99 @@
 | 
			
		||||
from rest_framework.serializers import ModelSerializer, ReadOnlyField, ValidationError
 | 
			
		||||
from django.db.models.base import Model
 | 
			
		||||
from rest_framework.serializers import (
 | 
			
		||||
    ModelSerializer,
 | 
			
		||||
    ReadOnlyField,
 | 
			
		||||
    Serializer,
 | 
			
		||||
    ValidationError,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
from .models import Client, Deployment, Site
 | 
			
		||||
from .models import Client, ClientCustomField, Deployment, Site, SiteCustomField
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SiteCustomFieldSerializer(ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = SiteCustomField
 | 
			
		||||
        fields = (
 | 
			
		||||
            "id",
 | 
			
		||||
            "field",
 | 
			
		||||
            "site",
 | 
			
		||||
            "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 SiteSerializer(ModelSerializer):
 | 
			
		||||
    client_name = ReadOnlyField(source="client.name")
 | 
			
		||||
    custom_fields = SiteCustomFieldSerializer(many=True, read_only=True)
 | 
			
		||||
    agent_count = ReadOnlyField()
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Site
 | 
			
		||||
        fields = "__all__"
 | 
			
		||||
        fields = (
 | 
			
		||||
            "id",
 | 
			
		||||
            "name",
 | 
			
		||||
            "server_policy",
 | 
			
		||||
            "workstation_policy",
 | 
			
		||||
            "alert_template",
 | 
			
		||||
            "client_name",
 | 
			
		||||
            "client",
 | 
			
		||||
            "custom_fields",
 | 
			
		||||
            "agent_count",
 | 
			
		||||
            "block_policy_inheritance",
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def validate(self, val):
 | 
			
		||||
        if "name" in val.keys() and "|" in val["name"]:
 | 
			
		||||
            raise ValidationError("Site name cannot contain the | character")
 | 
			
		||||
 | 
			
		||||
        if self.context:
 | 
			
		||||
            client = Client.objects.get(pk=self.context["clientpk"])
 | 
			
		||||
            if Site.objects.filter(client=client, name=val["name"]).exists():
 | 
			
		||||
                raise ValidationError(f"Site {val['name']} already exists")
 | 
			
		||||
 | 
			
		||||
        return val
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ClientCustomFieldSerializer(ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = ClientCustomField
 | 
			
		||||
        fields = (
 | 
			
		||||
            "id",
 | 
			
		||||
            "field",
 | 
			
		||||
            "client",
 | 
			
		||||
            "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 ClientSerializer(ModelSerializer):
 | 
			
		||||
    sites = SiteSerializer(many=True, read_only=True)
 | 
			
		||||
    custom_fields = ClientCustomFieldSerializer(many=True, read_only=True)
 | 
			
		||||
    agent_count = ReadOnlyField()
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Client
 | 
			
		||||
        fields = "__all__"
 | 
			
		||||
        fields = (
 | 
			
		||||
            "id",
 | 
			
		||||
            "name",
 | 
			
		||||
            "server_policy",
 | 
			
		||||
            "workstation_policy",
 | 
			
		||||
            "alert_template",
 | 
			
		||||
            "block_policy_inheritance",
 | 
			
		||||
            "sites",
 | 
			
		||||
            "custom_fields",
 | 
			
		||||
            "agent_count",
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def validate(self, val):
 | 
			
		||||
 | 
			
		||||
        if "site" in self.context:
 | 
			
		||||
            if "|" in self.context["site"]:
 | 
			
		||||
                raise ValidationError("Site name cannot contain the | character")
 | 
			
		||||
            if len(self.context["site"]) > 255:
 | 
			
		||||
                raise ValidationError("Site name too long")
 | 
			
		||||
 | 
			
		||||
        if "name" in val.keys() and "|" in val["name"]:
 | 
			
		||||
            raise ValidationError("Client name cannot contain the | character")
 | 
			
		||||
 | 
			
		||||
@@ -50,7 +107,6 @@ class SiteTreeSerializer(ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Site
 | 
			
		||||
        fields = "__all__"
 | 
			
		||||
        ordering = ("failing_checks",)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ClientTreeSerializer(ModelSerializer):
 | 
			
		||||
@@ -61,7 +117,6 @@ class ClientTreeSerializer(ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Client
 | 
			
		||||
        fields = "__all__"
 | 
			
		||||
        ordering = ("failing_checks",)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DeploymentSerializer(ModelSerializer):
 | 
			
		||||
@@ -83,4 +138,17 @@ class DeploymentSerializer(ModelSerializer):
 | 
			
		||||
            "arch",
 | 
			
		||||
            "expiry",
 | 
			
		||||
            "install_flags",
 | 
			
		||||
            "created",
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SiteAuditSerializer(ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Site
 | 
			
		||||
        fields = "__all__"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ClientAuditSerializer(ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Client
 | 
			
		||||
        fields = "__all__"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,12 @@
 | 
			
		||||
import uuid
 | 
			
		||||
from unittest.mock import patch
 | 
			
		||||
 | 
			
		||||
from model_bakery import baker
 | 
			
		||||
from rest_framework.serializers import ValidationError
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.test import TacticalTestCase
 | 
			
		||||
 | 
			
		||||
from .models import Client, Deployment, Site
 | 
			
		||||
from .models import Client, ClientCustomField, Deployment, Site, SiteCustomField
 | 
			
		||||
from .serializers import (
 | 
			
		||||
    ClientSerializer,
 | 
			
		||||
    ClientTreeSerializer,
 | 
			
		||||
@@ -28,18 +29,29 @@ class TestClientViews(TacticalTestCase):
 | 
			
		||||
        r = self.client.get(url, format="json")
 | 
			
		||||
        serializer = ClientSerializer(clients, many=True)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(r.data, serializer.data)
 | 
			
		||||
        self.assertEqual(r.data, serializer.data)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
    def test_add_client(self):
 | 
			
		||||
        url = "/clients/clients/"
 | 
			
		||||
        payload = {"client": "Company 1", "site": "Site 1"}
 | 
			
		||||
 | 
			
		||||
        # test successfull add client
 | 
			
		||||
        payload = {
 | 
			
		||||
            "client": {"name": "Client1"},
 | 
			
		||||
            "site": {"name": "Site1"},
 | 
			
		||||
            "custom_fields": [],
 | 
			
		||||
        }
 | 
			
		||||
        r = self.client.post(url, payload, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        payload["client"] = "Company1|askd"
 | 
			
		||||
        serializer = ClientSerializer(data={"name": payload["client"]}, context=payload)
 | 
			
		||||
        # test add client with | in name
 | 
			
		||||
        payload = {
 | 
			
		||||
            "client": {"name": "Client2|d"},
 | 
			
		||||
            "site": {"name": "Site1"},
 | 
			
		||||
            "custom_fields": [],
 | 
			
		||||
        }
 | 
			
		||||
        serializer = ClientSerializer(data=payload["client"])
 | 
			
		||||
        with self.assertRaisesMessage(
 | 
			
		||||
            ValidationError, "Client name cannot contain the | character"
 | 
			
		||||
        ):
 | 
			
		||||
@@ -48,19 +60,22 @@ class TestClientViews(TacticalTestCase):
 | 
			
		||||
        r = self.client.post(url, payload, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 400)
 | 
			
		||||
 | 
			
		||||
        payload = {"client": "Company 156", "site": "Site2|a34"}
 | 
			
		||||
        serializer = ClientSerializer(data={"name": payload["client"]}, context=payload)
 | 
			
		||||
        with self.assertRaisesMessage(
 | 
			
		||||
            ValidationError, "Site name cannot contain the | character"
 | 
			
		||||
        ):
 | 
			
		||||
            self.assertFalse(serializer.is_valid(raise_exception=True))
 | 
			
		||||
 | 
			
		||||
        # test add client with | in Site name
 | 
			
		||||
        payload = {
 | 
			
		||||
            "client": {"name": "Client2"},
 | 
			
		||||
            "site": {"name": "Site1|fds"},
 | 
			
		||||
            "custom_fields": [],
 | 
			
		||||
        }
 | 
			
		||||
        r = self.client.post(url, payload, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 400)
 | 
			
		||||
 | 
			
		||||
        # test unique
 | 
			
		||||
        payload = {"client": "Company 1", "site": "Site 1"}
 | 
			
		||||
        serializer = ClientSerializer(data={"name": payload["client"]}, context=payload)
 | 
			
		||||
        payload = {
 | 
			
		||||
            "client": {"name": "Client1"},
 | 
			
		||||
            "site": {"name": "Site1"},
 | 
			
		||||
            "custom_fields": [],
 | 
			
		||||
        }
 | 
			
		||||
        serializer = ClientSerializer(data=payload["client"])
 | 
			
		||||
        with self.assertRaisesMessage(
 | 
			
		||||
            ValidationError, "client with this name already exists."
 | 
			
		||||
        ):
 | 
			
		||||
@@ -69,66 +84,124 @@ class TestClientViews(TacticalTestCase):
 | 
			
		||||
        r = self.client.post(url, payload, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 400)
 | 
			
		||||
 | 
			
		||||
        # test long site name
 | 
			
		||||
        payload = {"client": "Company 2394", "site": "Site123" * 100}
 | 
			
		||||
        serializer = ClientSerializer(data={"name": payload["client"]}, context=payload)
 | 
			
		||||
        with self.assertRaisesMessage(ValidationError, "Site name too long"):
 | 
			
		||||
            self.assertFalse(serializer.is_valid(raise_exception=True))
 | 
			
		||||
 | 
			
		||||
        r = self.client.post(url, payload, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 400)
 | 
			
		||||
 | 
			
		||||
        # test initial setup
 | 
			
		||||
        payload = {
 | 
			
		||||
            "client": {"client": "Company 4", "site": "HQ"},
 | 
			
		||||
            "initialsetup": True,
 | 
			
		||||
            "client": {"name": "Setup Client"},
 | 
			
		||||
            "site": {"name": "Setup  Site"},
 | 
			
		||||
            "timezone": "America/Los_Angeles",
 | 
			
		||||
            "initialsetup": True,
 | 
			
		||||
        }
 | 
			
		||||
        r = self.client.post(url, payload, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        # test add with custom fields
 | 
			
		||||
        field = baker.make("core.CustomField", model="client", type="text")
 | 
			
		||||
        payload = {
 | 
			
		||||
            "client": {"name": "Custom Field Client"},
 | 
			
		||||
            "site": {"name": "Setup  Site"},
 | 
			
		||||
            "custom_fields": [{"field": field.id, "string_value": "new Value"}],  # type: ignore
 | 
			
		||||
        }
 | 
			
		||||
        r = self.client.post(url, payload, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        client = Client.objects.get(name="Custom Field Client")
 | 
			
		||||
        self.assertTrue(
 | 
			
		||||
            ClientCustomField.objects.filter(client=client, field=field).exists()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("post", url)
 | 
			
		||||
 | 
			
		||||
    def test_get_client(self):
 | 
			
		||||
        # setup data
 | 
			
		||||
        client = baker.make("clients.Client")
 | 
			
		||||
 | 
			
		||||
        url = f"/clients/{client.id}/client/"  # 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)
 | 
			
		||||
 | 
			
		||||
    def test_edit_client(self):
 | 
			
		||||
        # setup data
 | 
			
		||||
        client = baker.make("clients.Client")
 | 
			
		||||
        client = baker.make("clients.Client", name="OldClientName")
 | 
			
		||||
 | 
			
		||||
        # test invalid id
 | 
			
		||||
        r = self.client.put("/clients/500/client/", format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 404)
 | 
			
		||||
 | 
			
		||||
        data = {"id": client.id, "name": "New Name"}
 | 
			
		||||
 | 
			
		||||
        url = f"/clients/{client.id}/client/"
 | 
			
		||||
        # test successfull edit client
 | 
			
		||||
        data = {"client": {"name": "NewClientName"}, "custom_fields": []}
 | 
			
		||||
        url = f"/clients/{client.id}/client/"  # type: ignore
 | 
			
		||||
        r = self.client.put(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertTrue(Client.objects.filter(name="New Name").exists())
 | 
			
		||||
        self.assertTrue(Client.objects.filter(name="NewClientName").exists())
 | 
			
		||||
        self.assertFalse(Client.objects.filter(name="OldClientName").exists())
 | 
			
		||||
 | 
			
		||||
        # 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)
 | 
			
		||||
 | 
			
		||||
        # test add with custom fields new value
 | 
			
		||||
        field = baker.make("core.CustomField", model="client", type="checkbox")
 | 
			
		||||
        payload = {
 | 
			
		||||
            "client": {
 | 
			
		||||
                "id": client.id,  # type: ignore
 | 
			
		||||
                "name": "Custom Field Client",
 | 
			
		||||
            },
 | 
			
		||||
            "custom_fields": [{"field": field.id, "bool_value": True}],  # type: ignore
 | 
			
		||||
        }
 | 
			
		||||
        r = self.client.put(url, payload, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        client = Client.objects.get(name="Custom Field Client")
 | 
			
		||||
        self.assertTrue(
 | 
			
		||||
            ClientCustomField.objects.filter(client=client, field=field).exists()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # edit custom field value
 | 
			
		||||
        payload = {
 | 
			
		||||
            "client": {
 | 
			
		||||
                "id": client.id,  # type: ignore
 | 
			
		||||
                "name": "Custom Field Client",
 | 
			
		||||
            },
 | 
			
		||||
            "custom_fields": [{"field": field.id, "bool_value": False}],  # type: ignore
 | 
			
		||||
        }
 | 
			
		||||
        r = self.client.put(url, payload, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        self.assertFalse(
 | 
			
		||||
            ClientCustomField.objects.get(client=client, field=field).value
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("put", url)
 | 
			
		||||
 | 
			
		||||
    def test_delete_client(self):
 | 
			
		||||
        from agents.models import Agent
 | 
			
		||||
 | 
			
		||||
        # setup data
 | 
			
		||||
        client = baker.make("clients.Client")
 | 
			
		||||
        site = baker.make("clients.Site", client=client)
 | 
			
		||||
        agent = baker.make_recipe("agents.agent", site=site)
 | 
			
		||||
        client_to_delete = baker.make("clients.Client")
 | 
			
		||||
        client_to_move = baker.make("clients.Client")
 | 
			
		||||
        site_to_move = baker.make("clients.Site", client=client_to_move)
 | 
			
		||||
        agent = baker.make_recipe("agents.agent", site=site_to_move)
 | 
			
		||||
 | 
			
		||||
        # test invalid id
 | 
			
		||||
        r = self.client.delete("/clients/500/client/", format="json")
 | 
			
		||||
        r = self.client.delete("/clients/334/953/", format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 404)
 | 
			
		||||
 | 
			
		||||
        url = f"/clients/{client.id}/client/"
 | 
			
		||||
 | 
			
		||||
        # test deleting with agents under client
 | 
			
		||||
        r = self.client.delete(url, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 400)
 | 
			
		||||
        url = f"/clients/{client_to_delete.id}/{site_to_move.id}/"  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # test successful deletion
 | 
			
		||||
        agent.delete()
 | 
			
		||||
        r = self.client.delete(url, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertFalse(Client.objects.filter(pk=client.id).exists())
 | 
			
		||||
        self.assertFalse(Site.objects.filter(pk=site.id).exists())
 | 
			
		||||
        agent_moved = Agent.objects.get(pk=agent.pk)
 | 
			
		||||
        self.assertEqual(agent_moved.site.id, site_to_move.id)  # type: ignore
 | 
			
		||||
        self.assertFalse(Client.objects.filter(pk=client_to_delete.id).exists())  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("put", url)
 | 
			
		||||
        self.check_not_authenticated("delete", url)
 | 
			
		||||
 | 
			
		||||
    def test_get_sites(self):
 | 
			
		||||
        # setup data
 | 
			
		||||
@@ -139,29 +212,31 @@ class TestClientViews(TacticalTestCase):
 | 
			
		||||
        r = self.client.get(url, format="json")
 | 
			
		||||
        serializer = SiteSerializer(sites, many=True)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(r.data, serializer.data)
 | 
			
		||||
        self.assertEqual(r.data, serializer.data)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
    def test_add_site(self):
 | 
			
		||||
        # setup data
 | 
			
		||||
        site = baker.make("clients.Site")
 | 
			
		||||
        client = baker.make("clients.Client")
 | 
			
		||||
        site = baker.make("clients.Site", client=client)
 | 
			
		||||
 | 
			
		||||
        url = "/clients/sites/"
 | 
			
		||||
 | 
			
		||||
        # test success add
 | 
			
		||||
        payload = {"client": site.client.id, "name": "LA Office"}
 | 
			
		||||
        payload = {
 | 
			
		||||
            "site": {"client": client.id, "name": "LA Office"},  # type: ignore
 | 
			
		||||
            "custom_fields": [],
 | 
			
		||||
        }
 | 
			
		||||
        r = self.client.post(url, payload, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertTrue(
 | 
			
		||||
            Site.objects.filter(
 | 
			
		||||
                name="LA Office", client__name=site.client.name
 | 
			
		||||
            ).exists()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # test with | symbol
 | 
			
		||||
        payload = {"client": site.client.id, "name": "LA Off|ice  |*&@#$"}
 | 
			
		||||
        serializer = SiteSerializer(data=payload, context={"clientpk": site.client.id})
 | 
			
		||||
        payload = {
 | 
			
		||||
            "site": {"client": client.id, "name": "LA Office  |*&@#$"},  # type: ignore
 | 
			
		||||
            "custom_fields": [],
 | 
			
		||||
        }
 | 
			
		||||
        serializer = SiteSerializer(data=payload["site"])
 | 
			
		||||
        with self.assertRaisesMessage(
 | 
			
		||||
            ValidationError, "Site name cannot contain the | character"
 | 
			
		||||
        ):
 | 
			
		||||
@@ -171,55 +246,135 @@ class TestClientViews(TacticalTestCase):
 | 
			
		||||
        self.assertEqual(r.status_code, 400)
 | 
			
		||||
 | 
			
		||||
        # test site already exists
 | 
			
		||||
        payload = {"client": site.client.id, "name": "LA Office"}
 | 
			
		||||
        serializer = SiteSerializer(data=payload, context={"clientpk": site.client.id})
 | 
			
		||||
        with self.assertRaisesMessage(ValidationError, "Site LA Office already exists"):
 | 
			
		||||
        payload = {
 | 
			
		||||
            "site": {"client": site.client.id, "name": "LA Office"},  # type: ignore
 | 
			
		||||
            "custom_fields": [],
 | 
			
		||||
        }
 | 
			
		||||
        serializer = SiteSerializer(data=payload["site"])
 | 
			
		||||
        with self.assertRaisesMessage(
 | 
			
		||||
            ValidationError, "The fields client, name must make a unique set."
 | 
			
		||||
        ):
 | 
			
		||||
            self.assertFalse(serializer.is_valid(raise_exception=True))
 | 
			
		||||
 | 
			
		||||
        # test add with custom fields
 | 
			
		||||
        field = baker.make(
 | 
			
		||||
            "core.CustomField",
 | 
			
		||||
            model="site",
 | 
			
		||||
            type="single",
 | 
			
		||||
            options=["one", "two", "three"],
 | 
			
		||||
        )
 | 
			
		||||
        payload = {
 | 
			
		||||
            "site": {"client": client.id, "name": "Custom Field Site"},  # type: ignore
 | 
			
		||||
            "custom_fields": [{"field": field.id, "string_value": "one"}],  # type: ignore
 | 
			
		||||
        }
 | 
			
		||||
        r = self.client.post(url, payload, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        site = Site.objects.get(name="Custom Field Site")
 | 
			
		||||
        self.assertTrue(SiteCustomField.objects.filter(site=site, field=field).exists())
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("post", url)
 | 
			
		||||
 | 
			
		||||
    def test_edit_site(self):
 | 
			
		||||
    def test_get_site(self):
 | 
			
		||||
        # setup data
 | 
			
		||||
        site = baker.make("clients.Site")
 | 
			
		||||
 | 
			
		||||
        url = f"/clients/sites/{site.id}/"  # type: ignore
 | 
			
		||||
        r = self.client.get(url, format="json")
 | 
			
		||||
        serializer = SiteSerializer(site)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(r.data, serializer.data)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
    def test_edit_site(self):
 | 
			
		||||
        # setup data
 | 
			
		||||
        client = baker.make("clients.Client")
 | 
			
		||||
        site = baker.make("clients.Site", client=client)
 | 
			
		||||
 | 
			
		||||
        # test invalid id
 | 
			
		||||
        r = self.client.put("/clients/500/site/", format="json")
 | 
			
		||||
        r = self.client.put("/clients/sites/688/", format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 404)
 | 
			
		||||
 | 
			
		||||
        data = {"id": site.id, "name": "New Name", "client": site.client.id}
 | 
			
		||||
        data = {
 | 
			
		||||
            "site": {"client": client.id, "name": "New Site Name"},  # type: ignore
 | 
			
		||||
            "custom_fields": [],
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        url = f"/clients/{site.id}/site/"
 | 
			
		||||
        url = f"/clients/sites/{site.id}/"  # type: ignore
 | 
			
		||||
        r = self.client.put(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertTrue(Site.objects.filter(name="New Name").exists())
 | 
			
		||||
        self.assertTrue(
 | 
			
		||||
            Site.objects.filter(client=client, name="New Site Name").exists()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # test add with custom fields new value
 | 
			
		||||
        field = baker.make(
 | 
			
		||||
            "core.CustomField",
 | 
			
		||||
            model="site",
 | 
			
		||||
            type="multiple",
 | 
			
		||||
            options=["one", "two", "three"],
 | 
			
		||||
        )
 | 
			
		||||
        payload = {
 | 
			
		||||
            "site": {
 | 
			
		||||
                "id": site.id,  # type: ignore
 | 
			
		||||
                "client": site.client.id,  # type: ignore
 | 
			
		||||
                "name": "Custom Field Site",
 | 
			
		||||
            },
 | 
			
		||||
            "custom_fields": [{"field": field.id, "multiple_value": ["two", "three"]}],  # type: ignore
 | 
			
		||||
        }
 | 
			
		||||
        r = self.client.put(url, payload, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        site = Site.objects.get(name="Custom Field Site")
 | 
			
		||||
        self.assertTrue(SiteCustomField.objects.filter(site=site, field=field).exists())
 | 
			
		||||
 | 
			
		||||
        # edit custom field value
 | 
			
		||||
        payload = {
 | 
			
		||||
            "site": {
 | 
			
		||||
                "id": site.id,  # type: ignore
 | 
			
		||||
                "client": client.id,  # type: ignore
 | 
			
		||||
                "name": "Custom Field Site",
 | 
			
		||||
            },
 | 
			
		||||
            "custom_fields": [{"field": field.id, "multiple_value": ["one"]}],  # type: ignore
 | 
			
		||||
        }
 | 
			
		||||
        r = self.client.put(url, payload, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        self.assertTrue(
 | 
			
		||||
            SiteCustomField.objects.get(site=site, field=field).value,
 | 
			
		||||
            ["one"],
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("put", url)
 | 
			
		||||
 | 
			
		||||
    def test_delete_site(self):
 | 
			
		||||
        from agents.models import Agent
 | 
			
		||||
 | 
			
		||||
        # setup data
 | 
			
		||||
        site = baker.make("clients.Site")
 | 
			
		||||
        agent = baker.make_recipe("agents.agent", site=site)
 | 
			
		||||
        client = baker.make("clients.Client")
 | 
			
		||||
        site_to_delete = baker.make("clients.Site", client=client)
 | 
			
		||||
        site_to_move = baker.make("clients.Site")
 | 
			
		||||
        agent = baker.make_recipe("agents.agent", site=site_to_delete)
 | 
			
		||||
 | 
			
		||||
        # test invalid id
 | 
			
		||||
        r = self.client.delete("/clients/500/site/", format="json")
 | 
			
		||||
        r = self.client.delete("/clients/500/445/", format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 404)
 | 
			
		||||
 | 
			
		||||
        url = f"/clients/{site.id}/site/"
 | 
			
		||||
        url = f"/clients/sites/{site_to_delete.id}/{site_to_move.id}/"  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # test deleting with last site under client
 | 
			
		||||
        r = self.client.delete(url, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 400)
 | 
			
		||||
 | 
			
		||||
        # test deletion when agents exist under site
 | 
			
		||||
        baker.make("clients.Site", client=site.client)
 | 
			
		||||
        r = self.client.delete(url, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 400)
 | 
			
		||||
        self.assertEqual(r.json(), "A client must have at least 1 site.")
 | 
			
		||||
 | 
			
		||||
        # test successful deletion
 | 
			
		||||
        agent.delete()
 | 
			
		||||
        site_to_move.client = client  # type: ignore
 | 
			
		||||
        site_to_move.save(update_fields=["client"])  # type: ignore
 | 
			
		||||
        r = self.client.delete(url, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertFalse(Site.objects.filter(pk=site.id).exists())
 | 
			
		||||
        agent_moved = Agent.objects.get(pk=agent.pk)
 | 
			
		||||
        self.assertEqual(agent_moved.site.id, site_to_move.id)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("delete", url)
 | 
			
		||||
 | 
			
		||||
@@ -233,7 +388,7 @@ class TestClientViews(TacticalTestCase):
 | 
			
		||||
        r = self.client.get(url, format="json")
 | 
			
		||||
        serializer = ClientTreeSerializer(clients, many=True)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(r.data, serializer.data)
 | 
			
		||||
        self.assertEqual(r.data, serializer.data)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
@@ -245,7 +400,7 @@ class TestClientViews(TacticalTestCase):
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
        serializer = DeploymentSerializer(deployments, many=True)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(r.data, serializer.data)
 | 
			
		||||
        self.assertEqual(r.data, serializer.data)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
@@ -255,8 +410,8 @@ class TestClientViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        url = "/clients/deployments/"
 | 
			
		||||
        payload = {
 | 
			
		||||
            "client": site.client.id,
 | 
			
		||||
            "site": site.id,
 | 
			
		||||
            "client": site.client.id,  # type: ignore
 | 
			
		||||
            "site": site.id,  # type: ignore
 | 
			
		||||
            "expires": "2037-11-23 18:53",
 | 
			
		||||
            "power": 1,
 | 
			
		||||
            "ping": 0,
 | 
			
		||||
@@ -284,10 +439,10 @@ class TestClientViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        url = "/clients/deployments/"
 | 
			
		||||
 | 
			
		||||
        url = f"/clients/{deployment.id}/deployment/"
 | 
			
		||||
        url = f"/clients/{deployment.id}/deployment/"  # type: ignore
 | 
			
		||||
        r = self.client.delete(url)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertFalse(Deployment.objects.filter(pk=deployment.id).exists())
 | 
			
		||||
        self.assertFalse(Deployment.objects.filter(pk=deployment.id).exists())  # type: ignore
 | 
			
		||||
 | 
			
		||||
        url = "/clients/32348/deployment/"
 | 
			
		||||
        r = self.client.delete(url)
 | 
			
		||||
@@ -301,7 +456,7 @@ class TestClientViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
        self.assertEqual(r.status_code, 400)
 | 
			
		||||
        self.assertEqual(r.data, "invalid")
 | 
			
		||||
        self.assertEqual(r.data, "invalid")  # type: ignore
 | 
			
		||||
 | 
			
		||||
        uid = uuid.uuid4()
 | 
			
		||||
        url = f"/clients/{uid}/deploy/"
 | 
			
		||||
 
 | 
			
		||||
@@ -4,10 +4,12 @@ from . import views
 | 
			
		||||
 | 
			
		||||
urlpatterns = [
 | 
			
		||||
    path("clients/", views.GetAddClients.as_view()),
 | 
			
		||||
    path("<int:pk>/client/", views.GetUpdateDeleteClient.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("sites/", views.GetAddSites.as_view()),
 | 
			
		||||
    path("<int:pk>/site/", views.GetUpdateDeleteSite.as_view()),
 | 
			
		||||
    path("sites/<int:pk>/", views.GetUpdateSite.as_view()),
 | 
			
		||||
    path("sites/<int:pk>/<int:sitepk>/", views.DeleteSite.as_view()),
 | 
			
		||||
    path("deployments/", views.AgentDeployment.as_view()),
 | 
			
		||||
    path("<int:pk>/deployment/", views.AgentDeployment.as_view()),
 | 
			
		||||
    path("<str:uid>/deploy/", views.GenerateAgent.as_view()),
 | 
			
		||||
 
 | 
			
		||||
@@ -3,71 +3,132 @@ 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 rest_framework.permissions import AllowAny
 | 
			
		||||
from rest_framework.permissions import AllowAny, IsAuthenticated
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
from rest_framework.views import APIView
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from core.models import CoreSettings
 | 
			
		||||
from tacticalrmm.utils import generate_installer_exe, notify_error
 | 
			
		||||
from tacticalrmm.utils import notify_error
 | 
			
		||||
 | 
			
		||||
from .models import Client, Deployment, Site
 | 
			
		||||
from .models import Client, ClientCustomField, Deployment, Site, SiteCustomField
 | 
			
		||||
from .permissions import ManageClientsPerms, ManageDeploymentPerms, ManageSitesPerms
 | 
			
		||||
from .serializers import (
 | 
			
		||||
    ClientCustomFieldSerializer,
 | 
			
		||||
    ClientSerializer,
 | 
			
		||||
    ClientTreeSerializer,
 | 
			
		||||
    DeploymentSerializer,
 | 
			
		||||
    SiteCustomFieldSerializer,
 | 
			
		||||
    SiteSerializer,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetAddClients(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, ManageClientsPerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request):
 | 
			
		||||
        clients = Client.objects.all()
 | 
			
		||||
        return Response(ClientSerializer(clients, many=True).data)
 | 
			
		||||
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
        # create client
 | 
			
		||||
        client_serializer = ClientSerializer(data=request.data["client"])
 | 
			
		||||
        client_serializer.is_valid(raise_exception=True)
 | 
			
		||||
        client = client_serializer.save()
 | 
			
		||||
 | 
			
		||||
        if "initialsetup" in request.data:
 | 
			
		||||
            client = {"name": request.data["client"]["client"].strip()}
 | 
			
		||||
            site = {"name": request.data["client"]["site"].strip()}
 | 
			
		||||
            serializer = ClientSerializer(data=client, context=request.data["client"])
 | 
			
		||||
            serializer.is_valid(raise_exception=True)
 | 
			
		||||
        # create site
 | 
			
		||||
        site_serializer = SiteSerializer(
 | 
			
		||||
            data={"client": client.id, "name": request.data["site"]["name"]}
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # make sure site serializer doesn't return errors and save
 | 
			
		||||
        if site_serializer.is_valid():
 | 
			
		||||
            site_serializer.save()
 | 
			
		||||
        else:
 | 
			
		||||
            # delete client since site serializer was invalid
 | 
			
		||||
            client.delete()
 | 
			
		||||
            site_serializer.is_valid(raise_exception=True)
 | 
			
		||||
 | 
			
		||||
        if "initialsetup" in request.data.keys():
 | 
			
		||||
            core = CoreSettings.objects.first()
 | 
			
		||||
            core.default_time_zone = request.data["timezone"]
 | 
			
		||||
            core.save(update_fields=["default_time_zone"])
 | 
			
		||||
        else:
 | 
			
		||||
            client = {"name": request.data["client"].strip()}
 | 
			
		||||
            site = {"name": request.data["site"].strip()}
 | 
			
		||||
            serializer = ClientSerializer(data=client, context=request.data)
 | 
			
		||||
            serializer.is_valid(raise_exception=True)
 | 
			
		||||
 | 
			
		||||
        obj = serializer.save()
 | 
			
		||||
        Site(client=obj, name=site["name"]).save()
 | 
			
		||||
        # save custom fields
 | 
			
		||||
        if "custom_fields" in request.data.keys():
 | 
			
		||||
            for field in request.data["custom_fields"]:
 | 
			
		||||
 | 
			
		||||
        return Response(f"{obj} was added!")
 | 
			
		||||
                custom_field = field
 | 
			
		||||
                custom_field["client"] = client.id
 | 
			
		||||
 | 
			
		||||
                serializer = ClientCustomFieldSerializer(data=custom_field)
 | 
			
		||||
                serializer.is_valid(raise_exception=True)
 | 
			
		||||
                serializer.save()
 | 
			
		||||
 | 
			
		||||
        return Response(f"{client} was added!")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetUpdateDeleteClient(APIView):
 | 
			
		||||
class GetUpdateClient(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, ManageClientsPerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request, pk):
 | 
			
		||||
        client = get_object_or_404(Client, pk=pk)
 | 
			
		||||
        return Response(ClientSerializer(client).data)
 | 
			
		||||
 | 
			
		||||
    def put(self, request, pk):
 | 
			
		||||
        client = get_object_or_404(Client, pk=pk)
 | 
			
		||||
 | 
			
		||||
        serializer = ClientSerializer(data=request.data, instance=client, partial=True)
 | 
			
		||||
        serializer = ClientSerializer(
 | 
			
		||||
            data=request.data["client"], instance=client, partial=True
 | 
			
		||||
        )
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        serializer.save()
 | 
			
		||||
 | 
			
		||||
        return Response("The Client was renamed")
 | 
			
		||||
        # update custom fields
 | 
			
		||||
        if "custom_fields" in request.data.keys():
 | 
			
		||||
            for field in request.data["custom_fields"]:
 | 
			
		||||
 | 
			
		||||
                custom_field = field
 | 
			
		||||
                custom_field["client"] = pk
 | 
			
		||||
 | 
			
		||||
                if ClientCustomField.objects.filter(field=field["field"], client=pk):
 | 
			
		||||
                    value = ClientCustomField.objects.get(
 | 
			
		||||
                        field=field["field"], client=pk
 | 
			
		||||
                    )
 | 
			
		||||
                    serializer = ClientCustomFieldSerializer(
 | 
			
		||||
                        instance=value, data=custom_field
 | 
			
		||||
                    )
 | 
			
		||||
                    serializer.is_valid(raise_exception=True)
 | 
			
		||||
                    serializer.save()
 | 
			
		||||
                else:
 | 
			
		||||
                    serializer = ClientCustomFieldSerializer(data=custom_field)
 | 
			
		||||
                    serializer.is_valid(raise_exception=True)
 | 
			
		||||
                    serializer.save()
 | 
			
		||||
 | 
			
		||||
        return Response("The Client was updated")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DeleteClient(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, ManageClientsPerms]
 | 
			
		||||
 | 
			
		||||
    def delete(self, request, pk, sitepk):
 | 
			
		||||
        from automation.tasks import generate_agent_checks_task
 | 
			
		||||
 | 
			
		||||
    def delete(self, request, pk):
 | 
			
		||||
        client = get_object_or_404(Client, pk=pk)
 | 
			
		||||
        agent_count = Agent.objects.filter(site__client=client).count()
 | 
			
		||||
        if agent_count > 0:
 | 
			
		||||
        agents = Agent.objects.filter(site__client=client)
 | 
			
		||||
 | 
			
		||||
        if not sitepk:
 | 
			
		||||
            return notify_error(
 | 
			
		||||
                f"Cannot delete {client} while {agent_count} agents exist in it. Move the agents to another client first."
 | 
			
		||||
                "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!")
 | 
			
		||||
 | 
			
		||||
@@ -79,65 +140,126 @@ class GetClientTree(APIView):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetAddSites(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, ManageSitesPerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request):
 | 
			
		||||
        sites = Site.objects.all()
 | 
			
		||||
        return Response(SiteSerializer(sites, many=True).data)
 | 
			
		||||
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
        name = request.data["name"].strip()
 | 
			
		||||
        serializer = SiteSerializer(data=request.data["site"])
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        site = serializer.save()
 | 
			
		||||
 | 
			
		||||
        # save custom fields
 | 
			
		||||
        if "custom_fields" in request.data.keys():
 | 
			
		||||
 | 
			
		||||
            for field in request.data["custom_fields"]:
 | 
			
		||||
 | 
			
		||||
                custom_field = field
 | 
			
		||||
                custom_field["site"] = site.id
 | 
			
		||||
 | 
			
		||||
                serializer = SiteCustomFieldSerializer(data=custom_field)
 | 
			
		||||
                serializer.is_valid(raise_exception=True)
 | 
			
		||||
                serializer.save()
 | 
			
		||||
 | 
			
		||||
        return Response(f"Site {site.name} was added!")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetUpdateSite(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, ManageSitesPerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request, pk):
 | 
			
		||||
        site = get_object_or_404(Site, pk=pk)
 | 
			
		||||
        return Response(SiteSerializer(site).data)
 | 
			
		||||
 | 
			
		||||
    def put(self, request, pk):
 | 
			
		||||
        site = get_object_or_404(Site, pk=pk)
 | 
			
		||||
 | 
			
		||||
        if "client" in request.data["site"].keys() and (
 | 
			
		||||
            site.client.id != request.data["site"]["client"]
 | 
			
		||||
            and site.client.sites.count() == 1
 | 
			
		||||
        ):
 | 
			
		||||
            return notify_error("A client must have at least one site")
 | 
			
		||||
 | 
			
		||||
        serializer = SiteSerializer(
 | 
			
		||||
            data={"name": name, "client": request.data["client"]},
 | 
			
		||||
            context={"clientpk": request.data["client"]},
 | 
			
		||||
            instance=site, data=request.data["site"], partial=True
 | 
			
		||||
        )
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        serializer.save()
 | 
			
		||||
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
        # update custom field
 | 
			
		||||
        if "custom_fields" in request.data.keys():
 | 
			
		||||
 | 
			
		||||
            for field in request.data["custom_fields"]:
 | 
			
		||||
 | 
			
		||||
                custom_field = field
 | 
			
		||||
                custom_field["site"] = pk
 | 
			
		||||
 | 
			
		||||
                if SiteCustomField.objects.filter(field=field["field"], site=pk):
 | 
			
		||||
                    value = SiteCustomField.objects.get(field=field["field"], site=pk)
 | 
			
		||||
                    serializer = SiteCustomFieldSerializer(
 | 
			
		||||
                        instance=value, data=custom_field, partial=True
 | 
			
		||||
                    )
 | 
			
		||||
                    serializer.is_valid(raise_exception=True)
 | 
			
		||||
                    serializer.save()
 | 
			
		||||
                else:
 | 
			
		||||
                    serializer = SiteCustomFieldSerializer(data=custom_field)
 | 
			
		||||
                    serializer.is_valid(raise_exception=True)
 | 
			
		||||
                    serializer.save()
 | 
			
		||||
 | 
			
		||||
        return Response("Site was edited!")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetUpdateDeleteSite(APIView):
 | 
			
		||||
    def put(self, request, pk):
 | 
			
		||||
class DeleteSite(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, ManageSitesPerms]
 | 
			
		||||
 | 
			
		||||
        site = get_object_or_404(Site, pk=pk)
 | 
			
		||||
        serializer = SiteSerializer(instance=site, data=request.data, partial=True)
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        serializer.save()
 | 
			
		||||
    def delete(self, request, pk, sitepk):
 | 
			
		||||
        from automation.tasks import generate_agent_checks_task
 | 
			
		||||
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
    def delete(self, request, pk):
 | 
			
		||||
        site = get_object_or_404(Site, pk=pk)
 | 
			
		||||
        if site.client.sites.count() == 1:
 | 
			
		||||
            return notify_error(f"A client must have at least 1 site.")
 | 
			
		||||
            return notify_error("A client must have at least 1 site.")
 | 
			
		||||
 | 
			
		||||
        agent_count = Agent.objects.filter(site=site).count()
 | 
			
		||||
        agents = Agent.objects.filter(site=site)
 | 
			
		||||
 | 
			
		||||
        if agent_count > 0:
 | 
			
		||||
        if not sitepk:
 | 
			
		||||
            return notify_error(
 | 
			
		||||
                f"Cannot delete {site.name} while {agent_count} agents exist in it. Move the agents to another site first."
 | 
			
		||||
                "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!")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AgentDeployment(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, ManageDeploymentPerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request):
 | 
			
		||||
        deps = Deployment.objects.all()
 | 
			
		||||
        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"])
 | 
			
		||||
 | 
			
		||||
        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"],
 | 
			
		||||
@@ -173,6 +295,8 @@ class GenerateAgent(APIView):
 | 
			
		||||
    permission_classes = (AllowAny,)
 | 
			
		||||
 | 
			
		||||
    def get(self, request, uid):
 | 
			
		||||
        from tacticalrmm.utils import generate_winagent_exe
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            _ = uuid.UUID(uid, version=4)
 | 
			
		||||
        except ValueError:
 | 
			
		||||
@@ -180,28 +304,22 @@ class GenerateAgent(APIView):
 | 
			
		||||
 | 
			
		||||
        d = get_object_or_404(Deployment, uid=uid)
 | 
			
		||||
 | 
			
		||||
        inno = (
 | 
			
		||||
            f"winagent-v{settings.LATEST_AGENT_VER}.exe"
 | 
			
		||||
            if d.arch == "64"
 | 
			
		||||
            else f"winagent-v{settings.LATEST_AGENT_VER}-x86.exe"
 | 
			
		||||
        )
 | 
			
		||||
        client = d.client.name.replace(" ", "").lower()
 | 
			
		||||
        site = d.site.name.replace(" ", "").lower()
 | 
			
		||||
        client = re.sub(r"([^a-zA-Z0-9]+)", "", client)
 | 
			
		||||
        site = re.sub(r"([^a-zA-Z0-9]+)", "", site)
 | 
			
		||||
        ext = ".exe" if d.arch == "64" else "-x86.exe"
 | 
			
		||||
        file_name = f"rmm-{client}-{site}-{d.mon_type}{ext}"
 | 
			
		||||
 | 
			
		||||
        return generate_installer_exe(
 | 
			
		||||
            file_name=f"rmm-{client}-{site}-{d.mon_type}{ext}",
 | 
			
		||||
            goarch="amd64" if d.arch == "64" else "386",
 | 
			
		||||
            inno=inno,
 | 
			
		||||
            api=f"https://{request.get_host()}",
 | 
			
		||||
            client_id=d.client.pk,
 | 
			
		||||
            site_id=d.site.pk,
 | 
			
		||||
            atype=d.mon_type,
 | 
			
		||||
        return generate_winagent_exe(
 | 
			
		||||
            client=d.client.pk,
 | 
			
		||||
            site=d.site.pk,
 | 
			
		||||
            agent_type=d.mon_type,
 | 
			
		||||
            rdp=d.install_flags["rdp"],
 | 
			
		||||
            ping=d.install_flags["ping"],
 | 
			
		||||
            power=d.install_flags["power"],
 | 
			
		||||
            download_url=settings.DL_64 if d.arch == "64" else settings.DL_32,
 | 
			
		||||
            arch=d.arch,
 | 
			
		||||
            token=d.token_key,
 | 
			
		||||
            api=f"https://{request.get_host()}",
 | 
			
		||||
            file_name=file_name,
 | 
			
		||||
        )
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,7 @@
 | 
			
		||||
from django.contrib import admin
 | 
			
		||||
 | 
			
		||||
from .models import CoreSettings
 | 
			
		||||
from .models import CodeSignToken, CoreSettings, CustomField
 | 
			
		||||
 | 
			
		||||
admin.site.register(CoreSettings)
 | 
			
		||||
admin.site.register(CustomField)
 | 
			
		||||
admin.site.register(CodeSignToken)
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user