Compare commits
	
		
			1278 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 8867d12ec7 | ||
|  | 154149a068 | ||
|  | c96985af03 | ||
|  | e282420a6a | ||
|  | b9a207ea71 | ||
|  | 28d52b5e7a | ||
|  | 9761f1ae29 | ||
|  | e62c8cc2e2 | ||
|  | b5aea92791 | ||
|  | 2d7724383f | ||
|  | 03f35c1975 | ||
|  | bc7dad77f4 | ||
|  | aaa2540114 | ||
|  | f46787839a | ||
|  | 228be95af1 | ||
|  | a22d7e40e5 | ||
|  | d0f87c0980 | ||
|  | 5142783db9 | ||
|  | 4aea16ca8c | ||
|  | d91d372fc5 | ||
|  | 7405d884de | ||
|  | a9ae63043e | ||
|  | 6b943866ef | ||
|  | c7bb94d82a | ||
|  | 30fb855200 | ||
|  | 80f9e56e3f | ||
|  | d301d967c7 | ||
|  | 7b7bdc4e9c | ||
|  | 796ebca74c | ||
|  | 3150bc316a | ||
|  | 0a91b12e6e | ||
|  | 918e2cc1a9 | ||
|  | fb71f83d6d | ||
|  | 82470bf04f | ||
|  | 0ac75092e6 | ||
|  | e898163aff | ||
|  | 418c7e1d9e | ||
|  | 24cbabeaf0 | ||
|  | 91069b989d | ||
|  | 1b7902894a | ||
|  | 47e022897e | ||
|  | 9aada993b1 | ||
|  | cf837b6d05 | ||
|  | 09192da4fc | ||
|  | 3a792765cd | ||
|  | a8f1b1c8bc | ||
|  | 8ffdc6bbf8 | ||
|  | 945370bc25 | ||
|  | ed4b3b0b9c | ||
|  | 83a4268441 | ||
|  | 2938be7a70 | ||
|  | e3b2ee44ca | ||
|  | f0c4658c9f | ||
|  | 0a4b236293 | ||
|  | bc7b53c3d4 | ||
|  | 5535e26eec | ||
|  | c84c3d58db | ||
|  | d6caac51dd | ||
|  | 979e7a5e08 | ||
|  | 40f16eb984 | ||
|  | c17ad1b989 | ||
|  | 24bfa062da | ||
|  | 765f675da9 | ||
|  | c0650d2ef0 | ||
|  | 168434739f | ||
|  | 337eaa46e3 | ||
|  | 94d42503b7 | ||
|  | 202edc0588 | ||
|  | c95d11da47 | ||
|  | 4f8615398c | ||
|  | f3b5f0128f | ||
|  | ab5e50c29c | ||
|  | f9236bf92f | ||
|  | 2522968b04 | ||
|  | 9c1900963d | ||
|  | 82ff41e0bb | ||
|  | fb86c14d77 | ||
|  | c6c0159ee4 | ||
|  | fe5bba18a2 | ||
|  | f61329b5de | ||
|  | fbc04afa5b | ||
|  | 2f5bcf2263 | ||
|  | 92882c337c | ||
|  | bd41f69a1c | ||
|  | f801709587 | ||
|  | 1cb37d29df | ||
|  | 2d7db408fd | ||
|  | ef1afc99c6 | ||
|  | 5682c9a5b2 | ||
|  | c525b18a02 | ||
|  | 72159cb94d | ||
|  | 39e31a1039 | ||
|  | 734177fecc | ||
|  | 39311099df | ||
|  | b8653e6601 | ||
|  | cb4b1971e6 | ||
|  | 63c60ba716 | ||
|  | 50435425e5 | ||
|  | ff192f102d | ||
|  | 99cdaa1305 | ||
|  | 7fc897dba9 | ||
|  | 3bedd65ad8 | ||
|  | a46175ce53 | ||
|  | dba3bf8ce9 | ||
|  | 3f32234c93 | ||
|  | 2863e64e3b | ||
|  | 68ec78e01c | ||
|  | 3a7c506a8f | ||
|  | 1ca63ed2d2 | ||
|  | e9e98ebcfc | ||
|  | 04de7998af | ||
|  | a5d02dc34a | ||
|  | 6181b0466e | ||
|  | 810d8f637d | ||
|  | 223b3e81d5 | ||
|  | 3a8b5bbd3f | ||
|  | ecf3b33ca7 | ||
|  | 006b20351e | ||
|  | 4b577c9541 | ||
|  | 8db59458a8 | ||
|  | 7eed5f09aa | ||
|  | a1bb265222 | ||
|  | 0235f33f8b | ||
|  | 3d6fca85db | ||
|  | 4c06da0646 | ||
|  | f63603eb84 | ||
|  | 44418ef295 | ||
|  | 2a67218a34 | ||
|  | 911586ed0b | ||
|  | 9d6a6620e3 | ||
|  | 598d0acd8e | ||
|  | f16ece6207 | ||
|  | 9b55bc9892 | ||
|  | 707e67918b | ||
|  | faac572c30 | ||
|  | 571b37695b | ||
|  | 227adc459f | ||
|  | 2ee36f1a9c | ||
|  | 31830dc67d | ||
|  | d0ce2a46ac | ||
|  | 7e5bc4e1ce | ||
|  | d2b6d0a0ff | ||
|  | 542b0658b8 | ||
|  | e73c7e19b5 | ||
|  | 6a32ed7d7b | ||
|  | a63001f17c | ||
|  | 4d1ad9c832 | ||
|  | 455bf53ba6 | ||
|  | 454aa6ccda | ||
|  | 85ffebb3fa | ||
|  | bc99434574 | ||
|  | 9e86020ef7 | ||
|  | 6e9bb0c4f4 | ||
|  | d66a41a8a3 | ||
|  | 90914bff14 | ||
|  | 62414848f4 | ||
|  | d4ece6ecd7 | ||
|  | d1ec60bb63 | ||
|  | 4f672c736b | ||
|  | 2e5c351d8b | ||
|  | 3562553346 | ||
|  | 4750b292a5 | ||
|  | 3eb0561e90 | ||
|  | abb118c8ca | ||
|  | 2818a229b6 | ||
|  | a9b8af3677 | ||
|  | 0354da00da | ||
|  | b179587475 | ||
|  | 3021f90bc5 | ||
|  | a14b0278c8 | ||
|  | 80070b333e | ||
|  | 3aa8dcac11 | ||
|  | e920f05611 | ||
|  | 3594afd3aa | ||
|  | 9daaee8212 | ||
|  | d022707349 | ||
|  | 3948605ae6 | ||
|  | f2ded5fdd6 | ||
|  | 00b47be181 | ||
|  | a2fac5d946 | ||
|  | a00b5bb36b | ||
|  | d4fbc34085 | ||
|  | e9e3031992 | ||
|  | c2c7553f56 | ||
|  | 4e60cb89c9 | ||
|  | ec4523240f | ||
|  | 1655ddbcaa | ||
|  | 997c677f30 | ||
|  | d5fc8a2d7e | ||
|  | 3bcd0302a8 | ||
|  | de91b7e8af | ||
|  | 7efd1d7c9e | ||
|  | b5151a2178 | ||
|  | c8432020c6 | ||
|  | 2c9d413a1a | ||
|  | cdf842e7ad | ||
|  | c917007949 | ||
|  | 64278c6b3c | ||
|  | 10a01ed14a | ||
|  | ba3bd1407b | ||
|  | 73666c9a04 | ||
|  | eae24083c9 | ||
|  | a644510c27 | ||
|  | 57859d0da2 | ||
|  | 057f0ff648 | ||
|  | 05d1c867f2 | ||
|  | a2238fa435 | ||
|  | 12b7426a7c | ||
|  | 5148d613a7 | ||
|  | f455c15882 | ||
|  | 618fdabd0e | ||
|  | 3b69e2896c | ||
|  | 7306b63ab1 | ||
|  | 7e3133caa2 | ||
|  | 560901d714 | ||
|  | 166ce9ae78 | ||
|  | d3395a685e | ||
|  | 6d5e9a8566 | ||
|  | 69ec03feb4 | ||
|  | f92982cd5a | ||
|  | 5570f2b464 | ||
|  | ad19dc0240 | ||
|  | 9b1d4faff8 | ||
|  | 76756d20e9 | ||
|  | e564500480 | ||
|  | 19c15ce58d | ||
|  | a027785098 | ||
|  | 36a9f10aae | ||
|  | 99a11a4b53 | ||
|  | 55cac4465c | ||
|  | ff395fd074 | ||
|  | 972b6e09c7 | ||
|  | e793a33b15 | ||
|  | e70d4ff3f3 | ||
|  | cd0635d3a0 | ||
|  | 81702d8595 | ||
|  | aaa4a65b04 | ||
|  | 430797e626 | ||
|  | d454001f49 | ||
|  | bd90ee1f58 | ||
|  | 196aaa5427 | ||
|  | 6e42233b33 | ||
|  | 8e44df8525 | ||
|  | a8a1536941 | ||
|  | 99d1728c70 | ||
|  | 6bbb92cdb9 | ||
|  | b80e7c06bf | ||
|  | bf467b874c | ||
|  | 43c9f6be56 | ||
|  | 6811a4f4ae | ||
|  | 1f16dd9c43 | ||
|  | 63a43ce104 | ||
|  | bd7ce5417e | ||
|  | 941ee54a97 | ||
|  | a5d4a64f47 | ||
|  | d96fcd4a98 | ||
|  | de42e2f747 | ||
|  | 822a93aeb6 | ||
|  | c31b4aaeff | ||
|  | 8c9a386054 | ||
|  | 8c90933615 | ||
|  | 6f8c242333 | ||
|  | fe8b66873a | ||
|  | 00c5f1365a | ||
|  | f7d317328a | ||
|  | 3ccd705225 | ||
|  | 9e439fffaa | ||
|  | 859dc170e7 | ||
|  | 1932d8fad9 | ||
|  | 0c814ae436 | ||
|  | 89313d8a37 | ||
|  | 2b85722222 | ||
|  | 57e5b0188c | ||
|  | 2d7c830e70 | ||
|  | ccaa1790a9 | ||
|  | f6531d905e | ||
|  | 64a31879d3 | ||
|  | 0c6a4b1ed2 | ||
|  | 67801f39fe | ||
|  | 892a0d67bf | ||
|  | 9fc0b7d5cc | ||
|  | 22a614ef54 | ||
|  | cd257b8e4d | ||
|  | fa1ee2ca14 | ||
|  | 34ea1adde6 | ||
|  | 41cf8abb1f | ||
|  | c0ffec1a4c | ||
|  | 65779b8eaf | ||
|  | c47bdb2d56 | ||
|  | d47ae642e7 | ||
|  | 39c4609cc6 | ||
|  | 3ebba02a10 | ||
|  | 4dc7a96e79 | ||
|  | 5a49a29110 | ||
|  | 983a5c2034 | ||
|  | 15829f04a3 | ||
|  | 934618bc1c | ||
|  | 2c5ec75b88 | ||
|  | df11fd744f | ||
|  | 4dba0fb43d | ||
|  | 7a0d86b8dd | ||
|  | a94cd98e0f | ||
|  | 8e95e51edc | ||
|  | 6f1b00284a | ||
|  | 58549a6cac | ||
|  | acc9a6118f | ||
|  | c7811e861c | ||
|  | 55cf766ff0 | ||
|  | a1eaf38324 | ||
|  | c6788092d3 | ||
|  | f89f74ef3f | ||
|  | 3e40f02001 | ||
|  | c169967c1b | ||
|  | 2830e7c569 | ||
|  | 415f08ba3a | ||
|  | d726bcdc19 | ||
|  | f259c25a70 | ||
|  | 4db937cf1f | ||
|  | dad9d0660c | ||
|  | 0c450a5bb2 | ||
|  | ef59819c01 | ||
|  | c651e7c84b | ||
|  | 20b8debb1c | ||
|  | dd5743f0a1 | ||
|  | 7da2b51fae | ||
|  | 0236800392 | ||
|  | 4f822878f7 | ||
|  | c2810e5fe5 | ||
|  | b89ba4b801 | ||
|  | 07c680b839 | ||
|  | fd50db4eab | ||
|  | 0ee95b36a6 | ||
|  | b8cf07149e | ||
|  | 1b699f1a87 | ||
|  | d3bfd238d3 | ||
|  | 1f43abb3c8 | ||
|  | 287c753e4a | ||
|  | 8a5374d31a | ||
|  | e219eaa934 | ||
|  | fd314480ca | ||
|  | dd45396cf3 | ||
|  | 1e2a56c5e9 | ||
|  | 8011773af4 | ||
|  | ddc69c692e | ||
|  | df925c9744 | ||
|  | 1726341aad | ||
|  | 63b1ccc7a7 | ||
|  | ee5db31518 | ||
|  | e80397c857 | ||
|  | 81aa7ca1a4 | ||
|  | f0f7695890 | ||
|  | e7e8ce2f7a | ||
|  | ba37a3f18d | ||
|  | 60b11a7a5d | ||
|  | 29461c20a7 | ||
|  | 2ff1f34543 | ||
|  | b75d7f970f | ||
|  | 204681f097 | ||
|  | e239fe95a4 | ||
|  | 0a101f061a | ||
|  | f112a17afa | ||
|  | 54658a66d2 | ||
|  | 6b8f5a76e4 | ||
|  | 623a5d338d | ||
|  | 9c5565cfd5 | ||
|  | 722f2efaee | ||
|  | 4928264204 | ||
|  | 12d62ddc2a | ||
|  | da54e97217 | ||
|  | 9c0993dac8 | ||
|  | 175486b7c4 | ||
|  | 4760a287f6 | ||
|  | 0237b48c87 | ||
|  | 95c9f22e6c | ||
|  | 9b001219d5 | ||
|  | 6ff15efc7b | ||
|  | 6fe1dccc7e | ||
|  | 1c80f6f3fa | ||
|  | 54d3177fdd | ||
|  | a24ad245d2 | ||
|  | f38cfdcadf | ||
|  | 92e4ad8ccd | ||
|  | 3f3ab088d2 | ||
|  | 2c2cbaa175 | ||
|  | 911b6bf863 | ||
|  | 31462cab64 | ||
|  | 1ee35da62d | ||
|  | edf4815595 | ||
|  | 06ccee5d18 | ||
|  | d5ad85725f | ||
|  | 4d5bddb413 | ||
|  | 2f4da7c381 | ||
|  | 8b845fce03 | ||
|  | 9fd15c38a9 | ||
|  | ec1573d01f | ||
|  | 92ec1cc9e7 | ||
|  | 8b2f9665ce | ||
|  | cb388a5a78 | ||
|  | 7f4389ae08 | ||
|  | 76d71beaa2 | ||
|  | 31bb9c2197 | ||
|  | 6a2cd5c45a | ||
|  | 520632514b | ||
|  | f998b28d0b | ||
|  | 1a6587e9e6 | ||
|  | 9b4b729d19 | ||
|  | e80345295e | ||
|  | 026c259a2e | ||
|  | 63474c2269 | ||
|  | faa1a9312f | ||
|  | 23fa0726d5 | ||
|  | 22210eaf7d | ||
|  | dcd8bee676 | ||
|  | 06f0fa8f0e | ||
|  | 6d0f9e2cd5 | ||
|  | 732afdb65d | ||
|  | 1a9e8742f7 | ||
|  | b8eda37339 | ||
|  | 5107db6169 | ||
|  | 2c8f207454 | ||
|  | 489bc9c3b3 | ||
|  | 514713e883 | ||
|  | 17cc0cd09c | ||
|  | 4475df1295 | ||
|  | fdad267cfd | ||
|  | 3684fc80f0 | ||
|  | e97a5fef94 | ||
|  | de2972631f | ||
|  | e5b8fd67c8 | ||
|  | 5fade89e2d | ||
|  | 2eefedadb3 | ||
|  | e63d7a0b8a | ||
|  | 2a1b1849fa | ||
|  | 0461cb7f19 | ||
|  | 0932e0be03 | ||
|  | 4638ac9474 | ||
|  | d8d7255029 | ||
|  | fa05276c3f | ||
|  | e50a5d51d8 | ||
|  | c03ba78587 | ||
|  | ff07c69e7d | ||
|  | 735b84b26d | ||
|  | 8dd069ad67 | ||
|  | 1857e68003 | ||
|  | ff2508382a | ||
|  | 9cb952b116 | ||
|  | 105e8089bb | ||
|  | 730f37f247 | ||
|  | 284716751f | ||
|  | 8d0db699bf | ||
|  | 53cf1cae58 | ||
|  | 307e4719e0 | ||
|  | 5effae787a | ||
|  | 6532be0b52 | ||
|  | fb225a5347 | ||
|  | b83830a45e | ||
|  | ca28288c33 | ||
|  | b6f8d9cb25 | ||
|  | 9cad0f11e5 | ||
|  | 807be08566 | ||
|  | 67f6a985f8 | ||
|  | f87d54ae8d | ||
|  | d894bf7271 | ||
|  | 56e0e5cace | ||
|  | 685084e784 | ||
|  | cbeec5a973 | ||
|  | 3fff56bcd7 | ||
|  | c504c23eec | ||
|  | 16dae5a655 | ||
|  | e512c5ae7d | ||
|  | 094078b928 | ||
|  | 34fc3ff919 | ||
|  | 4391f48e78 | ||
|  | 775608a3c0 | ||
|  | b326228901 | ||
|  | b2e98173a8 | ||
|  | 65c9b7952c | ||
|  | b9dc9e7d62 | ||
|  | ce178d0354 | ||
|  | a3ff6efebc | ||
|  | 6a9bc56723 | ||
|  | c9ac158d25 | ||
|  | 4b937a0fe8 | ||
|  | 405bf26ac5 | ||
|  | 5dcda0e0a0 | ||
|  | 83e9b60308 | ||
|  | 10b40b4730 | ||
|  | 79d6d804ef | ||
|  | e9c7b6d8f8 | ||
|  | 4fcfbfb3f4 | ||
|  | 30cde14ed3 | ||
|  | cf76e6f538 | ||
|  | d0f600ec8d | ||
|  | 675f9e956f | ||
|  | 381605a6bb | ||
|  | 0fce66062b | ||
|  | 747cc9e5da | ||
|  | 25a1b464da | ||
|  | 3b6738b547 | ||
|  | fc93e3e97f | ||
|  | 0edbb13d48 | ||
|  | 673687341c | ||
|  | 3969208942 | ||
|  | 3fa89b58df | ||
|  | a43a9c8543 | ||
|  | 45deda4dea | ||
|  | 6ec46f02a9 | ||
|  | d643c17ff1 | ||
|  | e5de89c6b4 | ||
|  | c21e7c632d | ||
|  | 6ae771682a | ||
|  | bf2075b902 | ||
|  | 62ec8c8f76 | ||
|  | b84d4a99b8 | ||
|  | cce9dfe585 | ||
|  | 166be395b9 | ||
|  | fa3f5f8d68 | ||
|  | 2926b68c32 | ||
|  | a55f187958 | ||
|  | c76d263375 | ||
|  | 6740d97f8f | ||
|  | b079eebe79 | ||
|  | 363e48a1e8 | ||
|  | f60e4e3e4f | ||
|  | 1b02974efa | ||
|  | 496abdd230 | ||
|  | bc495d77d1 | ||
|  | fb54d4bb64 | ||
|  | 0786163dc3 | ||
|  | ed85611e75 | ||
|  | 86ebfce44a | ||
|  | dae51cff51 | ||
|  | 358a2e7220 | ||
|  | d45353e8c8 | ||
|  | 2f56e4e3a1 | ||
|  | 0e503f8273 | ||
|  | 876fe803f5 | ||
|  | 6adb9678b6 | ||
|  | 39bf7ba4a9 | ||
|  | 5da6e2ff99 | ||
|  | 44603c41a2 | ||
|  | 0feb982a73 | ||
|  | d93cb32f2e | ||
|  | 40c47eace2 | ||
|  | 509bdd879c | ||
|  | b98ebb6e9f | ||
|  | 924ddecff0 | ||
|  | ca64fd218d | ||
|  | 9b12b55acd | ||
|  | 450239564a | ||
|  | bb1cc62d2a | ||
|  | b4875c1e2d | ||
|  | a21440d663 | ||
|  | eb6836b63c | ||
|  | b39a2690c1 | ||
|  | 706902da1c | ||
|  | d5104b5d27 | ||
|  | a13ae5c4b1 | ||
|  | a92d1d9958 | ||
|  | 10852a9427 | ||
|  | b757ce1e38 | ||
|  | 91e75f3fa2 | ||
|  | 6c8e55eb2f | ||
|  | f821f700fa | ||
|  | d76d24408f | ||
|  | 7ad85dfe1c | ||
|  | 7d8be0a719 | ||
|  | bac15c18e4 | ||
|  | 2f266d39e6 | ||
|  | 5726d1fc52 | ||
|  | 69aee1823e | ||
|  | e6a0ae5f57 | ||
|  | e5df566c7a | ||
|  | 81e173b609 | ||
|  | d0ebcc6606 | ||
|  | 99c3fcf42a | ||
|  | 794666e7cc | ||
|  | 45abe4955d | ||
|  | 7eed421c70 | ||
|  | 69f7c397c2 | ||
|  | d2d136e922 | ||
|  | 396e435ae0 | ||
|  | 45d8e9102a | ||
|  | 12a51deffa | ||
|  | f2f69abec2 | ||
|  | 02b7f962e9 | ||
|  | eb813e6b22 | ||
|  | 5ddc604341 | ||
|  | 313e672e93 | ||
|  | ce77ad6de4 | ||
|  | bea22690b1 | ||
|  | c9a52bd7d0 | ||
|  | a244a341ec | ||
|  | 2b47870032 | ||
|  | de9e35ae6a | ||
|  | 1a6fec8ca9 | ||
|  | 094054cd99 | ||
|  | f85b8a81f1 | ||
|  | a44eaebf7c | ||
|  | f37b3c063e | ||
|  | 6e5d5a3b82 | ||
|  | bf0562d619 | ||
|  | ecaa81be3c | ||
|  | d98ae48935 | ||
|  | f52a76b16c | ||
|  | d421c27602 | ||
|  | 70e4cd4de1 | ||
|  | 29767e9265 | ||
|  | 46d4c7f96d | ||
|  | 161a6f3923 | ||
|  | 53e912341b | ||
|  | 19396ea11a | ||
|  | 1d9a5e742b | ||
|  | e8dfdd03f7 | ||
|  | 2f5b15dac7 | ||
|  | 525e1f5136 | ||
|  | 7d63d188af | ||
|  | 87889c12ea | ||
|  | 53d023f5ee | ||
|  | 1877ab8c67 | ||
|  | 72a5a8cab7 | ||
|  | 221e49a978 | ||
|  | 1a4c67d173 | ||
|  | 42fd23ece3 | ||
|  | 3035c0712a | ||
|  | 61315f8bfd | ||
|  | 52683124d8 | ||
|  | 1f77390366 | ||
|  | 322d492540 | ||
|  | f977d8cca9 | ||
|  | a9aedea2bd | ||
|  | 5560bbeecb | ||
|  | f226206703 | ||
|  | 170687226d | ||
|  | d56d3dc271 | ||
|  | 32a202aff4 | ||
|  | 6ee75e6e60 | ||
|  | 13d74cae3b | ||
|  | 88651916b0 | ||
|  | be12505d2f | ||
|  | 23fcf3b045 | ||
|  | 9e7459b204 | ||
|  | 4f0eb1d566 | ||
|  | ce00481f47 | ||
|  | f596af90ba | ||
|  | 5c74d1d021 | ||
|  | aff659b6b6 | ||
|  | 58724d95fa | ||
|  | 8d61fcd5c9 | ||
|  | 3e1be53c36 | ||
|  | f3754588bd | ||
|  | c4ffffeec8 | ||
|  | 5b69f6a358 | ||
|  | 1af89a7447 | ||
|  | 90abd81035 | ||
|  | 898824b13f | ||
|  | 9d093aa7f8 | ||
|  | 1770549f6c | ||
|  | d21be77fd2 | ||
|  | 41a1c19877 | ||
|  | 9b6571ce68 | ||
|  | 88e98e4e35 | ||
|  | 10c56ffbfa | ||
|  | cb2c8d6f3c | ||
|  | ca62b850ce | ||
|  | 5a75d4e140 | ||
|  | e0972b7c24 | ||
|  | 0db497916d | ||
|  | 23a0ad3c4e | ||
|  | 2b4e1c4b67 | ||
|  | 9b1b9244cf | ||
|  | ad570e9b16 | ||
|  | 812ba6de62 | ||
|  | 8f97124adb | ||
|  | 28289838f9 | ||
|  | cca8a010c3 | ||
|  | 91ab296692 | ||
|  | ee6c9c4272 | ||
|  | 21cd36fa92 | ||
|  | b1aafe3dbc | ||
|  | 5cd832de89 | ||
|  | 24dd9d0518 | ||
|  | aab6ab810a | ||
|  | d1d6d5e71e | ||
|  | e67dd68522 | ||
|  | e25eae846d | ||
|  | 995eeaa455 | ||
|  | 240c61b967 | ||
|  | 2d8b0753b4 | ||
|  | 44eab3de7f | ||
|  | 007be5bf95 | ||
|  | ee19c7c51f | ||
|  | ce56afbdf9 | ||
|  | 51012695a1 | ||
|  | 0eef2d2cc5 | ||
|  | 487f9f2815 | ||
|  | d065adcd8e | ||
|  | 0d9a1dc5eb | ||
|  | 8f9ad15108 | ||
|  | e538e9b843 | ||
|  | 4a702b6813 | ||
|  | 1e6fd2c57a | ||
|  | 600b959d89 | ||
|  | b96de9eb13 | ||
|  | 93be19b647 | ||
|  | 74f45f6f1d | ||
|  | 54ba3d2888 | ||
|  | 65d5149f60 | ||
|  | 917ebb3771 | ||
|  | 7e66b1f545 | ||
|  | 05837dca35 | ||
|  | 53be2ebe59 | ||
|  | 0341efcaea | ||
|  | ec75210fd3 | ||
|  | e6afe3e806 | ||
|  | 5aa46f068e | ||
|  | a11a5b28bc | ||
|  | 907aa566ca | ||
|  | 5c21f099a8 | ||
|  | b91201ae3e | ||
|  | 56d7e19968 | ||
|  | cf91c6c90e | ||
|  | 9011148adf | ||
|  | 897d0590d2 | ||
|  | 33b33e8458 | ||
|  | 7758f5c187 | ||
|  | 83d7a03ba4 | ||
|  | a9a0df9699 | ||
|  | df44f8f5f8 | ||
|  | 216a9ed035 | ||
|  | 35d61b6a6c | ||
|  | 5fb72cea53 | ||
|  | d54d021e9f | ||
|  | 06e78311df | ||
|  | df720f95ca | ||
|  | 00faff34d3 | ||
|  | 2b5b3ea4f3 | ||
|  | 95e608d0b4 | ||
|  | 1d55bf87dd | ||
|  | 1220ce53eb | ||
|  | 2006218f87 | ||
|  | 40f427a387 | ||
|  | 445e95baed | ||
|  | 67fbc9ad33 | ||
|  | 1253e9e465 | ||
|  | 21069432e8 | ||
|  | 6facf6a324 | ||
|  | 7556197485 | ||
|  | 8dddd2d896 | ||
|  | f319c95c2b | ||
|  | 8e972b0907 | ||
|  | 395e400215 | ||
|  | 3685e3111f | ||
|  | 7bb1c75dc6 | ||
|  | b20834929c | ||
|  | 181891757e | ||
|  | b16feeae44 | ||
|  | 684e049f27 | ||
|  | 8cebd901b2 | ||
|  | 3c96beb8fb | ||
|  | 8a46459cf9 | ||
|  | be5c3e9daa | ||
|  | e44453877c | ||
|  | f772a4ec56 | ||
|  | 44182ec683 | ||
|  | b9ab13fa53 | ||
|  | 2ad6721c95 | ||
|  | b7d0604e62 | ||
|  | a7518b4b26 | ||
|  | 50613f5d3e | ||
|  | f814767703 | ||
|  | 4af86d6456 | ||
|  | f0a4f00c2d | ||
|  | 4321affddb | ||
|  | 926ed55b9b | ||
|  | 2ebf308565 | ||
|  | 1c5e736dce | ||
|  | b591f9f5b7 | ||
|  | 9724882578 | ||
|  | ddef2df101 | ||
|  | 8af69c4284 | ||
|  | 6ebe1ab467 | ||
|  | 24e4d9cf6d | ||
|  | f35fa0aa58 | ||
|  | 4942f262f1 | ||
|  | a20b1a973e | ||
|  | eae5e00706 | ||
|  | 403762d862 | ||
|  | 5c92d4b454 | ||
|  | 38179b9d38 | ||
|  | 8f510dde5a | ||
|  | be42d56e37 | ||
|  | 6294530fa3 | ||
|  | c5c8f5fab1 | ||
|  | 3d41d79078 | ||
|  | 3005061a11 | ||
|  | 65ea46f457 | ||
|  | eca8f32570 | ||
|  | 8d1ef19c61 | ||
|  | 71d87d866b | ||
|  | c4f88bdce7 | ||
|  | f722a115b1 | ||
|  | 1583beea7b | ||
|  | 5b388c587b | ||
|  | e254923167 | ||
|  | b0dbdd7803 | ||
|  | aa6ebe0122 | ||
|  | c5f179bab8 | ||
|  | e65cb86638 | ||
|  | a349998640 | ||
|  | 43f60610b8 | ||
|  | 46d042087a | ||
|  | ee214727f6 | ||
|  | b4c1ec55ec | ||
|  | 0fdd54f710 | ||
|  | 4f0cdeaec0 | ||
|  | e5cc38857c | ||
|  | fe4b9d71c0 | ||
|  | 5c1181e40e | ||
|  | 8b71832bc2 | ||
|  | 8412ed6065 | ||
|  | 207f6cdc7c | ||
|  | b0b51f5730 | ||
|  | def6833ef0 | ||
|  | c528dd3de1 | ||
|  | 544270e35d | ||
|  | 657e029fee | ||
|  | 49469d7689 | ||
|  | 4f0dd452c8 | ||
|  | 3f741eab11 | ||
|  | 190368788f | ||
|  | 8306a3f566 | ||
|  | 988c134c09 | ||
|  | af0a4d578b | ||
|  | 9bc0abc831 | ||
|  | 41410e99e7 | ||
|  | deae04d5ff | ||
|  | 7d6eeffd66 | ||
|  | 629858e095 | ||
|  | dfdb628347 | ||
|  | 6e48b28fc9 | ||
|  | 3ba450e837 | ||
|  | 688ed93500 | ||
|  | 7268ba20a2 | ||
|  | 63d9e73098 | ||
|  | 564c048f90 | ||
|  | 5f801c74d5 | ||
|  | b405fbc09a | ||
|  | 7a64c2eb49 | ||
|  | c93cbac3b1 | ||
|  | 8b0f67b8a6 | ||
|  | 0d96129f2d | ||
|  | 54ee12d2b3 | ||
|  | 92fc042103 | ||
|  | 9bb7016fa7 | ||
|  | 3ad56feafb | ||
|  | 14d59c3dec | ||
|  | 443f419770 | ||
|  | ddbb58755e | ||
|  | 524283b9ff | ||
|  | fb178d2944 | ||
|  | 52f4ad9403 | ||
|  | ba0c08ef1f | ||
|  | 9e19b1e04c | ||
|  | b2118201b1 | ||
|  | b4346aa056 | ||
|  | b599f05aab | ||
|  | 93d78a0200 | ||
|  | 449957b2eb | ||
|  | 0a6d44bad3 | ||
|  | 17ceaaa503 | ||
|  | d70803b416 | ||
|  | aa414d4702 | ||
|  | f24e1b91ea | ||
|  | 1df8163090 | ||
|  | 659ddf6a45 | ||
|  | e110068da4 | ||
|  | c943f6f936 | ||
|  | cb1fe7fe54 | ||
|  | 593f1f63cc | ||
|  | 66aa70cf75 | ||
|  | 304be99067 | ||
|  | 9a01ec35f4 | ||
|  | bfa5b4fba5 | ||
|  | d2f63ef353 | ||
|  | 50f334425e | ||
|  | f78212073c | ||
|  | 5c655f5a82 | ||
|  | 6a6446bfcb | ||
|  | b60a3a5e50 | ||
|  | 02ccbab8e5 | ||
|  | 023ff3f964 | ||
|  | 7c5e8df3b8 | ||
|  | 56fdab260b | ||
|  | 7cce49dc1a | ||
|  | 2dfaafb20b | ||
|  | 6138a5bf54 | ||
|  | 828c67cc00 | ||
|  | e70cd44e18 | ||
|  | efa5ac5edd | ||
|  | 788b11e759 | ||
|  | d049d7a61f | ||
|  | 075c833b58 | ||
|  | e9309c2a96 | ||
|  | a592d2b397 | ||
|  | 3ad1805ac0 | ||
|  | dbc2bab698 | ||
|  | 79eec5c299 | ||
|  | 7754b0c575 | ||
|  | be4289ce76 | ||
|  | 67f5226270 | ||
|  | b6d77c581b | ||
|  | d84bf47d04 | ||
|  | aba3a7bb9e | ||
|  | 6281736d89 | ||
|  | 94d96f89d3 | ||
|  | 4b55f9dead | ||
|  | 5c6dce94df | ||
|  | f7d8f9c7f5 | ||
|  | 053df24f9c | ||
|  | 1dc470e434 | ||
|  | cfd8773267 | ||
|  | 67045cf6c1 | ||
|  | ddfb9e7239 | ||
|  | 9f6eed5472 | ||
|  | 15a1e2ebcb | ||
|  | fcfe450b07 | ||
|  | a69bbb3bc9 | ||
|  | 6d2559cfc1 | ||
|  | b3a62615f3 | ||
|  | 57f5cca1cb | ||
|  | 6b9851f540 | ||
|  | 36fd203a88 | ||
|  | 3f5cb5d61c | ||
|  | 862fc6a946 | ||
|  | 92c386ac0e | ||
|  | 98a11a3645 | ||
|  | 62be0ed936 | ||
|  | b7de73fd8a | ||
|  | e2413f1af2 | ||
|  | 0e77d575c4 | ||
|  | ba42c5e367 | ||
|  | 6a06734192 | ||
|  | 5e26a406b7 | ||
|  | b6dd03138d | ||
|  | cf03ee03ee | ||
|  | 0e665b6bf0 | ||
|  | e3d0de7313 | ||
|  | bcf3a543a1 | ||
|  | b27f17c74a | ||
|  | 75d864771e | ||
|  | 6420060f2a | ||
|  | c149ae71b9 | ||
|  | 3a49dd034c | ||
|  | b26d7e82e3 | ||
|  | 415abdf0ce | ||
|  | f7f6f6ecb2 | ||
|  | 43d54f134a | ||
|  | 0d2606a13b | ||
|  | 1deb10dc88 | ||
|  | 1236d55544 | ||
|  | ecccf39455 | ||
|  | 8e0316825a | ||
|  | aa45fa87af | ||
|  | 71e78bd0c5 | ||
|  | 4766477c58 | ||
|  | d97e49ff2b | ||
|  | 6b9d775cb9 | ||
|  | e521f580d7 | ||
|  | 25e7cf7db0 | ||
|  | 0cab33787d | ||
|  | bc6faf817f | ||
|  | d46ae55863 | ||
|  | bbd900ab25 | ||
|  | 129ae93e2b | ||
|  | 44dd59fa3f | ||
|  | ec4e7559b0 | ||
|  | dce40611cf | ||
|  | e71b8546f9 | ||
|  | f827348467 | ||
|  | f3978343db | ||
|  | 2654a7ea70 | ||
|  | 1068bf4ef7 | ||
|  | e7fccc97cc | ||
|  | 733e289852 | ||
|  | 29d71a104c | ||
|  | 05200420ad | ||
|  | eb762d4bfd | ||
|  | 58ace9eda1 | ||
|  | eeb2623be0 | ||
|  | cfa242c2fe | ||
|  | ec0441ccc2 | ||
|  | ae2782a8fe | ||
|  | 58ff570251 | ||
|  | 7b554b12c7 | ||
|  | 58f7603d4f | ||
|  | 8895994c54 | ||
|  | de8f7e36d5 | ||
|  | 88d7a50265 | ||
|  | 21e19fc7e5 | ||
|  | faf4935a69 | ||
|  | 71a1f9d74a | ||
|  | bd8d523e10 | ||
|  | 60cae0e3ac | ||
|  | 5a342ac012 | ||
|  | bb8767dfc3 | ||
|  | fcb2779c15 | ||
|  | 77dd6c1f61 | ||
|  | 8118eef300 | ||
|  | 802d1489fe | ||
|  | 443a029185 | ||
|  | 4ee508fdd0 | ||
|  | aa5608f7e8 | ||
|  | cc472b4613 | ||
|  | 764b945ddc | ||
|  | fd2206ce4c | ||
|  | 48c0ac9f00 | ||
|  | 84eb4fe9ed | ||
|  | 4a5428812c | ||
|  | 023f98a89d | ||
|  | 66893dd0c1 | ||
|  | 25a6666e35 | ||
|  | 19d75309b5 | ||
|  | 11110d65c1 | ||
|  | a348f58fe2 | ||
|  | 13851dd976 | ||
|  | 2ec37c5da9 | ||
|  | 8c127160de | ||
|  | 2af820de9a | ||
|  | 55fb0bb3a0 | ||
|  | 9f9ecc521f | ||
|  | dfd01df5ba | ||
|  | 474090698c | ||
|  | 6b71cdeea4 | ||
|  | 581e974236 | ||
|  | ba3c3a42ce | ||
|  | c8bc5671c5 | ||
|  | ff9401a040 | ||
|  | 5e1bc1989f | ||
|  | a1dc91cd7d | ||
|  | 99f2772bb3 | ||
|  | e5d0e42655 | ||
|  | 2c914cc374 | ||
|  | 9bceb62381 | ||
|  | de7518a800 | ||
|  | 304fb63453 | ||
|  | 0f7ef60ca0 | ||
|  | 07c74e4641 | ||
|  | de7f325cfb | ||
|  | 42cdf70cb4 | ||
|  | 6beb6be131 | ||
|  | fa4fc2a708 | ||
|  | 2db9758260 | ||
|  | 715982e40a | ||
|  | d00cd4453a | ||
|  | 429c08c24a | ||
|  | 6a71490e20 | ||
|  | 9bceda0646 | ||
|  | a1027a6773 | ||
|  | 302d4b75f9 | ||
|  | 5f6ee0e883 | ||
|  | 27f9720de1 | ||
|  | 22aa3fdbbc | ||
|  | 069ecdd33f | ||
|  | dd545ae933 | ||
|  | 6650b705c4 | ||
|  | 59b0350289 | ||
|  | 1ad159f820 | ||
|  | 0bf42190e9 | ||
|  | d2fa836232 | ||
|  | c387774093 | ||
|  | e99736ba3c | ||
|  | 16cb54fcc9 | ||
|  | 5aa15c51ec | ||
|  | a8aedd9cf3 | ||
|  | b851b632bc | ||
|  | 541e07fb65 | ||
|  | 6ad16a897d | ||
|  | 72f1053a93 | ||
|  | fb15a2762c | ||
|  | 9165248b91 | ||
|  | add18b29db | ||
|  | 1971653548 | ||
|  | 392cd64d7b | ||
|  | b5affbb7c8 | ||
|  | 71d1206277 | ||
|  | 26e6a8c409 | ||
|  | eb54fae11a | ||
|  | ee773e5966 | ||
|  | 7218ccdba8 | ||
|  | 332400e48a | ||
|  | ad1a5d3702 | ||
|  | 3006b4184d | ||
|  | 84eb84a080 | ||
|  | 60beea548b | ||
|  | 5f9c149e59 | ||
|  | 53367c6f04 | ||
|  | d7f817ee44 | ||
|  | d33a87da54 | ||
|  | 3aebfb12b7 | ||
|  | 1d6c55ffa6 | ||
|  | 5e7080aac3 | ||
|  | fad739bc01 | ||
|  | c6b7f23884 | ||
|  | a6f7e446de | ||
|  | 89d95d3ae1 | ||
|  | 764208698f | ||
|  | 57129cf934 | ||
|  | aae1a842d5 | ||
|  | 623f35aec7 | ||
|  | 870bf842cf | ||
|  | 07f2d7dd5c | ||
|  | f223f2edc5 | ||
|  | e848a9a577 | ||
|  | 7569d98e07 | ||
|  | 596dee2f24 | ||
|  | 9970403964 | ||
|  | 07a88ae00d | ||
|  | 5475b4d287 | ||
|  | 6631dcfd3e | ||
|  | 0dd3f337f3 | ||
|  | 8eb27b5875 | ||
|  | 2d1863031c | ||
|  | 9feb76ca81 | ||
|  | 993e8f4ab3 | ||
|  | e08ae95d4f | ||
|  | 15359e8846 | ||
|  | d1457b312b | ||
|  | c9dd2af196 | ||
|  | 564ef4e688 | ||
|  | a33e6e8bb5 | ||
|  | cf34f33f04 | ||
|  | 827cfe4e8f | ||
|  | 2ce1c2383c | ||
|  | 6fc0a665ae | ||
|  | 4f16d01263 | ||
|  | 67cc37354a | ||
|  | e388243ef4 | ||
|  | 3dc92763c7 | ||
|  | dfe97dd466 | ||
|  | 2803cee29b | ||
|  | 3a03020e54 | ||
|  | 64443cc703 | ||
|  | 4d1aa6ed18 | ||
|  | 84837e88d2 | ||
|  | ff49c936ea | ||
|  | e6e0901329 | ||
|  | 23b6284b51 | ||
|  | 33dfbcbe32 | ||
|  | 700c23d537 | ||
|  | 369fac9e38 | ||
|  | 2229eb1167 | ||
|  | a3dec841b6 | ||
|  | b17620bdb6 | ||
|  | f39cd5ae2f | ||
|  | 83a19e005b | ||
|  | a9dd01b0c8 | ||
|  | eb59afa1d1 | ||
|  | 2adcfce9d0 | ||
|  | 314ab9b304 | ||
|  | 8576fb82c7 | ||
|  | 0f95a6bb2f | ||
|  | ad5104567d | ||
|  | ece68ba1d5 | ||
|  | acccd3a586 | ||
|  | 8ebef1c1ca | ||
|  | 28abc0d5ed | ||
|  | 1efe25d3ec | ||
|  | c40e4f8e4b | ||
|  | baca84092d | ||
|  | 346d4da059 | ||
|  | 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 | 
| @@ -23,6 +23,9 @@ POSTGRES_USER=postgres | ||||
| POSTGRES_PASS=postgrespass | ||||
|  | ||||
| # DEV SETTINGS | ||||
| APP_PORT=80 | ||||
| APP_PORT=443 | ||||
| API_PORT=80 | ||||
| HTTP_PROTOCOL=https | ||||
| DOCKER_NETWORK=172.21.0.0/24 | ||||
| DOCKER_NGINX_IP=172.21.0.20 | ||||
| NATS_PORTS=4222:4222 | ||||
|   | ||||
| @@ -1,4 +1,11 @@ | ||||
| FROM python:3.9.2-slim | ||||
| # pulls community scripts from git repo | ||||
| FROM python:3.10-slim AS GET_SCRIPTS_STAGE | ||||
|  | ||||
| RUN apt-get update && \ | ||||
|     apt-get install -y --no-install-recommends git && \ | ||||
|     git clone https://github.com/amidaware/community-scripts.git /community-scripts | ||||
|  | ||||
| FROM python:3.10-slim | ||||
|  | ||||
| ENV TACTICAL_DIR /opt/tactical | ||||
| ENV TACTICAL_READY_FILE ${TACTICAL_DIR}/tmp/tactical.ready | ||||
| @@ -8,17 +15,24 @@ ENV VIRTUAL_ENV ${WORKSPACE_DIR}/api/tacticalrmm/env | ||||
| ENV PYTHONDONTWRITEBYTECODE=1 | ||||
| ENV PYTHONUNBUFFERED=1 | ||||
|  | ||||
| EXPOSE 8000 8383 | ||||
| EXPOSE 8000 8383 8005 | ||||
|  | ||||
| RUN apt-get update && \ | ||||
|     apt-get install -y build-essential | ||||
|  | ||||
| RUN groupadd -g 1000 tactical && \ | ||||
|     useradd -u 1000 -g 1000 tactical | ||||
|  | ||||
| # Copy Dev python reqs | ||||
| COPY ./requirements.txt / | ||||
| # copy community scripts | ||||
| COPY --from=GET_SCRIPTS_STAGE /community-scripts /community-scripts | ||||
|  | ||||
| # Copy Docker Entrypoint | ||||
| COPY ./entrypoint.sh / | ||||
| # Copy dev python reqs | ||||
| COPY .devcontainer/requirements.txt  / | ||||
|  | ||||
| # Copy docker entrypoint.sh | ||||
| COPY .devcontainer/entrypoint.sh / | ||||
| RUN chmod +x /entrypoint.sh | ||||
|  | ||||
| ENTRYPOINT ["/entrypoint.sh"] | ||||
|  | ||||
| WORKDIR ${WORKSPACE_DIR}/api/tacticalrmm | ||||
|   | ||||
| @@ -1,19 +0,0 @@ | ||||
| version: '3.4' | ||||
|  | ||||
| services: | ||||
|   api-dev: | ||||
|     image: api-dev | ||||
|     build: | ||||
|       context: . | ||||
|       dockerfile: ./api.dockerfile | ||||
|     command: ["sh", "-c", "pip install debugpy -t /tmp && python /tmp/debugpy --wait-for-client --listen 0.0.0.0:5678 manage.py runserver 0.0.0.0:8000 --nothreading --noreload"] | ||||
|     ports: | ||||
|       - 8000:8000 | ||||
|       - 5678:5678 | ||||
|     volumes: | ||||
|       - tactical-data-dev:/opt/tactical | ||||
|       - ..:/workspace:cached | ||||
|     networks: | ||||
|       dev: | ||||
|         aliases:  | ||||
|           - tactical-backend | ||||
| @@ -5,10 +5,11 @@ services: | ||||
|     container_name: trmm-api-dev | ||||
|     image: api-dev | ||||
|     restart: always | ||||
|     user: 1000:1000 | ||||
|     build: | ||||
|       context: . | ||||
|       dockerfile: ./api.dockerfile | ||||
|     command: ["tactical-api"] | ||||
|       context: .. | ||||
|       dockerfile: .devcontainer/api.dockerfile | ||||
|     command: [ "tactical-api" ] | ||||
|     environment: | ||||
|       API_PORT: ${API_PORT} | ||||
|     ports: | ||||
| @@ -18,12 +19,12 @@ services: | ||||
|       - ..:/workspace:cached | ||||
|     networks: | ||||
|       dev: | ||||
|         aliases:  | ||||
|         aliases: | ||||
|           - tactical-backend | ||||
|  | ||||
|   app-dev: | ||||
|     container_name: trmm-app-dev | ||||
|     image: node:14-alpine | ||||
|     image: node:16-alpine | ||||
|     restart: always | ||||
|     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 | ||||
| @@ -33,7 +34,7 @@ services: | ||||
|       - "8080:${APP_PORT}" | ||||
|     networks: | ||||
|       dev: | ||||
|         aliases:  | ||||
|         aliases: | ||||
|           - tactical-frontend | ||||
|  | ||||
|   # nats | ||||
| @@ -41,12 +42,13 @@ services: | ||||
|     container_name: trmm-nats-dev | ||||
|     image: ${IMAGE_REPO}tactical-nats:${VERSION} | ||||
|     restart: always | ||||
|     user: 1000:1000 | ||||
|     environment: | ||||
|       API_HOST: ${API_HOST} | ||||
|       API_PORT: ${API_PORT} | ||||
|       DEV: 1 | ||||
|     ports: | ||||
|       - "4222:4222" | ||||
|       - "${NATS_PORTS}" | ||||
|     volumes: | ||||
|       - tactical-data-dev:/opt/tactical | ||||
|       - ..:/workspace:cached | ||||
| @@ -61,13 +63,14 @@ services: | ||||
|     container_name: trmm-meshcentral-dev | ||||
|     image: ${IMAGE_REPO}tactical-meshcentral:${VERSION} | ||||
|     restart: always | ||||
|     environment:  | ||||
|     user: 1000:1000 | ||||
|     environment: | ||||
|       MESH_HOST: ${MESH_HOST} | ||||
|       MESH_USER: ${MESH_USER} | ||||
|       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: | ||||
| @@ -84,6 +87,7 @@ services: | ||||
|     container_name: trmm-mongodb-dev | ||||
|     image: mongo:4.4 | ||||
|     restart: always | ||||
|     user: 1000:1000 | ||||
|     environment: | ||||
|       MONGO_INITDB_ROOT_USERNAME: ${MONGODB_USER} | ||||
|       MONGO_INITDB_ROOT_PASSWORD: ${MONGODB_PASSWORD} | ||||
| @@ -115,7 +119,11 @@ services: | ||||
|   redis-dev: | ||||
|     container_name: trmm-redis-dev | ||||
|     restart: always | ||||
|     user: 1000:1000 | ||||
|     command: redis-server --appendonly yes | ||||
|     image: redis:6.0-alpine | ||||
|     volumes: | ||||
|       - redis-data-dev:/data | ||||
|     networks: | ||||
|       dev: | ||||
|         aliases: | ||||
| @@ -124,11 +132,8 @@ services: | ||||
|   init-dev: | ||||
|     container_name: trmm-init-dev | ||||
|     image: api-dev | ||||
|     build: | ||||
|       context: . | ||||
|       dockerfile: ./api.dockerfile | ||||
|     restart: on-failure | ||||
|     command: ["tactical-init-dev"] | ||||
|     command: [ "tactical-init-dev" ] | ||||
|     environment: | ||||
|       POSTGRES_USER: ${POSTGRES_USER} | ||||
|       POSTGRES_PASS: ${POSTGRES_PASS} | ||||
| @@ -147,17 +152,18 @@ services: | ||||
|       - dev | ||||
|     volumes: | ||||
|       - tactical-data-dev:/opt/tactical | ||||
|       - mesh-data-dev:/meshcentral-data | ||||
|       - redis-data-dev:/redis/data | ||||
|       - mongo-dev-data:/mongo/data/db | ||||
|       - ..:/workspace:cached | ||||
|  | ||||
|   # container for celery worker service | ||||
|   celery-dev: | ||||
|     container_name: trmm-celery-dev | ||||
|     image: api-dev | ||||
|     build: | ||||
|       context: . | ||||
|       dockerfile: ./api.dockerfile | ||||
|     command: ["tactical-celery-dev"] | ||||
|     command: [ "tactical-celery-dev" ] | ||||
|     restart: always | ||||
|     user: 1000:1000 | ||||
|     networks: | ||||
|       - dev | ||||
|     volumes: | ||||
| @@ -171,11 +177,9 @@ services: | ||||
|   celerybeat-dev: | ||||
|     container_name: trmm-celerybeat-dev | ||||
|     image: api-dev | ||||
|     build: | ||||
|       context: . | ||||
|       dockerfile: ./api.dockerfile | ||||
|     command: ["tactical-celerybeat-dev"] | ||||
|     command: [ "tactical-celerybeat-dev" ] | ||||
|     restart: always | ||||
|     user: 1000:1000 | ||||
|     networks: | ||||
|       - dev | ||||
|     volumes: | ||||
| @@ -189,11 +193,9 @@ services: | ||||
|   websockets-dev: | ||||
|     container_name: trmm-websockets-dev | ||||
|     image: api-dev | ||||
|     build: | ||||
|       context: . | ||||
|       dockerfile: ./api.dockerfile | ||||
|     command: ["tactical-websockets-dev"] | ||||
|     command: [ "tactical-websockets-dev" ] | ||||
|     restart: always | ||||
|     user: 1000:1000 | ||||
|     networks: | ||||
|       dev: | ||||
|         aliases: | ||||
| @@ -210,6 +212,7 @@ services: | ||||
|     container_name: trmm-nginx-dev | ||||
|     image: ${IMAGE_REPO}tactical-nginx:${VERSION} | ||||
|     restart: always | ||||
|     user: 1000:1000 | ||||
|     environment: | ||||
|       APP_HOST: ${APP_HOST} | ||||
|       API_HOST: ${API_HOST} | ||||
| @@ -218,20 +221,22 @@ services: | ||||
|       CERT_PRIV_KEY: ${CERT_PRIV_KEY} | ||||
|       APP_PORT: ${APP_PORT} | ||||
|       API_PORT: ${API_PORT} | ||||
|       DEV: 1 | ||||
|     networks: | ||||
|       dev: | ||||
|         ipv4_address: 172.21.0.20 | ||||
|         ipv4_address: ${DOCKER_NGINX_IP} | ||||
|     ports: | ||||
|       - "80:80" | ||||
|       - "443:443" | ||||
|       - "80:8080" | ||||
|       - "443:4443" | ||||
|     volumes: | ||||
|       - tactical-data-dev:/opt/tactical | ||||
|  | ||||
| volumes: | ||||
|   tactical-data-dev: | ||||
|   postgres-data-dev: | ||||
|   mongo-dev-data: | ||||
|   mesh-data-dev: | ||||
|   tactical-data-dev: null | ||||
|   postgres-data-dev: null | ||||
|   mongo-dev-data: null | ||||
|   mesh-data-dev: null | ||||
|   redis-data-dev: null | ||||
|  | ||||
| networks: | ||||
|   dev: | ||||
| @@ -239,4 +244,4 @@ networks: | ||||
|     ipam: | ||||
|       driver: default | ||||
|       config: | ||||
|         - subnet: 172.21.0.0/24   | ||||
|         - subnet: ${DOCKER_NETWORK} | ||||
|   | ||||
| @@ -9,7 +9,8 @@ set -e | ||||
| : "${POSTGRES_USER:=tactical}" | ||||
| : "${POSTGRES_PASS:=tactical}" | ||||
| : "${POSTGRES_DB:=tacticalrmm}" | ||||
| : "${MESH_CONTAINER:=tactical-meshcentral}" | ||||
| : "${MESH_SERVICE:=tactical-meshcentral}" | ||||
| : "${MESH_WS_URL:=ws://${MESH_SERVICE}:4443}" | ||||
| : "${MESH_USER:=meshcentral}" | ||||
| : "${MESH_PASS:=meshcentralpass}" | ||||
| : "${MESH_HOST:=tactical-meshcentral}" | ||||
| @@ -20,6 +21,9 @@ set -e | ||||
| : "${APP_PORT:=8080}" | ||||
| : "${API_PORT:=8000}" | ||||
|  | ||||
| : "${CERT_PRIV_PATH:=${TACTICAL_DIR}/certs/privkey.pem}" | ||||
| : "${CERT_PUB_PATH:=${TACTICAL_DIR}/certs/fullchain.pem}" | ||||
|  | ||||
| # Add python venv to path | ||||
| export PATH="${VIRTUAL_ENV}/bin:$PATH" | ||||
|  | ||||
| @@ -37,7 +41,7 @@ function django_setup { | ||||
|     sleep 5 | ||||
|   done | ||||
|  | ||||
|   until (echo > /dev/tcp/"${MESH_CONTAINER}"/443) &> /dev/null; do | ||||
|   until (echo > /dev/tcp/"${MESH_SERVICE}"/4443) &> /dev/null; do | ||||
|     echo "waiting for meshcentral container to be ready..." | ||||
|     sleep 5 | ||||
|   done | ||||
| @@ -56,10 +60,10 @@ DEBUG = True | ||||
|  | ||||
| DOCKER_BUILD = True | ||||
|  | ||||
| CERT_FILE = '/opt/tactical/certs/fullchain.pem' | ||||
| KEY_FILE = '/opt/tactical/certs/privkey.pem' | ||||
| CERT_FILE = '${CERT_PUB_PATH}' | ||||
| KEY_FILE = '${CERT_PRIV_PATH}' | ||||
|  | ||||
| SCRIPTS_DIR = '${WORKSPACE_DIR}/scripts' | ||||
| SCRIPTS_DIR = '/community-scripts' | ||||
|  | ||||
| ALLOWED_HOSTS = ['${API_HOST}', '*'] | ||||
|  | ||||
| @@ -78,28 +82,11 @@ 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}' | ||||
| REDIS_HOST    = '${REDIS_HOST}' | ||||
| MESH_WS_URL = '${MESH_WS_URL}' | ||||
| ADMIN_ENABLED = True | ||||
| EOF | ||||
| )" | ||||
| @@ -114,6 +101,10 @@ EOF | ||||
|   "${VIRTUAL_ENV}"/bin/python manage.py load_chocos | ||||
|   "${VIRTUAL_ENV}"/bin/python manage.py load_community_scripts | ||||
|   "${VIRTUAL_ENV}"/bin/python manage.py reload_nats | ||||
|   "${VIRTUAL_ENV}"/bin/python manage.py create_natsapi_conf | ||||
|   "${VIRTUAL_ENV}"/bin/python manage.py create_installer_user | ||||
|   "${VIRTUAL_ENV}"/bin/python manage.py post_update_tasks | ||||
|    | ||||
|  | ||||
|   # 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 | ||||
| @@ -126,8 +117,24 @@ if [ "$1" = 'tactical-init-dev' ]; then | ||||
|  | ||||
|   test -f "${TACTICAL_READY_FILE}" && rm "${TACTICAL_READY_FILE}" | ||||
|  | ||||
|   mkdir -p /meshcentral-data | ||||
|   mkdir -p ${TACTICAL_DIR}/tmp | ||||
|   mkdir -p ${TACTICAL_DIR}/certs | ||||
|   mkdir -p /mongo/data/db | ||||
|   mkdir -p /redis/data | ||||
|   touch /meshcentral-data/.initialized && chown -R 1000:1000 /meshcentral-data | ||||
|   touch ${TACTICAL_DIR}/tmp/.initialized && chown -R 1000:1000 ${TACTICAL_DIR} | ||||
|   touch ${TACTICAL_DIR}/certs/.initialized && chown -R 1000:1000 ${TACTICAL_DIR}/certs | ||||
|   touch /mongo/data/db/.initialized && chown -R 1000:1000 /mongo/data/db | ||||
|   touch /redis/data/.initialized && chown -R 1000:1000 /redis/data | ||||
|   mkdir -p ${TACTICAL_DIR}/api/tacticalrmm/private/exe | ||||
|   mkdir -p ${TACTICAL_DIR}/api/tacticalrmm/private/log | ||||
|   touch ${TACTICAL_DIR}/api/tacticalrmm/private/log/django_debug.log | ||||
|  | ||||
|   # setup Python virtual env and install dependencies | ||||
|   ! test -e "${VIRTUAL_ENV}" && python -m venv ${VIRTUAL_ENV} | ||||
|   "${VIRTUAL_ENV}"/bin/python -m pip install --upgrade pip | ||||
|   "${VIRTUAL_ENV}"/bin/pip install --no-cache-dir setuptools wheel | ||||
|   "${VIRTUAL_ENV}"/bin/pip install --no-cache-dir -r /requirements.txt | ||||
|  | ||||
|   django_setup | ||||
|   | ||||
| @@ -1,35 +1,36 @@ | ||||
| # 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 | ||||
| Django | ||||
| django-cors-headers | ||||
| django-rest-knox | ||||
| djangorestframework | ||||
| loguru | ||||
| msgpack | ||||
| psycopg2-binary | ||||
| pycparser | ||||
| pycryptodome | ||||
| pyotp | ||||
| pyparsing | ||||
| pytz | ||||
| qrcode | ||||
| redis | ||||
| twilio | ||||
| packaging | ||||
| validators | ||||
| websockets | ||||
| black | ||||
| Werkzeug | ||||
| django-extensions | ||||
| coverage | ||||
| coveralls | ||||
| model_bakery | ||||
| mkdocs | ||||
| mkdocs-material | ||||
| pymdown-extensions | ||||
| Pygments | ||||
| mypy | ||||
| pysnooper | ||||
| isort | ||||
| asgiref==3.5.0 | ||||
| celery==5.2.3 | ||||
| channels==3.0.4 | ||||
| channels_redis==3.3.1 | ||||
| daphne==3.0.2 | ||||
| Django==3.2.12 | ||||
| django-cors-headers==3.11.0 | ||||
| django-ipware==4.0.2 | ||||
| django-rest-knox==4.2.0 | ||||
| djangorestframework==3.13.1 | ||||
| future==0.18.2 | ||||
| msgpack==1.0.3 | ||||
| nats-py==2.0.0 | ||||
| packaging==21.3 | ||||
| psycopg2-binary==2.9.3 | ||||
| pycryptodome==3.14.1 | ||||
| pyotp==2.6.0 | ||||
| pytz==2021.3 | ||||
| qrcode==7.3.1 | ||||
| redis==4.1.3 | ||||
| requests==2.27.1 | ||||
| twilio==7.6.0 | ||||
| urllib3==1.26.8 | ||||
| validators==0.18.2 | ||||
| websockets==10.1 | ||||
| drf_spectacular==0.21.2 | ||||
|  | ||||
| # dev | ||||
| black==22.1.0 | ||||
| Werkzeug==2.0.2 | ||||
| django-extensions==3.1.5 | ||||
| Pygments==2.11.2 | ||||
| isort==5.10.1 | ||||
| mypy==0.931 | ||||
| types-pytz==2021.3.4 | ||||
|   | ||||
							
								
								
									
										70
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,70 @@ | ||||
| # For most projects, this workflow file will not need changing; you simply need | ||||
| # to commit it to your repository. | ||||
| # | ||||
| # You may wish to alter this file to override the set of languages analyzed, | ||||
| # or to provide custom queries or build logic. | ||||
| # | ||||
| # ******** NOTE ******** | ||||
| # We have attempted to detect the languages in your repository. Please check | ||||
| # the `language` matrix defined below to confirm you have the correct set of | ||||
| # supported CodeQL languages. | ||||
| # | ||||
| name: "CodeQL" | ||||
|  | ||||
| on: | ||||
|   push: | ||||
|     branches: [ develop ] | ||||
|   pull_request: | ||||
|     # The branches below must be a subset of the branches above | ||||
|     branches: [ develop ] | ||||
|   schedule: | ||||
|     - cron: '19 14 * * 6' | ||||
|  | ||||
| jobs: | ||||
|   analyze: | ||||
|     name: Analyze | ||||
|     runs-on: ubuntu-latest | ||||
|     permissions: | ||||
|       actions: read | ||||
|       contents: read | ||||
|       security-events: write | ||||
|  | ||||
|     strategy: | ||||
|       fail-fast: false | ||||
|       matrix: | ||||
|         language: [ 'go', 'javascript', 'python' ] | ||||
|         # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] | ||||
|         # Learn more about CodeQL language support at https://git.io/codeql-language-support | ||||
|  | ||||
|     steps: | ||||
|     - name: Checkout repository | ||||
|       uses: actions/checkout@v2 | ||||
|  | ||||
|     # Initializes the CodeQL tools for scanning. | ||||
|     - name: Initialize CodeQL | ||||
|       uses: github/codeql-action/init@v1 | ||||
|       with: | ||||
|         languages: ${{ matrix.language }} | ||||
|         # If you wish to specify custom queries, you can do so here or in a config file. | ||||
|         # By default, queries listed here will override any specified in a config file. | ||||
|         # Prefix the list here with "+" to use these queries and those in the config file. | ||||
|         # queries: ./path/to/local/query, your-org/your-repo/queries@main | ||||
|  | ||||
|     # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java). | ||||
|     # If this step fails, then you should remove it and run the build manually (see below) | ||||
|     - name: Autobuild | ||||
|       uses: github/codeql-action/autobuild@v1 | ||||
|  | ||||
|     # ℹ️ Command-line programs to run using the OS shell. | ||||
|     # 📚 https://git.io/JvXDl | ||||
|  | ||||
|     # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines | ||||
|     #    and modify them (or add more) to build your code if your project | ||||
|     #    uses a compiled language | ||||
|  | ||||
|     #- run: | | ||||
|     #   make bootstrap | ||||
|     #   make release | ||||
|  | ||||
|     - name: Perform CodeQL Analysis | ||||
|       uses: github/codeql-action/analyze@v1 | ||||
							
								
								
									
										22
									
								
								.github/workflows/deploy-docs.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										22
									
								
								.github/workflows/deploy-docs.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,22 +0,0 @@ | ||||
| name: Deploy Docs | ||||
| on: | ||||
|   push: | ||||
|     branches: | ||||
|       - master | ||||
|  | ||||
| defaults: | ||||
|   run: | ||||
|     working-directory: docs | ||||
|  | ||||
| jobs: | ||||
|   deploy: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - uses: actions/checkout@v2 | ||||
|       - uses: actions/setup-python@v2 | ||||
|         with: | ||||
|           python-version: 3.x | ||||
|       - run: pip install --upgrade pip | ||||
|       - run: pip install --upgrade setuptools wheel | ||||
|       - run: pip install mkdocs mkdocs-material pymdown-extensions | ||||
|       - run: mkdocs gh-deploy --force | ||||
							
								
								
									
										34
									
								
								.github/workflows/devskim-analysis.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								.github/workflows/devskim-analysis.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| # This workflow uses actions that are not certified by GitHub. | ||||
| # They are provided by a third-party and are governed by | ||||
| # separate terms of service, privacy policy, and support | ||||
| # documentation. | ||||
|  | ||||
| name: DevSkim | ||||
|  | ||||
| on: | ||||
|   push: | ||||
|     branches: [ develop ] | ||||
|   pull_request: | ||||
|     branches: [ develop ] | ||||
|   schedule: | ||||
|     - cron: '19 5 * * 0' | ||||
|  | ||||
| jobs: | ||||
|   lint: | ||||
|     name: DevSkim | ||||
|     runs-on: ubuntu-20.04 | ||||
|     permissions: | ||||
|       actions: read | ||||
|       contents: read | ||||
|       security-events: write | ||||
|     steps: | ||||
|       - name: Checkout code | ||||
|         uses: actions/checkout@v2 | ||||
|  | ||||
|       - name: Run DevSkim scanner | ||||
|         uses: microsoft/DevSkim-Action@v1 | ||||
|          | ||||
|       - name: Upload DevSkim scan results to GitHub Security tab | ||||
|         uses: github/codeql-action/upload-sarif@v1 | ||||
|         with: | ||||
|           sarif_file: devskim-results.sarif | ||||
							
								
								
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -47,3 +47,7 @@ docs/.vuepress/dist | ||||
| nats-rmm.conf | ||||
| .mypy_cache | ||||
| docs/site/ | ||||
| reset_db.sh | ||||
| run_go_cmd.py | ||||
| nats-api.conf | ||||
| ignore/ | ||||
|   | ||||
							
								
								
									
										4
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							| @@ -1,5 +1,5 @@ | ||||
| { | ||||
|     "python.pythonPath": "api/tacticalrmm/env/bin/python", | ||||
|     "python.defaultInterpreterPath": "api/tacticalrmm/env/bin/python", | ||||
|     "python.languageServer": "Pylance", | ||||
|     "python.analysis.extraPaths": [ | ||||
|         "api/tacticalrmm", | ||||
| @@ -9,8 +9,6 @@ | ||||
|         "reportUnusedImport": "error", | ||||
|         "reportDuplicateImport": "error", | ||||
|     }, | ||||
|     "python.analysis.memory.keepLibraryAst": true, | ||||
|     "python.linting.mypyEnabled": true, | ||||
|     "python.analysis.typeCheckingMode": "basic", | ||||
|     "python.formatting.provider": "black", | ||||
|     "editor.formatOnSave": true, | ||||
|   | ||||
							
								
								
									
										21
									
								
								LICENSE
									
									
									
									
									
								
							
							
						
						
									
										21
									
								
								LICENSE
									
									
									
									
									
								
							| @@ -1,21 +0,0 @@ | ||||
| MIT License | ||||
|  | ||||
| Copyright (c) 2019-present wh1te909 | ||||
|  | ||||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
| of this software and associated documentation files (the "Software"), to deal | ||||
| in the Software without restriction, including without limitation the rights | ||||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||||
| copies of the Software, and to permit persons to whom the Software is | ||||
| furnished to do so, subject to the following conditions: | ||||
|  | ||||
| The above copyright notice and this permission notice shall be included in all | ||||
| copies or substantial portions of the Software. | ||||
|  | ||||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||
| SOFTWARE. | ||||
							
								
								
									
										74
									
								
								LICENSE.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								LICENSE.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,74 @@ | ||||
| ### Tactical RMM License Version 1.0 | ||||
|  | ||||
| Text of license:   Copyright © 2022 AmidaWare LLC.  All rights reserved.<br> | ||||
|           Amending the text of this license is not permitted. | ||||
|  | ||||
| Trade Mark:    "Tactical RMM" is a trade mark of AmidaWare LLC. | ||||
|  | ||||
| Licensor:       AmidaWare LLC of 1968 S Coast Hwy PMB 3847 Laguna Beach, CA, USA. | ||||
|  | ||||
| Licensed Software:  The software known as Tactical RMM Version v0.12.0 (and all subsequent releases and versions) and the Tactical RMM Agent v2.0.0 (and all subsequent releases and versions). | ||||
|  | ||||
| ### 1. Preamble | ||||
| The Licensed Software is designed to facilitate the remote monitoring and management (RMM) of networks, systems, servers, computers and other devices.  The Licensed Software is made available primarily for use by organisations and managed service providers for monitoring and management purposes. | ||||
|  | ||||
| The Tactical RMM License is not an open-source software license.  This license contains certain restrictions on the use of the Licensed Software.  For example the functionality of the Licensed Software may not be made available as part of a SaaS (Software-as-a-Service) service or product to provide a commercial or for-profit service without the express prior permission of the Licensor. | ||||
|  | ||||
| ### 2. License Grant | ||||
| Permission is hereby granted, free of charge, on a non-exclusive basis, to copy, modify, create derivative works and use the Licensed Software in source and binary forms subject to the following terms and conditions.  No additional rights will be implied under this license. | ||||
|  | ||||
| * The hosting and use of the Licensed Software to monitor and manage in-house networks/systems and/or customer networks/systems is permitted. | ||||
|  | ||||
| This license does not allow the functionality of the Licensed Software (whether in whole or in part) or a modified version of the Licensed Software or a derivative work to be used or otherwise made available as part of any other commercial or for-profit service, including, without limitation, any of the following: | ||||
| * a service allowing third parties to interact remotely through a computer network; | ||||
| * as part of a SaaS service or product; | ||||
| * as part of the provision of a managed hosting service or product; | ||||
| * the offering of installation and/or configuration services; | ||||
| * the offer for sale, distribution or sale of any service or product (whether or not branded as Tactical RMM). | ||||
|  | ||||
| The prior written approval of AmidaWare LLC must be obtained for all commercial use and/or for-profit service use of the (i) Licensed Software (whether in whole or in part), (ii) a modified version of the Licensed Software and/or (iii) a derivative work. | ||||
|  | ||||
| The terms of this license apply to all copies of the Licensed Software (including modified versions) and derivative works. | ||||
|  | ||||
| All use of the Licensed Software must immediately cease if use breaches the terms of this license. | ||||
|  | ||||
| ### 3. Derivative Works | ||||
| If a derivative work is created which is based on or otherwise incorporates all or any part of the Licensed Software, and the derivative work is made available to any other person, the complete corresponding machine readable source code (including all changes made to the Licensed Software) must accompany the derivative work and be made publicly available online. | ||||
|  | ||||
| ### 4. Copyright Notice | ||||
| The following copyright notice shall be included in all copies of the Licensed Software: | ||||
|  | ||||
|    Copyright © 2022 AmidaWare LLC. | ||||
|  | ||||
|    Licensed under the Tactical RMM License Version 1.0 (the “License”).<br> | ||||
|    You may only use the Licensed Software in accordance with the License.<br> | ||||
|    A copy of the License is available at: https://license.tacticalrmm.com | ||||
|  | ||||
| ### 5. Disclaimer of Warranty | ||||
| THE LICENSED SOFTWARE IS PROVIDED "AS IS".  TO THE FULLEST EXTENT PERMISSIBLE AT LAW ALL CONDITIONS, WARRANTIES OR OTHER TERMS OF ANY KIND WHICH MIGHT HAVE EFFECT OR BE IMPLIED OR INCORPORATED, WHETHER BY STATUTE, COMMON LAW OR OTHERWISE ARE HEREBY EXCLUDED, INCLUDING THE CONDITIONS, WARRANTIES OR OTHER TERMS AS TO SATISFACTORY QUALITY AND/OR MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, THE USE OF REASONABLE SKILL AND CARE AND NON-INFRINGEMENT. | ||||
|  | ||||
| ### 6. Limits of Liability | ||||
| THE FOLLOWING EXCLUSIONS SHALL APPLY TO THE FULLEST EXTENT PERMISSIBLE AT LAW.  NEITHER THE AUTHORS NOR THE COPYRIGHT HOLDERS SHALL IN ANY CIRCUMSTANCES HAVE ANY LIABILITY FOR ANY CLAIM, LOSSES, DAMAGES OR OTHER LIABILITY, WHETHER THE SAME ARE SUFFERED DIRECTLY OR INDIRECTLY OR ARE IMMEDIATE OR CONSEQUENTIAL, AND WHETHER THE SAME ARISE IN CONTRACT, TORT OR DELICT (INCLUDING NEGLIGENCE) OR OTHERWISE HOWSOEVER ARISING FROM, OUT OF OR IN CONNECTION WITH THE LICENSED SOFTWARE OR THE USE OR INABILITY TO USE THE LICENSED SOFTWARE OR OTHER DEALINGS IN THE LICENSED SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH LOSS OR DAMAGE.  THE FOREGOING EXCLUSIONS SHALL INCLUDE, WITHOUT LIMITATION, LIABILITY FOR ANY LOSSES OR DAMAGES WHICH FALL WITHIN ANY OF THE FOLLOWING CATEGORIES: SPECIAL, EXEMPLARY, OR INCIDENTAL LOSS OR DAMAGE, LOSS OF PROFITS, LOSS OF ANTICIPATED SAVINGS, LOSS OF BUSINESS OPPORTUNITY, LOSS OF GOODWILL, AND LOSS OR CORRUPTION OF DATA. | ||||
|  | ||||
| ### 7. Termination | ||||
| This license shall terminate with immediate effect if there is a material breach of any of its terms. | ||||
|  | ||||
| ### 8. No partnership, agency or joint venture | ||||
| Nothing in this license agreement is intended to, or shall be deemed to, establish any partnership or joint venture or any relationship of agency between AmidaWare LLC and any other person. | ||||
|  | ||||
| ### 9. No endorsement | ||||
| The names of the authors and/or the copyright holders must not be used to promote or endorse any products or services which are in any way derived from the Licensed Software without prior written consent. | ||||
|  | ||||
| ### 10. Trademarks | ||||
| No permission is granted to use the trademark “Tactical RMM” or any other trade name, trademark, service mark or product name of AmidaWare LLC except to the extent necessary to comply with the notice requirements in Section 4 (Copyright Notice). | ||||
|  | ||||
| ### 11. Entire agreement | ||||
| This license contains the whole agreement relating to its subject matter. | ||||
|  | ||||
|  | ||||
|  | ||||
| ### 12. Severance | ||||
| If any provision or part-provision of this license is or becomes invalid, illegal or unenforceable, it shall be deemed deleted, but that shall not affect the validity and enforceability of the rest of this license. | ||||
|  | ||||
| ### 13. Acceptance of these terms | ||||
| The terms and conditions of this license are accepted by copying, downloading, installing, redistributing, or otherwise using the Licensed Software. | ||||
							
								
								
									
										13
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								README.md
									
									
									
									
									
								
							| @@ -2,20 +2,17 @@ | ||||
|  | ||||
| [](https://dev.azure.com/dcparsi/Tactical%20RMM/_build/latest?definitionId=4&branchName=develop) | ||||
| [](https://coveralls.io/github/wh1te909/tacticalrmm?branch=develop) | ||||
| [](https://opensource.org/licenses/MIT) | ||||
| [](https://github.com/python/black) | ||||
|  | ||||
| Tactical RMM is a remote monitoring & management tool for Windows computers, built with Django and Vue.\ | ||||
| It uses an [agent](https://github.com/wh1te909/rmmagent) written in golang and integrates with [MeshCentral](https://github.com/Ylianst/MeshCentral) | ||||
| Tactical RMM is a remote monitoring & management tool, built with Django and Vue.\ | ||||
| It uses an [agent](https://github.com/amidaware/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) | ||||
|  | ||||
| ### [Documentation](https://wh1te909.github.io/tacticalrmm/) | ||||
| ### [Documentation](https://docs.tacticalrmm.com) | ||||
|  | ||||
| ## Features | ||||
|  | ||||
| @@ -37,4 +34,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://docs.tacticalrmm.com) | ||||
|   | ||||
							
								
								
									
										12
									
								
								SECURITY.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								SECURITY.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| # Security Policy | ||||
|  | ||||
| ## Supported Versions | ||||
|  | ||||
| | Version | Supported          | | ||||
| | ------- | ------------------ | | ||||
| | 0.12.0   | :white_check_mark: | | ||||
| | < 0.12.0 | :x:                | | ||||
|  | ||||
| ## Reporting a Vulnerability | ||||
|  | ||||
| https://docs.tacticalrmm.com/security | ||||
| @@ -21,4 +21,6 @@ omit = | ||||
|     */tests.py | ||||
|     */test.py | ||||
|     checks/utils.py | ||||
|     */asgi.py | ||||
|     */demo_views.py | ||||
|      | ||||
|   | ||||
| @@ -1,7 +1,8 @@ | ||||
| from django.contrib import admin | ||||
| from rest_framework.authtoken.admin import TokenAdmin | ||||
|  | ||||
| from .models import User | ||||
| from .models import Role, User | ||||
|  | ||||
| admin.site.register(User) | ||||
| TokenAdmin.raw_id_fields = ("user",) | ||||
| admin.site.register(Role) | ||||
|   | ||||
| @@ -0,0 +1,22 @@ | ||||
| import uuid | ||||
|  | ||||
| from accounts.models import User | ||||
| from django.core.management.base import BaseCommand | ||||
|  | ||||
|  | ||||
| class Command(BaseCommand): | ||||
|     help = "Creates the installer user" | ||||
|  | ||||
|     def handle(self, *args, **kwargs): | ||||
|         self.stdout.write("Checking if installer user has been created...") | ||||
|         if User.objects.filter(is_installer_user=True).exists(): | ||||
|             self.stdout.write("Installer user already 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, | ||||
|         ) | ||||
|         self.stdout.write("Installer user has been created") | ||||
| @@ -1,9 +1,8 @@ | ||||
| import subprocess | ||||
|  | ||||
| import pyotp | ||||
| from django.core.management.base import BaseCommand | ||||
|  | ||||
| from accounts.models import User | ||||
| from django.core.management.base import BaseCommand | ||||
|  | ||||
|  | ||||
| class Command(BaseCommand): | ||||
|   | ||||
| @@ -2,9 +2,8 @@ import os | ||||
| import subprocess | ||||
|  | ||||
| import pyotp | ||||
| from django.core.management.base import BaseCommand | ||||
|  | ||||
| from accounts.models import User | ||||
| from django.core.management.base import BaseCommand | ||||
|  | ||||
|  | ||||
| class Command(BaseCommand): | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| from django.core.management.base import BaseCommand | ||||
| from accounts.models import User | ||||
| from django.core.management.base import BaseCommand | ||||
|  | ||||
|  | ||||
| class Command(BaseCommand): | ||||
|   | ||||
| @@ -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 | ||||
|  | ||||
| import django.db.models.deletion | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| 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 | ||||
|  | ||||
| import django.db.models.deletion | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('accounts', '0026_auto_20210901_1247'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='apikey', | ||||
|             name='user', | ||||
|             field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='api_key', to='accounts.user'), | ||||
|             preserve_default=False, | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='user', | ||||
|             name='block_dashboard_login', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										150
									
								
								api/tacticalrmm/accounts/migrations/0028_auto_20211010_0249.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										150
									
								
								api/tacticalrmm/accounts/migrations/0028_auto_20211010_0249.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,150 @@ | ||||
| # Generated by Django 3.2.6 on 2021-10-10 02:49 | ||||
|  | ||||
| import django.db.models.deletion | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('clients', '0018_auto_20211010_0249'), | ||||
|         ('accounts', '0027_auto_20210903_0054'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='role', | ||||
|             name='can_list_accounts', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='role', | ||||
|             name='can_list_agent_history', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='role', | ||||
|             name='can_list_agents', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='role', | ||||
|             name='can_list_alerts', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='role', | ||||
|             name='can_list_api_keys', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='role', | ||||
|             name='can_list_automation_policies', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='role', | ||||
|             name='can_list_autotasks', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='role', | ||||
|             name='can_list_checks', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='role', | ||||
|             name='can_list_clients', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='role', | ||||
|             name='can_list_deployments', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='role', | ||||
|             name='can_list_notes', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='role', | ||||
|             name='can_list_pendingactions', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='role', | ||||
|             name='can_list_roles', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='role', | ||||
|             name='can_list_scripts', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='role', | ||||
|             name='can_list_sites', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='role', | ||||
|             name='can_list_software', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='role', | ||||
|             name='can_ping_agents', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='role', | ||||
|             name='can_recover_agents', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='role', | ||||
|             name='can_view_clients', | ||||
|             field=models.ManyToManyField(blank=True, related_name='role_clients', to='clients.Client'), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='role', | ||||
|             name='can_view_sites', | ||||
|             field=models.ManyToManyField(blank=True, related_name='role_sites', to='clients.Site'), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name='apikey', | ||||
|             name='created_by', | ||||
|             field=models.CharField(blank=True, max_length=255, null=True), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name='apikey', | ||||
|             name='modified_by', | ||||
|             field=models.CharField(blank=True, max_length=255, null=True), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name='role', | ||||
|             name='created_by', | ||||
|             field=models.CharField(blank=True, max_length=255, null=True), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name='role', | ||||
|             name='modified_by', | ||||
|             field=models.CharField(blank=True, max_length=255, null=True), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name='user', | ||||
|             name='created_by', | ||||
|             field=models.CharField(blank=True, max_length=255, null=True), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name='user', | ||||
|             name='modified_by', | ||||
|             field=models.CharField(blank=True, max_length=255, null=True), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name='user', | ||||
|             name='role', | ||||
|             field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='users', to='accounts.role'), | ||||
|         ), | ||||
|     ] | ||||
| @@ -0,0 +1,28 @@ | ||||
| # Generated by Django 3.2.6 on 2021-10-22 22:45 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('accounts', '0028_auto_20211010_0249'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='role', | ||||
|             name='can_list_alerttemplates', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='role', | ||||
|             name='can_manage_alerttemplates', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='role', | ||||
|             name='can_run_urlactions', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|     ] | ||||
| @@ -0,0 +1,23 @@ | ||||
| # Generated by Django 3.2.6 on 2021-11-04 02:21 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('accounts', '0029_auto_20211022_2245'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='role', | ||||
|             name='can_manage_customfields', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='role', | ||||
|             name='can_view_customfields', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|     ] | ||||
| @@ -1,12 +1,13 @@ | ||||
| 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 | ||||
|  | ||||
| AGENT_DBLCLICK_CHOICES = [ | ||||
|     ("editagent", "Edit Agent"), | ||||
|     ("takecontrol", "Take Control"), | ||||
|     ("remotebg", "Remote Background"), | ||||
|     ("urlaction", "URL Action"), | ||||
| ] | ||||
|  | ||||
| AGENT_TBL_TAB_CHOICES = [ | ||||
| @@ -23,12 +24,20 @@ CLIENT_TREE_SORT_CHOICES = [ | ||||
|  | ||||
| class User(AbstractUser, BaseAuditModel): | ||||
|     is_active = models.BooleanField(default=True) | ||||
|     block_dashboard_login = models.BooleanField(default=False) | ||||
|     totp_key = models.CharField(max_length=50, null=True, blank=True) | ||||
|     dark_mode = models.BooleanField(default=True) | ||||
|     show_community_scripts = models.BooleanField(default=True) | ||||
|     agent_dblclick_action = models.CharField( | ||||
|         max_length=50, choices=AGENT_DBLCLICK_CHOICES, default="editagent" | ||||
|     ) | ||||
|     url_action = models.ForeignKey( | ||||
|         "core.URLAction", | ||||
|         related_name="user", | ||||
|         null=True, | ||||
|         blank=True, | ||||
|         on_delete=models.SET_NULL, | ||||
|     ) | ||||
|     default_agent_tbl_tab = models.CharField( | ||||
|         max_length=50, choices=AGENT_TBL_TAB_CHOICES, default="server" | ||||
|     ) | ||||
| @@ -38,6 +47,9 @@ class User(AbstractUser, BaseAuditModel): | ||||
|     ) | ||||
|     client_tree_splitter = models.PositiveIntegerField(default=11) | ||||
|     loading_bar_color = models.CharField(max_length=255, default="red") | ||||
|     clear_search_when_switching = models.BooleanField(default=True) | ||||
|     is_installer_user = models.BooleanField(default=False) | ||||
|     last_login_ip = models.GenericIPAddressField(default=None, blank=True, null=True) | ||||
|  | ||||
|     agent = models.OneToOneField( | ||||
|         "agents.Agent", | ||||
| @@ -47,9 +59,141 @@ class User(AbstractUser, BaseAuditModel): | ||||
|         on_delete=models.CASCADE, | ||||
|     ) | ||||
|  | ||||
|     role = models.ForeignKey( | ||||
|         "accounts.Role", | ||||
|         null=True, | ||||
|         blank=True, | ||||
|         related_name="users", | ||||
|         on_delete=models.SET_NULL, | ||||
|     ) | ||||
|  | ||||
|     @staticmethod | ||||
|     def serialize(user): | ||||
|         # serializes the task and returns json | ||||
|         from .serializers import UserSerializer | ||||
|  | ||||
|         return UserSerializer(user).data | ||||
|  | ||||
|  | ||||
| class Role(BaseAuditModel): | ||||
|     name = models.CharField(max_length=255, unique=True) | ||||
|     is_superuser = models.BooleanField(default=False) | ||||
|  | ||||
|     # agents | ||||
|     can_list_agents = models.BooleanField(default=False) | ||||
|     can_ping_agents = models.BooleanField(default=False) | ||||
|     can_use_mesh = models.BooleanField(default=False) | ||||
|     can_uninstall_agents = models.BooleanField(default=False) | ||||
|     can_update_agents = models.BooleanField(default=False) | ||||
|     can_edit_agent = models.BooleanField(default=False) | ||||
|     can_manage_procs = models.BooleanField(default=False) | ||||
|     can_view_eventlogs = models.BooleanField(default=False) | ||||
|     can_send_cmd = models.BooleanField(default=False) | ||||
|     can_reboot_agents = models.BooleanField(default=False) | ||||
|     can_install_agents = models.BooleanField(default=False) | ||||
|     can_run_scripts = models.BooleanField(default=False) | ||||
|     can_run_bulk = models.BooleanField(default=False) | ||||
|     can_recover_agents = models.BooleanField(default=False) | ||||
|     can_list_agent_history = models.BooleanField(default=False) | ||||
|  | ||||
|     # core | ||||
|     can_list_notes = models.BooleanField(default=False) | ||||
|     can_manage_notes = models.BooleanField(default=False) | ||||
|     can_view_core_settings = models.BooleanField(default=False) | ||||
|     can_edit_core_settings = models.BooleanField(default=False) | ||||
|     can_do_server_maint = models.BooleanField(default=False) | ||||
|     can_code_sign = models.BooleanField(default=False) | ||||
|     can_run_urlactions = models.BooleanField(default=False) | ||||
|     can_view_customfields = models.BooleanField(default=False) | ||||
|     can_manage_customfields = models.BooleanField(default=False) | ||||
|  | ||||
|     # checks | ||||
|     can_list_checks = models.BooleanField(default=False) | ||||
|     can_manage_checks = models.BooleanField(default=False) | ||||
|     can_run_checks = models.BooleanField(default=False) | ||||
|  | ||||
|     # clients | ||||
|     can_list_clients = models.BooleanField(default=False) | ||||
|     can_manage_clients = models.BooleanField(default=False) | ||||
|     can_list_sites = models.BooleanField(default=False) | ||||
|     can_manage_sites = models.BooleanField(default=False) | ||||
|     can_list_deployments = models.BooleanField(default=False) | ||||
|     can_manage_deployments = models.BooleanField(default=False) | ||||
|     can_view_clients = models.ManyToManyField( | ||||
|         "clients.Client", related_name="role_clients", blank=True | ||||
|     ) | ||||
|     can_view_sites = models.ManyToManyField( | ||||
|         "clients.Site", related_name="role_sites", blank=True | ||||
|     ) | ||||
|  | ||||
|     # automation | ||||
|     can_list_automation_policies = models.BooleanField(default=False) | ||||
|     can_manage_automation_policies = models.BooleanField(default=False) | ||||
|  | ||||
|     # automated tasks | ||||
|     can_list_autotasks = models.BooleanField(default=False) | ||||
|     can_manage_autotasks = models.BooleanField(default=False) | ||||
|     can_run_autotasks = models.BooleanField(default=False) | ||||
|  | ||||
|     # logs | ||||
|     can_view_auditlogs = models.BooleanField(default=False) | ||||
|     can_list_pendingactions = models.BooleanField(default=False) | ||||
|     can_manage_pendingactions = models.BooleanField(default=False) | ||||
|     can_view_debuglogs = models.BooleanField(default=False) | ||||
|  | ||||
|     # scripts | ||||
|     can_list_scripts = models.BooleanField(default=False) | ||||
|     can_manage_scripts = models.BooleanField(default=False) | ||||
|  | ||||
|     # alerts | ||||
|     can_list_alerts = models.BooleanField(default=False) | ||||
|     can_manage_alerts = models.BooleanField(default=False) | ||||
|     can_list_alerttemplates = models.BooleanField(default=False) | ||||
|     can_manage_alerttemplates = models.BooleanField(default=False) | ||||
|  | ||||
|     # win services | ||||
|     can_manage_winsvcs = models.BooleanField(default=False) | ||||
|  | ||||
|     # software | ||||
|     can_list_software = models.BooleanField(default=False) | ||||
|     can_manage_software = models.BooleanField(default=False) | ||||
|  | ||||
|     # windows updates | ||||
|     can_manage_winupdates = models.BooleanField(default=False) | ||||
|  | ||||
|     # accounts | ||||
|     can_list_accounts = models.BooleanField(default=False) | ||||
|     can_manage_accounts = models.BooleanField(default=False) | ||||
|     can_list_roles = models.BooleanField(default=False) | ||||
|     can_manage_roles = models.BooleanField(default=False) | ||||
|  | ||||
|     # authentication | ||||
|     can_list_api_keys = models.BooleanField(default=False) | ||||
|     can_manage_api_keys = models.BooleanField(default=False) | ||||
|  | ||||
|     def __str__(self): | ||||
|         return self.name | ||||
|  | ||||
|     @staticmethod | ||||
|     def serialize(role): | ||||
|         # serializes the agent and returns json | ||||
|         from .serializers import RoleAuditSerializer | ||||
|  | ||||
|         return RoleAuditSerializer(role).data | ||||
|  | ||||
|  | ||||
| class APIKey(BaseAuditModel): | ||||
|     name = CharField(unique=True, max_length=25) | ||||
|     key = CharField(unique=True, blank=True, max_length=48) | ||||
|     expiration = DateTimeField(blank=True, null=True, default=None) | ||||
|     user = models.ForeignKey( | ||||
|         "accounts.User", | ||||
|         related_name="api_key", | ||||
|         on_delete=models.CASCADE, | ||||
|     ) | ||||
|  | ||||
|     @staticmethod | ||||
|     def serialize(apikey): | ||||
|         from .serializers import APIKeyAuditSerializer | ||||
|  | ||||
|         return APIKeyAuditSerializer(apikey).data | ||||
|   | ||||
							
								
								
									
										43
									
								
								api/tacticalrmm/accounts/permissions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								api/tacticalrmm/accounts/permissions.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | ||||
| from rest_framework import permissions | ||||
|  | ||||
| from tacticalrmm.permissions import _has_perm | ||||
|  | ||||
|  | ||||
| class AccountsPerms(permissions.BasePermission): | ||||
|     def has_permission(self, r, view): | ||||
|         if r.method == "GET": | ||||
|             return _has_perm(r, "can_list_accounts") | ||||
|         else: | ||||
|  | ||||
|             # allow users to reset their own password/2fa see issue #686 | ||||
|             base_path = "/accounts/users/" | ||||
|             paths = ["reset/", "reset_totp/"] | ||||
|  | ||||
|             if r.path in [base_path + i for i in paths]: | ||||
|                 from accounts.models import User | ||||
|  | ||||
|                 try: | ||||
|                     user = User.objects.get(pk=r.data["id"]) | ||||
|                 except User.DoesNotExist: | ||||
|                     pass | ||||
|                 else: | ||||
|                     if user == r.user: | ||||
|                         return True | ||||
|  | ||||
|             return _has_perm(r, "can_manage_accounts") | ||||
|  | ||||
|  | ||||
| class RolesPerms(permissions.BasePermission): | ||||
|     def has_permission(self, r, view): | ||||
|         if r.method == "GET": | ||||
|             return _has_perm(r, "can_list_roles") | ||||
|         else: | ||||
|             return _has_perm(r, "can_manage_roles") | ||||
|  | ||||
|  | ||||
| class APIKeyPerms(permissions.BasePermission): | ||||
|     def has_permission(self, r, view): | ||||
|         if r.method == "GET": | ||||
|             return _has_perm(r, "can_list_api_keys") | ||||
|  | ||||
|         return _has_perm(r, "can_manage_api_keys") | ||||
| @@ -1,7 +1,11 @@ | ||||
| import pyotp | ||||
| from rest_framework.serializers import ModelSerializer, SerializerMethodField | ||||
| from rest_framework.serializers import ( | ||||
|     ModelSerializer, | ||||
|     ReadOnlyField, | ||||
|     SerializerMethodField, | ||||
| ) | ||||
|  | ||||
| from .models import User | ||||
| from .models import APIKey, Role, User | ||||
|  | ||||
|  | ||||
| class UserUISerializer(ModelSerializer): | ||||
| @@ -11,17 +15,20 @@ class UserUISerializer(ModelSerializer): | ||||
|             "dark_mode", | ||||
|             "show_community_scripts", | ||||
|             "agent_dblclick_action", | ||||
|             "url_action", | ||||
|             "default_agent_tbl_tab", | ||||
|             "client_tree_sort", | ||||
|             "client_tree_splitter", | ||||
|             "loading_bar_color", | ||||
|             "clear_search_when_switching", | ||||
|             "block_dashboard_login", | ||||
|         ] | ||||
|  | ||||
|  | ||||
| class UserSerializer(ModelSerializer): | ||||
|     class Meta: | ||||
|         model = User | ||||
|         fields = ( | ||||
|         fields = [ | ||||
|             "id", | ||||
|             "username", | ||||
|             "first_name", | ||||
| @@ -29,7 +36,10 @@ class UserSerializer(ModelSerializer): | ||||
|             "email", | ||||
|             "is_active", | ||||
|             "last_login", | ||||
|         ) | ||||
|             "last_login_ip", | ||||
|             "role", | ||||
|             "block_dashboard_login", | ||||
|         ] | ||||
|  | ||||
|  | ||||
| class TOTPSetupSerializer(ModelSerializer): | ||||
| @@ -48,3 +58,41 @@ class TOTPSetupSerializer(ModelSerializer): | ||||
|         return pyotp.totp.TOTP(obj.totp_key).provisioning_uri( | ||||
|             obj.username, issuer_name="Tactical RMM" | ||||
|         ) | ||||
|  | ||||
|  | ||||
| class RoleSerializer(ModelSerializer): | ||||
|     user_count = SerializerMethodField() | ||||
|  | ||||
|     class Meta: | ||||
|         model = Role | ||||
|         fields = "__all__" | ||||
|  | ||||
|     def get_user_count(self, obj): | ||||
|         return obj.users.count() | ||||
|  | ||||
|  | ||||
| class RoleAuditSerializer(ModelSerializer): | ||||
|     class Meta: | ||||
|         model = Role | ||||
|         fields = "__all__" | ||||
|  | ||||
|  | ||||
| class APIKeySerializer(ModelSerializer): | ||||
|  | ||||
|     username = ReadOnlyField(source="user.username") | ||||
|  | ||||
|     class Meta: | ||||
|         model = APIKey | ||||
|         fields = "__all__" | ||||
|  | ||||
|  | ||||
| class APIKeyAuditSerializer(ModelSerializer): | ||||
|     username = ReadOnlyField(source="user.username") | ||||
|  | ||||
|     class Meta: | ||||
|         model = APIKey | ||||
|         fields = [ | ||||
|             "name", | ||||
|             "username", | ||||
|             "expiration", | ||||
|         ] | ||||
|   | ||||
| @@ -1,8 +1,10 @@ | ||||
| from unittest.mock import patch | ||||
|  | ||||
| from accounts.models import APIKey, User | ||||
| from accounts.serializers import APIKeySerializer | ||||
| from django.test import override_settings | ||||
| from model_bakery import baker, seq | ||||
|  | ||||
| from accounts.models import User | ||||
| from tacticalrmm.test import TacticalTestCase | ||||
|  | ||||
|  | ||||
| @@ -25,12 +27,12 @@ class TestAccounts(TacticalTestCase): | ||||
|         data = {"username": "bob", "password": "a3asdsa2314"} | ||||
|         r = self.client.post(url, data, format="json") | ||||
|         self.assertEqual(r.status_code, 400) | ||||
|         self.assertEqual(r.data, "bad credentials") | ||||
|         self.assertEqual(r.data, "Bad credentials") | ||||
|  | ||||
|         data = {"username": "billy", "password": "hunter2"} | ||||
|         r = self.client.post(url, data, format="json") | ||||
|         self.assertEqual(r.status_code, 400) | ||||
|         self.assertEqual(r.data, "bad credentials") | ||||
|         self.assertEqual(r.data, "Bad credentials") | ||||
|  | ||||
|         self.bob.totp_key = "AB5RI6YPFTZAS52G" | ||||
|         self.bob.save() | ||||
| @@ -39,6 +41,12 @@ class TestAccounts(TacticalTestCase): | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|         self.assertEqual(r.data, "ok") | ||||
|  | ||||
|         # test user set to block dashboard logins | ||||
|         self.bob.block_dashboard_login = True | ||||
|         self.bob.save() | ||||
|         r = self.client.post(url, data, format="json") | ||||
|         self.assertEqual(r.status_code, 400) | ||||
|  | ||||
|     @patch("pyotp.TOTP.verify") | ||||
|     def test_login_view(self, mock_verify): | ||||
|         url = "/login/" | ||||
| @@ -53,7 +61,7 @@ class TestAccounts(TacticalTestCase): | ||||
|         mock_verify.return_value = False | ||||
|         r = self.client.post(url, data, format="json") | ||||
|         self.assertEqual(r.status_code, 400) | ||||
|         self.assertEqual(r.data, "bad credentials") | ||||
|         self.assertEqual(r.data, "Bad credentials") | ||||
|  | ||||
|         mock_verify.return_value = True | ||||
|         data = {"username": "bob", "password": "asd234234asd", "twofactor": "123456"} | ||||
| @@ -280,6 +288,7 @@ class TestUserAction(TacticalTestCase): | ||||
|             "client_tree_sort": "alpha", | ||||
|             "client_tree_splitter": 14, | ||||
|             "loading_bar_color": "green", | ||||
|             "clear_search_when_switching": False, | ||||
|         } | ||||
|         r = self.client.patch(url, data, format="json") | ||||
|         self.assertEqual(r.status_code, 200) | ||||
| @@ -287,6 +296,68 @@ class TestUserAction(TacticalTestCase): | ||||
|         self.check_not_authenticated("patch", url) | ||||
|  | ||||
|  | ||||
| class TestAPIKeyViews(TacticalTestCase): | ||||
|     def setUp(self): | ||||
|         self.setup_coresettings() | ||||
|         self.authenticate() | ||||
|  | ||||
|     def test_get_api_keys(self): | ||||
|         url = "/accounts/apikeys/" | ||||
|         apikeys = baker.make("accounts.APIKey", key=seq("APIKEY"), _quantity=3) | ||||
|  | ||||
|         serializer = APIKeySerializer(apikeys, many=True) | ||||
|         resp = self.client.get(url, format="json") | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|         self.assertEqual(serializer.data, resp.data)  # type: ignore | ||||
|  | ||||
|         self.check_not_authenticated("get", url) | ||||
|  | ||||
|     def test_add_api_keys(self): | ||||
|         url = "/accounts/apikeys/" | ||||
|  | ||||
|         user = baker.make("accounts.User") | ||||
|         data = {"name": "Name", "user": user.id, "expiration": None} | ||||
|  | ||||
|         resp = self.client.post(url, data, format="json") | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|         self.assertTrue(APIKey.objects.filter(name="Name").exists()) | ||||
|         self.assertTrue(APIKey.objects.get(name="Name").key) | ||||
|  | ||||
|         self.check_not_authenticated("post", url) | ||||
|  | ||||
|     def test_modify_api_key(self): | ||||
|         # test a call where api key doesn't exist | ||||
|         resp = self.client.put("/accounts/apikeys/500/", format="json") | ||||
|         self.assertEqual(resp.status_code, 404) | ||||
|  | ||||
|         apikey = baker.make("accounts.APIKey", name="Test") | ||||
|         url = f"/accounts/apikeys/{apikey.pk}/"  # type: ignore | ||||
|  | ||||
|         data = {"name": "New Name"}  # type: ignore | ||||
|  | ||||
|         resp = self.client.put(url, data, format="json") | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|         apikey = APIKey.objects.get(pk=apikey.pk)  # type: ignore | ||||
|         self.assertEquals(apikey.name, "New Name") | ||||
|  | ||||
|         self.check_not_authenticated("put", url) | ||||
|  | ||||
|     def test_delete_api_key(self): | ||||
|         # test a call where api key doesn't exist | ||||
|         resp = self.client.delete("/accounts/apikeys/500/", format="json") | ||||
|         self.assertEqual(resp.status_code, 404) | ||||
|  | ||||
|         # test delete api key | ||||
|         apikey = baker.make("accounts.APIKey") | ||||
|         url = f"/accounts/apikeys/{apikey.pk}/"  # type: ignore | ||||
|         resp = self.client.delete(url, format="json") | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|  | ||||
|         self.assertFalse(APIKey.objects.filter(pk=apikey.pk).exists())  # type: ignore | ||||
|  | ||||
|         self.check_not_authenticated("delete", url) | ||||
|  | ||||
|  | ||||
| class TestTOTPSetup(TacticalTestCase): | ||||
|     def setUp(self): | ||||
|         self.authenticate() | ||||
| @@ -312,3 +383,29 @@ class TestTOTPSetup(TacticalTestCase): | ||||
|         r = self.client.post(url) | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|         self.assertEqual(r.data, "totp token already set") | ||||
|  | ||||
|  | ||||
| class TestAPIAuthentication(TacticalTestCase): | ||||
|     def setUp(self): | ||||
|         # create User and associate to API Key | ||||
|         self.user = User.objects.create(username="api_user", is_superuser=True) | ||||
|         self.api_key = APIKey.objects.create( | ||||
|             name="Test Token", key="123456", user=self.user | ||||
|         ) | ||||
|  | ||||
|         self.client_setup() | ||||
|  | ||||
|     def test_api_auth(self): | ||||
|         url = "/clients/" | ||||
|         # auth should fail if no header set | ||||
|         self.check_not_authenticated("get", url) | ||||
|  | ||||
|         # invalid api key in header should return code 400 | ||||
|         self.client.credentials(HTTP_X_API_KEY="000000") | ||||
|         r = self.client.get(url, format="json") | ||||
|         self.assertEqual(r.status_code, 401) | ||||
|  | ||||
|         # valid api key in header should return code 200 | ||||
|         self.client.credentials(HTTP_X_API_KEY="123456") | ||||
|         r = self.client.get(url, format="json") | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|   | ||||
| @@ -9,4 +9,8 @@ urlpatterns = [ | ||||
|     path("users/reset_totp/", views.UserActions.as_view()), | ||||
|     path("users/setup_totp/", views.TOTPSetup.as_view()), | ||||
|     path("users/ui/", views.UserUI.as_view()), | ||||
|     path("roles/", views.GetAddRoles.as_view()), | ||||
|     path("roles/<int:pk>/", views.GetUpdateDeleteRole.as_view()), | ||||
|     path("apikeys/", views.GetAddAPIKeys.as_view()), | ||||
|     path("apikeys/<int:pk>/", views.GetUpdateDeleteAPIKey.as_view()), | ||||
| ] | ||||
|   | ||||
| @@ -3,26 +3,37 @@ 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 rest_framework import status | ||||
| from logs.models import AuditLog | ||||
| 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 AccountsPerms, APIKeyPerms, RolesPerms | ||||
| from .serializers import ( | ||||
|     APIKeySerializer, | ||||
|     RoleSerializer, | ||||
|     TOTPSetupSerializer, | ||||
|     UserSerializer, | ||||
|     UserUISerializer, | ||||
| ) | ||||
|  | ||||
|  | ||||
| def _is_root_user(request, user) -> bool: | ||||
|     return ( | ||||
|     root = ( | ||||
|         hasattr(settings, "ROOT_USER") | ||||
|         and request.user != user | ||||
|         and user.username == settings.ROOT_USER | ||||
|     ) | ||||
|     demo = ( | ||||
|         getattr(settings, "DEMO", False) and request.user.username == settings.ROOT_USER | ||||
|     ) | ||||
|     return root or demo | ||||
|  | ||||
|  | ||||
| class CheckCreds(KnoxLoginView): | ||||
| @@ -34,11 +45,16 @@ class CheckCreds(KnoxLoginView): | ||||
|         # check credentials | ||||
|         serializer = AuthTokenSerializer(data=request.data) | ||||
|         if not serializer.is_valid(): | ||||
|             AuditLog.audit_user_failed_login(request.data["username"]) | ||||
|             return Response("bad credentials", status=status.HTTP_400_BAD_REQUEST) | ||||
|             AuditLog.audit_user_failed_login( | ||||
|                 request.data["username"], debug_info={"ip": request._client_ip} | ||||
|             ) | ||||
|             return notify_error("Bad credentials") | ||||
|  | ||||
|         user = serializer.validated_data["user"] | ||||
|  | ||||
|         if user.block_dashboard_login: | ||||
|             return notify_error("Bad credentials") | ||||
|  | ||||
|         # if totp token not set modify response to notify frontend | ||||
|         if not user.totp_key: | ||||
|             login(request, user) | ||||
| @@ -60,26 +76,50 @@ class LoginView(KnoxLoginView): | ||||
|         serializer.is_valid(raise_exception=True) | ||||
|         user = serializer.validated_data["user"] | ||||
|  | ||||
|         if user.block_dashboard_login: | ||||
|             return notify_error("Bad credentials") | ||||
|  | ||||
|         token = request.data["twofactor"] | ||||
|         totp = pyotp.TOTP(user.totp_key) | ||||
|  | ||||
|         if settings.DEBUG and token == "sekret": | ||||
|             valid = True | ||||
|         elif getattr(settings, "DEMO", False): | ||||
|             valid = True | ||||
|         elif totp.verify(token, valid_window=10): | ||||
|             valid = True | ||||
|  | ||||
|         if valid: | ||||
|             login(request, user) | ||||
|             AuditLog.audit_user_login_successful(request.data["username"]) | ||||
|  | ||||
|             # save ip information | ||||
|             client_ip, is_routable = get_client_ip(request) | ||||
|             user.last_login_ip = client_ip | ||||
|             user.save() | ||||
|  | ||||
|             AuditLog.audit_user_login_successful( | ||||
|                 request.data["username"], debug_info={"ip": request._client_ip} | ||||
|             ) | ||||
|             return super(LoginView, self).post(request, format=None) | ||||
|         else: | ||||
|             AuditLog.audit_user_failed_twofactor(request.data["username"]) | ||||
|             return Response("bad credentials", status=status.HTTP_400_BAD_REQUEST) | ||||
|             AuditLog.audit_user_failed_twofactor( | ||||
|                 request.data["username"], debug_info={"ip": request._client_ip} | ||||
|             ) | ||||
|             return notify_error("Bad credentials") | ||||
|  | ||||
|  | ||||
| class GetAddUsers(APIView): | ||||
|     permission_classes = [IsAuthenticated, AccountsPerms] | ||||
|  | ||||
|     def get(self, request): | ||||
|         users = User.objects.filter(agent=None) | ||||
|         search = request.GET.get("search", None) | ||||
|  | ||||
|         if search: | ||||
|             users = User.objects.filter(agent=None, is_installer_user=False).filter( | ||||
|                 username__icontains=search | ||||
|             ) | ||||
|         else: | ||||
|             users = User.objects.filter(agent=None, is_installer_user=False) | ||||
|  | ||||
|         return Response(UserSerializer(users, many=True).data) | ||||
|  | ||||
| @@ -96,15 +136,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 +179,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 +228,76 @@ class UserUI(APIView): | ||||
|         serializer.is_valid(raise_exception=True) | ||||
|         serializer.save() | ||||
|         return Response("ok") | ||||
|  | ||||
|  | ||||
| class GetAddRoles(APIView): | ||||
|     permission_classes = [IsAuthenticated, RolesPerms] | ||||
|  | ||||
|     def get(self, request): | ||||
|         roles = Role.objects.all() | ||||
|         return Response(RoleSerializer(roles, many=True).data) | ||||
|  | ||||
|     def post(self, request): | ||||
|         serializer = RoleSerializer(data=request.data) | ||||
|         serializer.is_valid(raise_exception=True) | ||||
|         serializer.save() | ||||
|         return Response("Role was added") | ||||
|  | ||||
|  | ||||
| class GetUpdateDeleteRole(APIView): | ||||
|     permission_classes = [IsAuthenticated, RolesPerms] | ||||
|  | ||||
|     def get(self, request, pk): | ||||
|         role = get_object_or_404(Role, pk=pk) | ||||
|         return Response(RoleSerializer(role).data) | ||||
|  | ||||
|     def put(self, request, pk): | ||||
|         role = get_object_or_404(Role, pk=pk) | ||||
|         serializer = RoleSerializer(instance=role, data=request.data) | ||||
|         serializer.is_valid(raise_exception=True) | ||||
|         serializer.save() | ||||
|         return Response("Role was edited") | ||||
|  | ||||
|     def delete(self, request, pk): | ||||
|         role = get_object_or_404(Role, pk=pk) | ||||
|         role.delete() | ||||
|         return Response("Role was removed") | ||||
|  | ||||
|  | ||||
| class GetAddAPIKeys(APIView): | ||||
|     permission_classes = [IsAuthenticated, APIKeyPerms] | ||||
|  | ||||
|     def get(self, request): | ||||
|         apikeys = APIKey.objects.all() | ||||
|         return Response(APIKeySerializer(apikeys, many=True).data) | ||||
|  | ||||
|     def post(self, request): | ||||
|         # generate a random API Key | ||||
|         from django.utils.crypto import get_random_string | ||||
|  | ||||
|         request.data["key"] = get_random_string(length=32).upper() | ||||
|         serializer = APIKeySerializer(data=request.data) | ||||
|         serializer.is_valid(raise_exception=True) | ||||
|         obj = serializer.save() | ||||
|         return Response("The API Key was added") | ||||
|  | ||||
|  | ||||
| class GetUpdateDeleteAPIKey(APIView): | ||||
|     permission_classes = [IsAuthenticated, APIKeyPerms] | ||||
|  | ||||
|     def put(self, request, pk): | ||||
|         apikey = get_object_or_404(APIKey, pk=pk) | ||||
|  | ||||
|         # remove API key is present in request data | ||||
|         if "key" in request.data.keys(): | ||||
|             request.data.pop("key") | ||||
|  | ||||
|         serializer = APIKeySerializer(instance=apikey, data=request.data, partial=True) | ||||
|         serializer.is_valid(raise_exception=True) | ||||
|         serializer.save() | ||||
|         return Response("The API Key was edited") | ||||
|  | ||||
|     def delete(self, request, pk): | ||||
|         apikey = get_object_or_404(APIKey, pk=pk) | ||||
|         apikey.delete() | ||||
|         return Response("The API Key was deleted") | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| from django.contrib import admin | ||||
|  | ||||
| from .models import Agent, AgentCustomField, Note, RecoveryAction | ||||
| from .models import Agent, AgentCustomField, AgentHistory, Note | ||||
|  | ||||
| admin.site.register(Agent) | ||||
| admin.site.register(RecoveryAction) | ||||
| admin.site.register(Note) | ||||
| admin.site.register(AgentCustomField) | ||||
| admin.site.register(AgentHistory) | ||||
|   | ||||
| @@ -30,7 +30,9 @@ agent = Recipe( | ||||
|     hostname="DESKTOP-TEST123", | ||||
|     version="1.3.0", | ||||
|     monitoring_type=cycle(["workstation", "server"]), | ||||
|     agent_id=seq("asdkj3h4234-1234hg3h4g34-234jjh34|DESKTOP-TEST123"), | ||||
|     agent_id=seq(generate_agent_id("DESKTOP-TEST123")), | ||||
|     last_seen=djangotime.now() - djangotime.timedelta(days=5), | ||||
|     plat="windows", | ||||
| ) | ||||
|  | ||||
| server_agent = agent.extend( | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| from django.core.management.base import BaseCommand | ||||
|  | ||||
| from agents.models import Agent | ||||
| from clients.models import Client, Site | ||||
| from django.core.management.base import BaseCommand | ||||
|  | ||||
|  | ||||
| class Command(BaseCommand): | ||||
|   | ||||
| @@ -0,0 +1,82 @@ | ||||
| import asyncio | ||||
|  | ||||
| from agents.models import Agent | ||||
| from django.core.management.base import BaseCommand | ||||
| from django.utils import timezone as djangotime | ||||
| from packaging import version as pyver | ||||
|  | ||||
| from tacticalrmm.constants import AGENT_DEFER | ||||
| from tacticalrmm.utils import reload_nats | ||||
|  | ||||
|  | ||||
| class Command(BaseCommand): | ||||
|     help = "Delete old agents" | ||||
|  | ||||
|     def add_arguments(self, parser): | ||||
|         parser.add_argument( | ||||
|             "--days", | ||||
|             type=int, | ||||
|             help="Delete agents that have not checked in for this many days", | ||||
|         ) | ||||
|         parser.add_argument( | ||||
|             "--agentver", | ||||
|             type=str, | ||||
|             help="Delete agents that equal to or less than this version", | ||||
|         ) | ||||
|         parser.add_argument( | ||||
|             "--delete", | ||||
|             action="store_true", | ||||
|             help="This will delete agents", | ||||
|         ) | ||||
|  | ||||
|     def handle(self, *args, **kwargs): | ||||
|         days = kwargs["days"] | ||||
|         agentver = kwargs["agentver"] | ||||
|         delete = kwargs["delete"] | ||||
|  | ||||
|         if not days and not agentver: | ||||
|             self.stdout.write( | ||||
|                 self.style.ERROR("Must have at least one parameter: days or agentver") | ||||
|             ) | ||||
|             return | ||||
|  | ||||
|         q = Agent.objects.defer(*AGENT_DEFER) | ||||
|  | ||||
|         agents = [] | ||||
|         if days: | ||||
|             overdue = djangotime.now() - djangotime.timedelta(days=days) | ||||
|             agents = [i for i in q if i.last_seen < overdue] | ||||
|  | ||||
|         if agentver: | ||||
|             agents = [i for i in q if pyver.parse(i.version) <= pyver.parse(agentver)] | ||||
|  | ||||
|         if not agents: | ||||
|             self.stdout.write(self.style.ERROR("No agents matched")) | ||||
|             return | ||||
|  | ||||
|         deleted_count = 0 | ||||
|         for agent in agents: | ||||
|             s = f"{agent.hostname} | Version {agent.version} | Last Seen {agent.last_seen} | {agent.client} > {agent.site}" | ||||
|             if delete: | ||||
|                 s = "Deleting " + s | ||||
|                 self.stdout.write(self.style.SUCCESS(s)) | ||||
|                 asyncio.run(agent.nats_cmd({"func": "uninstall"}, wait=False)) | ||||
|                 try: | ||||
|                     agent.delete() | ||||
|                 except Exception as e: | ||||
|                     err = f"Failed to delete agent {agent.hostname}: {str(e)}" | ||||
|                     self.stdout.write(self.style.ERROR(err)) | ||||
|                 else: | ||||
|                     deleted_count += 1 | ||||
|             else: | ||||
|                 self.stdout.write(self.style.WARNING(s)) | ||||
|  | ||||
|         if delete: | ||||
|             reload_nats() | ||||
|             self.stdout.write(self.style.SUCCESS(f"Deleted {deleted_count} agents")) | ||||
|         else: | ||||
|             self.stdout.write( | ||||
|                 self.style.SUCCESS( | ||||
|                     "The above agents would be deleted. Run again with --delete to actually delete them." | ||||
|                 ) | ||||
|             ) | ||||
							
								
								
									
										39
									
								
								api/tacticalrmm/agents/management/commands/demo_cron.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								api/tacticalrmm/agents/management/commands/demo_cron.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | ||||
| # import datetime as dt | ||||
| import random | ||||
|  | ||||
| from agents.models import Agent | ||||
| from core.tasks import cache_db_fields_task | ||||
| from django.core.management.base import BaseCommand | ||||
| from django.utils import timezone as djangotime | ||||
|  | ||||
|  | ||||
| class Command(BaseCommand): | ||||
|     help = "stuff for demo site in cron" | ||||
|  | ||||
|     def handle(self, *args, **kwargs): | ||||
|  | ||||
|         random_dates = [] | ||||
|         now = djangotime.now() | ||||
|  | ||||
|         for _ in range(20): | ||||
|             rand = now - djangotime.timedelta(minutes=random.randint(1, 2)) | ||||
|             random_dates.append(rand) | ||||
|  | ||||
|         for _ in range(5): | ||||
|             rand = now - djangotime.timedelta(minutes=random.randint(10, 20)) | ||||
|             random_dates.append(rand) | ||||
|  | ||||
|         """ for _ in range(5): | ||||
|             rand = djangotime.now() - djangotime.timedelta(hours=random.randint(1, 10)) | ||||
|             random_dates.append(rand) | ||||
|  | ||||
|         for _ in range(5): | ||||
|             rand = djangotime.now() - djangotime.timedelta(days=random.randint(40, 90)) | ||||
|             random_dates.append(rand) """ | ||||
|  | ||||
|         agents = Agent.objects.only("last_seen") | ||||
|         for agent in agents: | ||||
|             agent.last_seen = random.choice(random_dates) | ||||
|             agent.save(update_fields=["last_seen"]) | ||||
|  | ||||
|         cache_db_fields_task() | ||||
							
								
								
									
										696
									
								
								api/tacticalrmm/agents/management/commands/fake_agents.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										696
									
								
								api/tacticalrmm/agents/management/commands/fake_agents.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,696 @@ | ||||
| import datetime as dt | ||||
| import json | ||||
| import random | ||||
| import string | ||||
|  | ||||
| from accounts.models import User | ||||
| from agents.models import Agent, AgentHistory | ||||
| from automation.models import Policy | ||||
| from autotasks.models import AutomatedTask | ||||
| from checks.models import Check, CheckHistory | ||||
| from clients.models import Client, Site | ||||
| from django.conf import settings | ||||
| from django.core.management import call_command | ||||
| from django.core.management.base import BaseCommand | ||||
| from django.utils import timezone as djangotime | ||||
| from logs.models import AuditLog, PendingAction | ||||
| from scripts.models import Script | ||||
| from software.models import InstalledSoftware | ||||
| from winupdate.models import WinUpdate, WinUpdatePolicy | ||||
|  | ||||
| from tacticalrmm.demo_data import ( | ||||
|     disks, | ||||
|     ping_fail_output, | ||||
|     ping_success_output, | ||||
|     spooler_stdout, | ||||
|     temp_dir_stdout, | ||||
| ) | ||||
|  | ||||
| AGENTS_TO_GENERATE = 250 | ||||
|  | ||||
| SVCS = settings.BASE_DIR.joinpath("tacticalrmm/test_data/winsvcs.json") | ||||
| WMI_1 = settings.BASE_DIR.joinpath("tacticalrmm/test_data/wmi1.json") | ||||
| WMI_2 = settings.BASE_DIR.joinpath("tacticalrmm/test_data/wmi2.json") | ||||
| WMI_3 = settings.BASE_DIR.joinpath("tacticalrmm/test_data/wmi3.json") | ||||
| SW_1 = settings.BASE_DIR.joinpath("tacticalrmm/test_data/software1.json") | ||||
| SW_2 = settings.BASE_DIR.joinpath("tacticalrmm/test_data/software2.json") | ||||
| WIN_UPDATES = settings.BASE_DIR.joinpath("tacticalrmm/test_data/winupdates.json") | ||||
| EVT_LOG_FAIL = settings.BASE_DIR.joinpath( | ||||
|     "tacticalrmm/test_data/eventlog_check_fail.json" | ||||
| ) | ||||
|  | ||||
|  | ||||
| class Command(BaseCommand): | ||||
|     help = "populate database with fake agents" | ||||
|  | ||||
|     def rand_string(self, length): | ||||
|         chars = string.ascii_letters | ||||
|         return "".join(random.choice(chars) for _ in range(length)) | ||||
|  | ||||
|     def handle(self, *args, **kwargs): | ||||
|  | ||||
|         user = User.objects.first() | ||||
|         user.totp_key = "ABSA234234" | ||||
|         user.save(update_fields=["totp_key"]) | ||||
|  | ||||
|         Client.objects.all().delete() | ||||
|         Agent.objects.all().delete() | ||||
|         Check.objects.all().delete() | ||||
|         Script.objects.all().delete() | ||||
|         AutomatedTask.objects.all().delete() | ||||
|         CheckHistory.objects.all().delete() | ||||
|         Policy.objects.all().delete() | ||||
|         AuditLog.objects.all().delete() | ||||
|         PendingAction.objects.all().delete() | ||||
|  | ||||
|         call_command("load_community_scripts") | ||||
|  | ||||
|         # policies | ||||
|         check_policy = Policy() | ||||
|         check_policy.name = "Demo Checks Policy" | ||||
|         check_policy.desc = "Demo Checks Policy" | ||||
|         check_policy.active = True | ||||
|         check_policy.enforced = True | ||||
|         check_policy.save() | ||||
|  | ||||
|         patch_policy = Policy() | ||||
|         patch_policy.name = "Demo Patch Policy" | ||||
|         patch_policy.desc = "Demo Patch Policy" | ||||
|         patch_policy.active = True | ||||
|         patch_policy.enforced = True | ||||
|         patch_policy.save() | ||||
|  | ||||
|         update_policy = WinUpdatePolicy() | ||||
|         update_policy.policy = patch_policy | ||||
|         update_policy.critical = "approve" | ||||
|         update_policy.important = "approve" | ||||
|         update_policy.moderate = "approve" | ||||
|         update_policy.low = "ignore" | ||||
|         update_policy.other = "ignore" | ||||
|         update_policy.run_time_days = [6, 0, 2] | ||||
|         update_policy.run_time_day = 1 | ||||
|         update_policy.reboot_after_install = "required" | ||||
|         update_policy.reprocess_failed = True | ||||
|         update_policy.email_if_fail = True | ||||
|         update_policy.save() | ||||
|  | ||||
|         clients = [ | ||||
|             "Company 2", | ||||
|             "Company 3", | ||||
|             "Company 1", | ||||
|             "Company 4", | ||||
|             "Company 5", | ||||
|             "Company 6", | ||||
|         ] | ||||
|         sites1 = ["HQ1", "LA Office 1", "NY Office 1"] | ||||
|         sites2 = ["HQ2", "LA Office 2", "NY Office 2"] | ||||
|         sites3 = ["HQ3", "LA Office 3", "NY Office 3"] | ||||
|         sites4 = ["HQ4", "LA Office 4", "NY Office 4"] | ||||
|         sites5 = ["HQ5", "LA Office 5", "NY Office 5"] | ||||
|         sites6 = ["HQ6", "LA Office 6", "NY Office 6"] | ||||
|  | ||||
|         client1 = Client(name="Company 1") | ||||
|         client2 = Client(name="Company 2") | ||||
|         client3 = Client(name="Company 3") | ||||
|         client4 = Client(name="Company 4") | ||||
|         client5 = Client(name="Company 5") | ||||
|         client6 = Client(name="Company 6") | ||||
|  | ||||
|         client1.save() | ||||
|         client2.save() | ||||
|         client3.save() | ||||
|         client4.save() | ||||
|         client5.save() | ||||
|         client6.save() | ||||
|  | ||||
|         for site in sites1: | ||||
|             Site(client=client1, name=site).save() | ||||
|  | ||||
|         for site in sites2: | ||||
|             Site(client=client2, name=site).save() | ||||
|  | ||||
|         for site in sites3: | ||||
|             Site(client=client3, name=site).save() | ||||
|  | ||||
|         for site in sites4: | ||||
|             Site(client=client4, name=site).save() | ||||
|  | ||||
|         for site in sites5: | ||||
|             Site(client=client5, name=site).save() | ||||
|  | ||||
|         for site in sites6: | ||||
|             Site(client=client6, name=site).save() | ||||
|  | ||||
|         hostnames = [ | ||||
|             "DC-1", | ||||
|             "DC-2", | ||||
|             "FSV-1", | ||||
|             "FSV-2", | ||||
|             "WSUS", | ||||
|             "DESKTOP-12345", | ||||
|             "LAPTOP-55443", | ||||
|         ] | ||||
|         descriptions = ["Bob's computer", "Primary DC", "File Server", "Karen's Laptop"] | ||||
|         modes = ["server", "workstation"] | ||||
|         op_systems_servers = [ | ||||
|             "Microsoft Windows Server 2016 Standard, 64bit (build 14393)", | ||||
|             "Microsoft Windows Server 2012 R2 Standard, 64bit (build 9600)", | ||||
|             "Microsoft Windows Server 2019 Standard, 64bit (build 17763)", | ||||
|         ] | ||||
|  | ||||
|         op_systems_workstations = [ | ||||
|             "Microsoft Windows 8.1 Pro, 64bit (build 9600)", | ||||
|             "Microsoft Windows 10 Pro for Workstations, 64bit (build 18363)", | ||||
|             "Microsoft Windows 10 Pro, 64bit (build 18363)", | ||||
|         ] | ||||
|  | ||||
|         public_ips = ["65.234.22.4", "74.123.43.5", "44.21.134.45"] | ||||
|  | ||||
|         total_rams = [4, 8, 16, 32, 64, 128] | ||||
|  | ||||
|         now = dt.datetime.now() | ||||
|  | ||||
|         boot_times = [] | ||||
|  | ||||
|         for _ in range(15): | ||||
|             rand_hour = now - dt.timedelta(hours=random.randint(1, 22)) | ||||
|             boot_times.append(str(rand_hour.timestamp())) | ||||
|  | ||||
|         for _ in range(5): | ||||
|             rand_days = now - dt.timedelta(days=random.randint(2, 50)) | ||||
|             boot_times.append(str(rand_days.timestamp())) | ||||
|  | ||||
|         user_names = ["None", "Karen", "Steve", "jsmith", "jdoe"] | ||||
|  | ||||
|         with open(SVCS) as f: | ||||
|             services = json.load(f) | ||||
|  | ||||
|         # WMI | ||||
|         with open(WMI_1) as f: | ||||
|             wmi1 = json.load(f) | ||||
|  | ||||
|         with open(WMI_2) as f: | ||||
|             wmi2 = json.load(f) | ||||
|  | ||||
|         with open(WMI_3) as f: | ||||
|             wmi3 = json.load(f) | ||||
|  | ||||
|         wmi_details = [] | ||||
|         wmi_details.append(wmi1) | ||||
|         wmi_details.append(wmi2) | ||||
|         wmi_details.append(wmi3) | ||||
|  | ||||
|         # software | ||||
|         with open(SW_1) as f: | ||||
|             software1 = json.load(f) | ||||
|  | ||||
|         with open(SW_2) as f: | ||||
|             software2 = json.load(f) | ||||
|  | ||||
|         softwares = [] | ||||
|         softwares.append(software1) | ||||
|         softwares.append(software2) | ||||
|  | ||||
|         # windows updates | ||||
|         with open(WIN_UPDATES) as f: | ||||
|             windows_updates = json.load(f)["samplecomputer"] | ||||
|  | ||||
|         # event log check fail data | ||||
|         with open(EVT_LOG_FAIL) as f: | ||||
|             eventlog_check_fail_data = json.load(f) | ||||
|  | ||||
|         # create scripts | ||||
|  | ||||
|         clear_spool = Script() | ||||
|         clear_spool.name = "Clear Print Spooler" | ||||
|         clear_spool.description = "clears the print spooler. Fuck printers" | ||||
|         clear_spool.filename = "clear_print_spool.bat" | ||||
|         clear_spool.shell = "cmd" | ||||
|         clear_spool.save() | ||||
|  | ||||
|         check_net_aware = Script() | ||||
|         check_net_aware.name = "Check Network Location Awareness" | ||||
|         check_net_aware.description = "Check's network location awareness on domain computers, should always be domain profile and not public or private. Sometimes happens when computer restarts before domain available. This script will return 0 if check passes or 1 if it fails." | ||||
|         check_net_aware.filename = "check_network_loc_aware.ps1" | ||||
|         check_net_aware.shell = "powershell" | ||||
|         check_net_aware.save() | ||||
|  | ||||
|         check_pool_health = Script() | ||||
|         check_pool_health.name = "Check storage spool health" | ||||
|         check_pool_health.description = "loops through all storage pools and will fail if any of them are not healthy" | ||||
|         check_pool_health.filename = "check_storage_pool_health.ps1" | ||||
|         check_pool_health.shell = "powershell" | ||||
|         check_pool_health.save() | ||||
|  | ||||
|         restart_nla = Script() | ||||
|         restart_nla.name = "Restart NLA Service" | ||||
|         restart_nla.description = "restarts the Network Location Awareness windows service to fix the nic profile. Run this after the check network service fails" | ||||
|         restart_nla.filename = "restart_nla.ps1" | ||||
|         restart_nla.shell = "powershell" | ||||
|         restart_nla.save() | ||||
|  | ||||
|         show_tmp_dir_script = Script() | ||||
|         show_tmp_dir_script.name = "Check temp dir" | ||||
|         show_tmp_dir_script.description = "shows files in temp dir using python" | ||||
|         show_tmp_dir_script.filename = "show_temp_dir.py" | ||||
|         show_tmp_dir_script.shell = "python" | ||||
|         show_tmp_dir_script.save() | ||||
|  | ||||
|         for count_agents in range(AGENTS_TO_GENERATE): | ||||
|  | ||||
|             client = random.choice(clients) | ||||
|  | ||||
|             if client == "Company 1": | ||||
|                 site = random.choice(sites1) | ||||
|             elif client == "Company 2": | ||||
|                 site = random.choice(sites2) | ||||
|             elif client == "Company 3": | ||||
|                 site = random.choice(sites3) | ||||
|             elif client == "Company 4": | ||||
|                 site = random.choice(sites4) | ||||
|             elif client == "Company 5": | ||||
|                 site = random.choice(sites5) | ||||
|             elif client == "Company 6": | ||||
|                 site = random.choice(sites6) | ||||
|  | ||||
|             agent = Agent() | ||||
|  | ||||
|             mode = random.choice(modes) | ||||
|             if mode == "server": | ||||
|                 agent.operating_system = random.choice(op_systems_servers) | ||||
|             else: | ||||
|                 agent.operating_system = random.choice(op_systems_workstations) | ||||
|  | ||||
|             agent.hostname = random.choice(hostnames) | ||||
|             agent.version = settings.LATEST_AGENT_VER | ||||
|             agent.site = Site.objects.get(name=site) | ||||
|             agent.agent_id = self.rand_string(25) | ||||
|             agent.description = random.choice(descriptions) | ||||
|             agent.monitoring_type = mode | ||||
|             agent.public_ip = random.choice(public_ips) | ||||
|             agent.last_seen = djangotime.now() | ||||
|             agent.plat = "windows" | ||||
|             agent.plat_release = "windows-2019Server" | ||||
|             agent.total_ram = random.choice(total_rams) | ||||
|             agent.boot_time = random.choice(boot_times) | ||||
|             agent.logged_in_username = random.choice(user_names) | ||||
|             agent.mesh_node_id = ( | ||||
|                 "3UiLhe420@kaVQ0rswzBeonW$WY0xrFFUDBQlcYdXoriLXzvPmBpMrV99vRHXFlb" | ||||
|             ) | ||||
|             agent.overdue_email_alert = random.choice([True, False]) | ||||
|             agent.overdue_text_alert = random.choice([True, False]) | ||||
|             agent.needs_reboot = random.choice([True, False]) | ||||
|             agent.wmi_detail = random.choice(wmi_details) | ||||
|             agent.services = services | ||||
|             agent.disks = random.choice(disks) | ||||
|  | ||||
|             agent.save() | ||||
|  | ||||
|             InstalledSoftware(agent=agent, software=random.choice(softwares)).save() | ||||
|  | ||||
|             if mode == "workstation": | ||||
|                 WinUpdatePolicy(agent=agent, run_time_days=[5, 6]).save() | ||||
|             else: | ||||
|                 WinUpdatePolicy(agent=agent).save() | ||||
|  | ||||
|             # windows updates load | ||||
|             guids = [] | ||||
|             for k in windows_updates.keys(): | ||||
|                 guids.append(k) | ||||
|  | ||||
|             for i in guids: | ||||
|                 WinUpdate( | ||||
|                     agent=agent, | ||||
|                     guid=i, | ||||
|                     kb=windows_updates[i]["KBs"][0], | ||||
|                     title=windows_updates[i]["Title"], | ||||
|                     installed=windows_updates[i]["Installed"], | ||||
|                     downloaded=windows_updates[i]["Downloaded"], | ||||
|                     description=windows_updates[i]["Description"], | ||||
|                     severity=windows_updates[i]["Severity"], | ||||
|                 ).save() | ||||
|  | ||||
|             # agent histories | ||||
|             hist = AgentHistory() | ||||
|             hist.agent = agent | ||||
|             hist.type = "cmd_run" | ||||
|             hist.command = "ping google.com" | ||||
|             hist.username = "demo" | ||||
|             hist.results = ping_success_output | ||||
|             hist.save() | ||||
|  | ||||
|             hist1 = AgentHistory() | ||||
|             hist1.agent = agent | ||||
|             hist1.type = "script_run" | ||||
|             hist1.script = clear_spool | ||||
|             hist1.script_results = { | ||||
|                 "id": 1, | ||||
|                 "stderr": "", | ||||
|                 "stdout": spooler_stdout, | ||||
|                 "execution_time": 3.5554593, | ||||
|                 "retcode": 0, | ||||
|             } | ||||
|             hist1.save() | ||||
|  | ||||
|             # disk space check | ||||
|             check1 = Check() | ||||
|             check1.agent = agent | ||||
|             check1.check_type = "diskspace" | ||||
|             check1.status = "passing" | ||||
|             check1.last_run = djangotime.now() | ||||
|             check1.more_info = "Total: 498.7GB, Free: 287.4GB" | ||||
|             check1.warning_threshold = 25 | ||||
|             check1.error_threshold = 10 | ||||
|             check1.disk = "C:" | ||||
|             check1.email_alert = random.choice([True, False]) | ||||
|             check1.text_alert = random.choice([True, False]) | ||||
|             check1.save() | ||||
|  | ||||
|             for i in range(30): | ||||
|                 check1_history = CheckHistory() | ||||
|                 check1_history.check_id = check1.id | ||||
|                 check1_history.x = djangotime.now() - djangotime.timedelta( | ||||
|                     minutes=i * 2 | ||||
|                 ) | ||||
|                 check1_history.y = random.randint(13, 40) | ||||
|                 check1_history.save() | ||||
|  | ||||
|             # ping check | ||||
|             check2 = Check() | ||||
|             check2.agent = agent | ||||
|             check2.check_type = "ping" | ||||
|             check2.last_run = djangotime.now() | ||||
|             check2.email_alert = random.choice([True, False]) | ||||
|             check2.text_alert = random.choice([True, False]) | ||||
|  | ||||
|             if site in sites5: | ||||
|                 check2.name = "Synology NAS" | ||||
|                 check2.status = "failing" | ||||
|                 check2.ip = "172.17.14.26" | ||||
|                 check2.more_info = ping_fail_output | ||||
|             else: | ||||
|                 check2.name = "Google" | ||||
|                 check2.status = "passing" | ||||
|                 check2.ip = "8.8.8.8" | ||||
|                 check2.more_info = ping_success_output | ||||
|  | ||||
|             check2.save() | ||||
|  | ||||
|             for i in range(30): | ||||
|                 check2_history = CheckHistory() | ||||
|                 check2_history.check_id = check2.id | ||||
|                 check2_history.x = djangotime.now() - djangotime.timedelta( | ||||
|                     minutes=i * 2 | ||||
|                 ) | ||||
|                 if site in sites5: | ||||
|                     check2_history.y = 1 | ||||
|                     check2_history.results = ping_fail_output | ||||
|                 else: | ||||
|                     check2_history.y = 0 | ||||
|                     check2_history.results = ping_success_output | ||||
|                 check2_history.save() | ||||
|  | ||||
|             # cpu load check | ||||
|             check3 = Check() | ||||
|             check3.agent = agent | ||||
|             check3.check_type = "cpuload" | ||||
|             check3.status = "passing" | ||||
|             check3.last_run = djangotime.now() | ||||
|             check3.warning_threshold = 70 | ||||
|             check3.error_threshold = 90 | ||||
|             check3.history = [15, 23, 16, 22, 22, 27, 15, 23, 23, 20, 10, 10, 13, 34] | ||||
|             check3.email_alert = random.choice([True, False]) | ||||
|             check3.text_alert = random.choice([True, False]) | ||||
|             check3.save() | ||||
|  | ||||
|             for i in range(30): | ||||
|                 check3_history = CheckHistory() | ||||
|                 check3_history.check_id = check3.id | ||||
|                 check3_history.x = djangotime.now() - djangotime.timedelta( | ||||
|                     minutes=i * 2 | ||||
|                 ) | ||||
|                 check3_history.y = random.randint(2, 79) | ||||
|                 check3_history.save() | ||||
|  | ||||
|             # memory check | ||||
|             check4 = Check() | ||||
|             check4.agent = agent | ||||
|             check4.check_type = "memory" | ||||
|             check4.status = "passing" | ||||
|             check4.warning_threshold = 70 | ||||
|             check4.error_threshold = 85 | ||||
|             check4.history = [34, 34, 35, 36, 34, 34, 34, 34, 34, 34] | ||||
|             check4.email_alert = random.choice([True, False]) | ||||
|             check4.text_alert = random.choice([True, False]) | ||||
|             check4.save() | ||||
|  | ||||
|             for i in range(30): | ||||
|                 check4_history = CheckHistory() | ||||
|                 check4_history.check_id = check4.id | ||||
|                 check4_history.x = djangotime.now() - djangotime.timedelta( | ||||
|                     minutes=i * 2 | ||||
|                 ) | ||||
|                 check4_history.y = random.randint(2, 79) | ||||
|                 check4_history.save() | ||||
|  | ||||
|             # script check storage pool | ||||
|             check5 = Check() | ||||
|             check5.agent = agent | ||||
|             check5.check_type = "script" | ||||
|             check5.status = "passing" | ||||
|             check5.last_run = djangotime.now() | ||||
|             check5.email_alert = random.choice([True, False]) | ||||
|             check5.text_alert = random.choice([True, False]) | ||||
|             check5.timeout = 120 | ||||
|             check5.retcode = 0 | ||||
|             check5.execution_time = "4.0000" | ||||
|             check5.script = check_pool_health | ||||
|             check5.save() | ||||
|  | ||||
|             for i in range(30): | ||||
|                 check5_history = CheckHistory() | ||||
|                 check5_history.check_id = check5.id | ||||
|                 check5_history.x = djangotime.now() - djangotime.timedelta( | ||||
|                     minutes=i * 2 | ||||
|                 ) | ||||
|                 if i == 10 or i == 18: | ||||
|                     check5_history.y = 1 | ||||
|                 else: | ||||
|                     check5_history.y = 0 | ||||
|                 check5_history.save() | ||||
|  | ||||
|             check6 = Check() | ||||
|             check6.agent = agent | ||||
|             check6.check_type = "script" | ||||
|             check6.status = "passing" | ||||
|             check6.last_run = djangotime.now() | ||||
|             check6.email_alert = random.choice([True, False]) | ||||
|             check6.text_alert = random.choice([True, False]) | ||||
|             check6.timeout = 120 | ||||
|             check6.retcode = 0 | ||||
|             check6.execution_time = "4.0000" | ||||
|             check6.script = check_net_aware | ||||
|             check6.save() | ||||
|  | ||||
|             for i in range(30): | ||||
|                 check6_history = CheckHistory() | ||||
|                 check6_history.check_id = check6.id | ||||
|                 check6_history.x = djangotime.now() - djangotime.timedelta( | ||||
|                     minutes=i * 2 | ||||
|                 ) | ||||
|                 check6_history.y = 0 | ||||
|                 check6_history.save() | ||||
|  | ||||
|             nla_task = AutomatedTask() | ||||
|             nla_task.agent = agent | ||||
|             actions = [ | ||||
|                 { | ||||
|                     "name": restart_nla.name, | ||||
|                     "type": "script", | ||||
|                     "script": restart_nla.pk, | ||||
|                     "timeout": 90, | ||||
|                     "script_args": [], | ||||
|                 } | ||||
|             ] | ||||
|             nla_task.actions = actions | ||||
|             nla_task.assigned_check = check6 | ||||
|             nla_task.name = "Restart NLA" | ||||
|             nla_task.task_type = "checkfailure" | ||||
|             nla_task.win_task_name = "demotask123" | ||||
|             nla_task.execution_time = "1.8443" | ||||
|             nla_task.last_run = djangotime.now() | ||||
|             nla_task.stdout = "no stdout" | ||||
|             nla_task.retcode = 0 | ||||
|             nla_task.sync_status = "synced" | ||||
|             nla_task.save() | ||||
|  | ||||
|             spool_task = AutomatedTask() | ||||
|             spool_task.agent = agent | ||||
|             actions = [ | ||||
|                 { | ||||
|                     "name": clear_spool.name, | ||||
|                     "type": "script", | ||||
|                     "script": clear_spool.pk, | ||||
|                     "timeout": 90, | ||||
|                     "script_args": [], | ||||
|                 } | ||||
|             ] | ||||
|             spool_task.actions = actions | ||||
|             spool_task.name = "Clear the print spooler" | ||||
|             spool_task.task_type = "daily" | ||||
|             spool_task.run_time_date = djangotime.now() + djangotime.timedelta( | ||||
|                 minutes=10 | ||||
|             ) | ||||
|             spool_task.expire_date = djangotime.now() + djangotime.timedelta(days=753) | ||||
|             spool_task.daily_interval = 1 | ||||
|             spool_task.weekly_interval = 1 | ||||
|             spool_task.task_repetition_duration = "2h" | ||||
|             spool_task.task_repetition_interval = "25m" | ||||
|             spool_task.random_task_delay = "3m" | ||||
|             spool_task.win_task_name = "demospool123" | ||||
|             spool_task.last_run = djangotime.now() | ||||
|             spool_task.retcode = 0 | ||||
|             spool_task.stdout = spooler_stdout | ||||
|             spool_task.sync_status = "synced" | ||||
|             spool_task.save() | ||||
|  | ||||
|             tmp_dir_task = AutomatedTask() | ||||
|             tmp_dir_task.agent = agent | ||||
|             tmp_dir_task.name = "show temp dir files" | ||||
|             actions = [ | ||||
|                 { | ||||
|                     "name": show_tmp_dir_script.name, | ||||
|                     "type": "script", | ||||
|                     "script": show_tmp_dir_script.pk, | ||||
|                     "timeout": 90, | ||||
|                     "script_args": [], | ||||
|                 } | ||||
|             ] | ||||
|             tmp_dir_task.actions = actions | ||||
|             tmp_dir_task.task_type = "manual" | ||||
|             tmp_dir_task.win_task_name = "demotemp" | ||||
|             tmp_dir_task.last_run = djangotime.now() | ||||
|             tmp_dir_task.stdout = temp_dir_stdout | ||||
|             tmp_dir_task.retcode = 0 | ||||
|             tmp_dir_task.sync_status = "synced" | ||||
|             tmp_dir_task.save() | ||||
|  | ||||
|             check7 = Check() | ||||
|             check7.agent = agent | ||||
|             check7.check_type = "script" | ||||
|             check7.status = "passing" | ||||
|             check7.last_run = djangotime.now() | ||||
|             check7.email_alert = random.choice([True, False]) | ||||
|             check7.text_alert = random.choice([True, False]) | ||||
|             check7.timeout = 120 | ||||
|             check7.retcode = 0 | ||||
|             check7.execution_time = "3.1337" | ||||
|             check7.script = clear_spool | ||||
|             check7.stdout = spooler_stdout | ||||
|             check7.save() | ||||
|  | ||||
|             for i in range(30): | ||||
|                 check7_history = CheckHistory() | ||||
|                 check7_history.check_id = check7.id | ||||
|                 check7_history.x = djangotime.now() - djangotime.timedelta( | ||||
|                     minutes=i * 2 | ||||
|                 ) | ||||
|                 check7_history.y = 0 | ||||
|                 check7_history.save() | ||||
|  | ||||
|             check8 = Check() | ||||
|             check8.agent = agent | ||||
|             check8.check_type = "winsvc" | ||||
|             check8.status = "passing" | ||||
|             check8.last_run = djangotime.now() | ||||
|             check8.email_alert = random.choice([True, False]) | ||||
|             check8.text_alert = random.choice([True, False]) | ||||
|             check8.more_info = "Status RUNNING" | ||||
|             check8.fails_b4_alert = 4 | ||||
|             check8.svc_name = "Spooler" | ||||
|             check8.svc_display_name = "Print Spooler" | ||||
|             check8.pass_if_start_pending = False | ||||
|             check8.restart_if_stopped = True | ||||
|             check8.save() | ||||
|  | ||||
|             for i in range(30): | ||||
|                 check8_history = CheckHistory() | ||||
|                 check8_history.check_id = check8.id | ||||
|                 check8_history.x = djangotime.now() - djangotime.timedelta( | ||||
|                     minutes=i * 2 | ||||
|                 ) | ||||
|                 if i == 10 or i == 18: | ||||
|                     check8_history.y = 1 | ||||
|                     check8_history.results = "Status STOPPED" | ||||
|                 else: | ||||
|                     check8_history.y = 0 | ||||
|                     check8_history.results = "Status RUNNING" | ||||
|                 check8_history.save() | ||||
|  | ||||
|             check9 = Check() | ||||
|             check9.agent = agent | ||||
|             check9.check_type = "eventlog" | ||||
|             check9.name = "unexpected shutdown" | ||||
|  | ||||
|             check9.last_run = djangotime.now() | ||||
|             check9.email_alert = random.choice([True, False]) | ||||
|             check9.text_alert = random.choice([True, False]) | ||||
|             check9.fails_b4_alert = 2 | ||||
|  | ||||
|             if site in sites5: | ||||
|                 check9.extra_details = eventlog_check_fail_data | ||||
|                 check9.status = "failing" | ||||
|             else: | ||||
|                 check9.extra_details = {"log": []} | ||||
|                 check9.status = "passing" | ||||
|  | ||||
|             check9.log_name = "Application" | ||||
|             check9.event_id = 1001 | ||||
|             check9.event_type = "INFO" | ||||
|             check9.fail_when = "contains" | ||||
|             check9.search_last_days = 30 | ||||
|  | ||||
|             check9.save() | ||||
|  | ||||
|             for i in range(30): | ||||
|                 check9_history = CheckHistory() | ||||
|                 check9_history.check_id = check9.id | ||||
|                 check9_history.x = djangotime.now() - djangotime.timedelta( | ||||
|                     minutes=i * 2 | ||||
|                 ) | ||||
|                 if i == 10 or i == 18: | ||||
|                     check9_history.y = 1 | ||||
|                     check9_history.results = "Events Found: 16" | ||||
|                 else: | ||||
|                     check9_history.y = 0 | ||||
|                     check9_history.results = "Events Found: 0" | ||||
|                 check9_history.save() | ||||
|  | ||||
|             pick = random.randint(1, 10) | ||||
|  | ||||
|             if pick == 5 or pick == 3: | ||||
|  | ||||
|                 reboot_time = djangotime.now() + djangotime.timedelta( | ||||
|                     minutes=random.randint(1000, 500000) | ||||
|                 ) | ||||
|                 date_obj = dt.datetime.strftime(reboot_time, "%Y-%m-%d %H:%M") | ||||
|  | ||||
|                 obj = dt.datetime.strptime(date_obj, "%Y-%m-%d %H:%M") | ||||
|  | ||||
|                 task_name = "TacticalRMM_SchedReboot_" + "".join( | ||||
|                     random.choice(string.ascii_letters) for _ in range(10) | ||||
|                 ) | ||||
|  | ||||
|                 sched_reboot = PendingAction() | ||||
|                 sched_reboot.agent = agent | ||||
|                 sched_reboot.action_type = "schedreboot" | ||||
|                 sched_reboot.details = { | ||||
|                     "time": str(obj), | ||||
|                     "taskname": task_name, | ||||
|                 } | ||||
|                 sched_reboot.save() | ||||
|  | ||||
|             self.stdout.write(self.style.SUCCESS(f"Added agent # {count_agents + 1}")) | ||||
|  | ||||
|         call_command("load_demo_scripts") | ||||
|         self.stdout.write("done") | ||||
| @@ -1,16 +0,0 @@ | ||||
| from django.core.management.base import BaseCommand | ||||
|  | ||||
| from agents.models import Agent | ||||
|  | ||||
|  | ||||
| class Command(BaseCommand): | ||||
|     help = "Changes existing agents salt_id from a property to a model field" | ||||
|  | ||||
|     def handle(self, *args, **kwargs): | ||||
|         agents = Agent.objects.filter(salt_id=None) | ||||
|         for agent in agents: | ||||
|             self.stdout.write( | ||||
|                 self.style.SUCCESS(f"Setting salt_id on {agent.hostname}") | ||||
|             ) | ||||
|             agent.salt_id = f"{agent.hostname}-{agent.pk}" | ||||
|             agent.save(update_fields=["salt_id"]) | ||||
| @@ -1,8 +1,7 @@ | ||||
| from agents.models import Agent | ||||
| from django.conf import settings | ||||
| from django.core.management.base import BaseCommand | ||||
|  | ||||
| from agents.models import Agent | ||||
|  | ||||
|  | ||||
| class Command(BaseCommand): | ||||
|     help = "Shows online agents that are not on the latest version" | ||||
|   | ||||
							
								
								
									
										25
									
								
								api/tacticalrmm/agents/management/commands/update_agents.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								api/tacticalrmm/agents/management/commands/update_agents.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| from agents.models import Agent | ||||
| from agents.tasks import send_agent_update_task | ||||
| from core.models import CoreSettings | ||||
| from django.conf import settings | ||||
| from django.core.management.base import BaseCommand | ||||
| from packaging import version as pyver | ||||
|  | ||||
| from tacticalrmm.constants import AGENT_DEFER | ||||
|  | ||||
|  | ||||
| class Command(BaseCommand): | ||||
|     help = "Triggers an agent update task to run" | ||||
|  | ||||
|     def handle(self, *args, **kwargs): | ||||
|         core = CoreSettings.objects.first() | ||||
|         if not core.agent_auto_update:  # type: ignore | ||||
|             return | ||||
|  | ||||
|         q = Agent.objects.defer(*AGENT_DEFER).exclude(version=settings.LATEST_AGENT_VER) | ||||
|         agent_ids: list[str] = [ | ||||
|             i.agent_id | ||||
|             for i in q | ||||
|             if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER) | ||||
|         ] | ||||
|         send_agent_update_task.delay(agent_ids=agent_ids) | ||||
| @@ -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 | ||||
|  | ||||
| import django.db.models.deletion | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| 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 | ||||
|  | ||||
| import django.db.models.deletion | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('scripts', '0008_script_guid'), | ||||
|         ('agents', '0038_agenthistory'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='agenthistory', | ||||
|             name='script', | ||||
|             field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='history', to='scripts.script'), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='agenthistory', | ||||
|             name='script_results', | ||||
|             field=models.JSONField(blank=True, null=True), | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										28
									
								
								api/tacticalrmm/agents/migrations/0040_auto_20211010_0249.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								api/tacticalrmm/agents/migrations/0040_auto_20211010_0249.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | ||||
| # Generated by Django 3.2.6 on 2021-10-10 02:49 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('agents', '0039_auto_20210714_0738'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name='agent', | ||||
|             name='agent_id', | ||||
|             field=models.CharField(max_length=200, unique=True), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name='agent', | ||||
|             name='created_by', | ||||
|             field=models.CharField(blank=True, max_length=255, null=True), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name='agent', | ||||
|             name='modified_by', | ||||
|             field=models.CharField(blank=True, max_length=255, null=True), | ||||
|         ), | ||||
|     ] | ||||
| @@ -0,0 +1,18 @@ | ||||
| # Generated by Django 3.2.6 on 2021-10-18 03:04 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('agents', '0040_auto_20211010_0249'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name='agenthistory', | ||||
|             name='username', | ||||
|             field=models.CharField(default='system', max_length=255), | ||||
|         ), | ||||
|     ] | ||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										25
									
								
								api/tacticalrmm/agents/migrations/0043_auto_20220227_0554.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								api/tacticalrmm/agents/migrations/0043_auto_20220227_0554.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| # Generated by Django 3.2.12 on 2022-02-27 05:54 | ||||
|  | ||||
| from django.db import migrations | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('agents', '0042_alter_agent_time_zone'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.RemoveField( | ||||
|             model_name='agent', | ||||
|             name='antivirus', | ||||
|         ), | ||||
|         migrations.RemoveField( | ||||
|             model_name='agent', | ||||
|             name='local_ip', | ||||
|         ), | ||||
|         migrations.RemoveField( | ||||
|             model_name='agent', | ||||
|             name='used_ram', | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										22
									
								
								api/tacticalrmm/agents/migrations/0044_auto_20220227_0717.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								api/tacticalrmm/agents/migrations/0044_auto_20220227_0717.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| # Generated by Django 3.2.12 on 2022-02-27 07:17 | ||||
|  | ||||
| from django.db import migrations | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('agents', '0043_auto_20220227_0554'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.RenameField( | ||||
|             model_name='agent', | ||||
|             old_name='salt_id', | ||||
|             new_name='goarch', | ||||
|         ), | ||||
|         migrations.RemoveField( | ||||
|             model_name='agent', | ||||
|             name='salt_ver', | ||||
|         ), | ||||
|     ] | ||||
| @@ -0,0 +1,16 @@ | ||||
| # Generated by Django 3.2.12 on 2022-03-12 02:30 | ||||
|  | ||||
| from django.db import migrations | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('agents', '0044_auto_20220227_0717'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.DeleteModel( | ||||
|             name='RecoveryAction', | ||||
|         ), | ||||
|     ] | ||||
| @@ -0,0 +1,18 @@ | ||||
| # Generated by Django 3.2.12 on 2022-03-17 17:15 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('agents', '0045_delete_recoveryaction'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name='agenthistory', | ||||
|             name='command', | ||||
|             field=models.TextField(blank=True, default='', null=True), | ||||
|         ), | ||||
|     ] | ||||
| @@ -7,7 +7,10 @@ from distutils.version import LooseVersion | ||||
| from typing import Any | ||||
|  | ||||
| import msgpack | ||||
| import nats | ||||
| import validators | ||||
| from asgiref.sync import sync_to_async | ||||
| from core.models import TZ_CHOICES, CoreSettings | ||||
| from Crypto.Cipher import AES | ||||
| from Crypto.Hash import SHA3_384 | ||||
| from Crypto.Random import get_random_bytes | ||||
| @@ -16,36 +19,30 @@ 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 logs.models import BaseAuditModel, DebugLog | ||||
| from nats.errors import TimeoutError | ||||
|  | ||||
| from core.models import TZ_CHOICES, CoreSettings | ||||
| from logs.models import BaseAuditModel | ||||
|  | ||||
| logger.configure(**settings.LOG_CONFIG) | ||||
| from tacticalrmm.models import PermissionQuerySet | ||||
|  | ||||
|  | ||||
| class Agent(BaseAuditModel): | ||||
|     objects = PermissionQuerySet.as_manager() | ||||
|  | ||||
|     version = models.CharField(default="0.1.0", max_length=255) | ||||
|     salt_ver = models.CharField(default="1.0.3", max_length=255) | ||||
|     operating_system = models.CharField(null=True, blank=True, max_length=255) | ||||
|     plat = models.CharField(max_length=255, null=True, blank=True) | ||||
|     goarch = models.CharField(max_length=255, null=True, blank=True) | ||||
|     plat_release = models.CharField(max_length=255, null=True, blank=True) | ||||
|     hostname = models.CharField(max_length=255) | ||||
|     salt_id = models.CharField(null=True, blank=True, max_length=255) | ||||
|     local_ip = models.TextField(null=True, blank=True)  # deprecated | ||||
|     agent_id = models.CharField(max_length=200) | ||||
|     agent_id = models.CharField(max_length=200, unique=True) | ||||
|     last_seen = models.DateTimeField(null=True, blank=True) | ||||
|     services = models.JSONField(null=True, blank=True) | ||||
|     public_ip = models.CharField(null=True, max_length=255) | ||||
|     total_ram = models.IntegerField(null=True, blank=True) | ||||
|     used_ram = models.IntegerField(null=True, blank=True)  # deprecated | ||||
|     disks = models.JSONField(null=True, blank=True) | ||||
|     boot_time = models.FloatField(null=True, blank=True) | ||||
|     logged_in_username = models.CharField(null=True, blank=True, max_length=255) | ||||
|     last_logged_in_user = models.CharField(null=True, blank=True, max_length=255) | ||||
|     antivirus = models.CharField(default="n/a", max_length=255)  # deprecated | ||||
|     monitoring_type = models.CharField(max_length=30) | ||||
|     description = models.CharField(null=True, blank=True, max_length=255) | ||||
|     mesh_node_id = models.CharField(null=True, blank=True, max_length=255) | ||||
| @@ -63,6 +60,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 +86,24 @@ class Agent(BaseAuditModel): | ||||
|     ) | ||||
|  | ||||
|     def save(self, *args, **kwargs): | ||||
|  | ||||
|         # 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 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() | ||||
|             from automation.tasks import generate_agent_checks_task | ||||
|  | ||||
|             generate_agent_checks_task.delay(agents=[self.pk], create_tasks=True) | ||||
|  | ||||
|     def __str__(self): | ||||
|         return self.hostname | ||||
| @@ -118,10 +120,17 @@ 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 is_posix(self): | ||||
|         return self.plat == "linux" or self.plat == "darwin" | ||||
|  | ||||
|     @property | ||||
|     def arch(self): | ||||
|         if self.is_posix: | ||||
|             return self.goarch | ||||
|  | ||||
|         if self.operating_system is not None: | ||||
|             if "64 bit" in self.operating_system or "64bit" in self.operating_system: | ||||
|                 return "64" | ||||
| @@ -160,10 +169,6 @@ class Agent(BaseAuditModel): | ||||
|         else: | ||||
|             return "offline" | ||||
|  | ||||
|     @property | ||||
|     def has_patches_pending(self): | ||||
|         return self.winupdates.filter(action="approve").filter(installed=False).exists()  # type: ignore | ||||
|  | ||||
|     @property | ||||
|     def checks(self): | ||||
|         total, passing, failing, warning, info = 0, 0, 0, 0, 0 | ||||
| @@ -193,6 +198,12 @@ class Agent(BaseAuditModel): | ||||
|  | ||||
|     @property | ||||
|     def cpu_model(self): | ||||
|         if self.is_posix: | ||||
|             try: | ||||
|                 return self.wmi_detail["cpus"] | ||||
|             except: | ||||
|                 return ["unknown cpu model"] | ||||
|  | ||||
|         ret = [] | ||||
|         try: | ||||
|             cpus = self.wmi_detail["cpu"] | ||||
| @@ -204,6 +215,14 @@ class Agent(BaseAuditModel): | ||||
|  | ||||
|     @property | ||||
|     def graphics(self): | ||||
|         if self.is_posix: | ||||
|             try: | ||||
|                 if not self.wmi_detail["gpus"]: | ||||
|                     return "No graphics cards" | ||||
|                 return self.wmi_detail["gpus"] | ||||
|             except: | ||||
|                 return "Error getting graphics cards" | ||||
|  | ||||
|         ret, mrda = [], [] | ||||
|         try: | ||||
|             graphics = self.wmi_detail["graphics"] | ||||
| @@ -225,6 +244,12 @@ class Agent(BaseAuditModel): | ||||
|  | ||||
|     @property | ||||
|     def local_ips(self): | ||||
|         if self.is_posix: | ||||
|             try: | ||||
|                 return ", ".join(self.wmi_detail["local_ips"]) | ||||
|             except: | ||||
|                 return "error getting local ips" | ||||
|  | ||||
|         ret = [] | ||||
|         try: | ||||
|             ips = self.wmi_detail["network_config"] | ||||
| @@ -251,6 +276,12 @@ class Agent(BaseAuditModel): | ||||
|  | ||||
|     @property | ||||
|     def make_model(self): | ||||
|         if self.is_posix: | ||||
|             try: | ||||
|                 return self.wmi_detail["make_model"] | ||||
|             except: | ||||
|                 return "error getting make/model" | ||||
|  | ||||
|         try: | ||||
|             comp_sys = self.wmi_detail["comp_sys"][0] | ||||
|             comp_sys_prod = self.wmi_detail["comp_sys_prod"][0] | ||||
| @@ -262,6 +293,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 | ||||
| @@ -276,6 +312,12 @@ class Agent(BaseAuditModel): | ||||
|  | ||||
|     @property | ||||
|     def physical_disks(self): | ||||
|         if self.is_posix: | ||||
|             try: | ||||
|                 return self.wmi_detail["disks"] | ||||
|             except: | ||||
|                 return ["unknown disk"] | ||||
|  | ||||
|         try: | ||||
|             disks = self.wmi_detail["disk"] | ||||
|             ret = [] | ||||
| @@ -297,6 +339,37 @@ class Agent(BaseAuditModel): | ||||
|         except: | ||||
|             return ["unknown disk"] | ||||
|  | ||||
|     def is_supported_script(self, platforms: list) -> bool: | ||||
|         return self.plat.lower() in platforms if platforms else True | ||||
|  | ||||
|     def get_agent_policies(self): | ||||
|         site_policy = getattr(self.site, f"{self.monitoring_type}_policy", None) | ||||
|         client_policy = getattr(self.client, f"{self.monitoring_type}_policy", None) | ||||
|         default_policy = getattr( | ||||
|             CoreSettings.objects.first(), f"{self.monitoring_type}_policy", None | ||||
|         ) | ||||
|  | ||||
|         return { | ||||
|             "agent_policy": self.policy | ||||
|             if self.policy and not self.policy.is_agent_excluded(self) | ||||
|             else None, | ||||
|             "site_policy": site_policy | ||||
|             if (site_policy and not site_policy.is_agent_excluded(self)) | ||||
|             and not self.block_policy_inheritance | ||||
|             else None, | ||||
|             "client_policy": client_policy | ||||
|             if (client_policy and not client_policy.is_agent_excluded(self)) | ||||
|             and not self.block_policy_inheritance | ||||
|             and not self.site.block_policy_inheritance | ||||
|             else None, | ||||
|             "default_policy": default_policy | ||||
|             if (default_policy and not default_policy.is_agent_excluded(self)) | ||||
|             and not self.block_policy_inheritance | ||||
|             and not self.site.block_policy_inheritance | ||||
|             and not self.client.block_policy_inheritance | ||||
|             else None, | ||||
|         } | ||||
|  | ||||
|     def check_run_interval(self) -> int: | ||||
|         interval = self.check_interval | ||||
|         # determine if any agent checks have a custom interval and set the lowest interval | ||||
| @@ -319,6 +392,7 @@ class Agent(BaseAuditModel): | ||||
|         full: bool = False, | ||||
|         wait: bool = False, | ||||
|         run_on_any: bool = False, | ||||
|         history_pk: int = 0, | ||||
|     ) -> Any: | ||||
|  | ||||
|         from scripts.models import Script | ||||
| @@ -337,6 +411,9 @@ class Agent(BaseAuditModel): | ||||
|             }, | ||||
|         } | ||||
|  | ||||
|         if history_pk != 0: | ||||
|             data["id"] = history_pk | ||||
|  | ||||
|         running_agent = self | ||||
|         if run_on_any: | ||||
|             nats_ping = {"func": "ping"} | ||||
| @@ -409,58 +486,20 @@ class Agent(BaseAuditModel): | ||||
|     def get_patch_policy(self): | ||||
|  | ||||
|         # check if site has a patch policy and if so use it | ||||
|         site = self.site | ||||
|         core_settings = CoreSettings.objects.first() | ||||
|         patch_policy = None | ||||
|         agent_policy = self.winupdatepolicy.get()  # type: ignore | ||||
|         agent_policy = self.winupdatepolicy.first()  # type: ignore | ||||
|  | ||||
|         if self.monitoring_type == "server": | ||||
|             # check agent policy first which should override client or site policy | ||||
|             if self.policy and self.policy.winupdatepolicy.exists(): | ||||
|                 patch_policy = self.policy.winupdatepolicy.get() | ||||
|         policies = self.get_agent_policies() | ||||
|  | ||||
|             # 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() | ||||
|  | ||||
|             # if site doesn't have a patch policy check the client | ||||
|             elif ( | ||||
|                 site.client.server_policy | ||||
|                 and site.client.server_policy.winupdatepolicy.exists() | ||||
|         processed_policies = list() | ||||
|         for _, policy in policies.items(): | ||||
|             if ( | ||||
|                 policy | ||||
|                 and policy.active | ||||
|                 and policy.pk not in processed_policies | ||||
|                 and policy.winupdatepolicy.exists() | ||||
|             ): | ||||
|                 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() | ||||
|             ): | ||||
|                 patch_policy = core_settings.server_policy.winupdatepolicy.get() | ||||
|  | ||||
|         elif self.monitoring_type == "workstation": | ||||
|             # check agent policy first which should override client or site policy | ||||
|             if self.policy and self.policy.winupdatepolicy.exists(): | ||||
|                 patch_policy = self.policy.winupdatepolicy.get() | ||||
|  | ||||
|             elif ( | ||||
|                 site.workstation_policy | ||||
|                 and site.workstation_policy.winupdatepolicy.exists() | ||||
|             ): | ||||
|                 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() | ||||
|  | ||||
|             # if patch policy still doesn't exist check default policy | ||||
|             elif ( | ||||
|                 core_settings.workstation_policy | ||||
|                 and core_settings.workstation_policy.winupdatepolicy.exists() | ||||
|             ): | ||||
|                 patch_policy = core_settings.workstation_policy.winupdatepolicy.get() | ||||
|                 patch_policy = policy.winupdatepolicy.first() | ||||
|  | ||||
|         # if policy still doesn't exist return the agent patch policy | ||||
|         if not patch_policy: | ||||
| @@ -507,114 +546,55 @@ class Agent(BaseAuditModel): | ||||
|     # sets alert template assigned in the following order: policy, site, client, global | ||||
|     # sets None if nothing is found | ||||
|     def set_alert_template(self): | ||||
|  | ||||
|         site = self.site | ||||
|         client = self.client | ||||
|         core = CoreSettings.objects.first() | ||||
|         policies = self.get_agent_policies() | ||||
|  | ||||
|         templates = list() | ||||
|         # check if alert template is on a policy assigned to agent | ||||
|         if ( | ||||
|             self.policy | ||||
|             and self.policy.alert_template | ||||
|             and self.policy.alert_template.is_active | ||||
|         ): | ||||
|             templates.append(self.policy.alert_template) | ||||
|  | ||||
|         # check if policy with alert template is assigned to the site | ||||
|         if ( | ||||
|             self.monitoring_type == "server" | ||||
|             and site.server_policy | ||||
|             and site.server_policy.alert_template | ||||
|             and site.server_policy.alert_template.is_active | ||||
|         ): | ||||
|             templates.append(site.server_policy.alert_template) | ||||
|         if ( | ||||
|             self.monitoring_type == "workstation" | ||||
|             and site.workstation_policy | ||||
|             and site.workstation_policy.alert_template | ||||
|             and site.workstation_policy.alert_template.is_active | ||||
|         ): | ||||
|             templates.append(site.workstation_policy.alert_template) | ||||
|  | ||||
|         # check if alert template is assigned to site | ||||
|         if site.alert_template and site.alert_template.is_active: | ||||
|             templates.append(site.alert_template) | ||||
|  | ||||
|         # check if policy with alert template is assigned to the client | ||||
|         if ( | ||||
|             self.monitoring_type == "server" | ||||
|             and client.server_policy | ||||
|             and client.server_policy.alert_template | ||||
|             and client.server_policy.alert_template.is_active | ||||
|         ): | ||||
|             templates.append(client.server_policy.alert_template) | ||||
|         if ( | ||||
|             self.monitoring_type == "workstation" | ||||
|             and client.workstation_policy | ||||
|             and client.workstation_policy.alert_template | ||||
|             and client.workstation_policy.alert_template.is_active | ||||
|         ): | ||||
|             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: | ||||
|             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 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 | ||||
|         ): | ||||
|             templates.append(core.server_policy.alert_template) | ||||
|         if ( | ||||
|             self.monitoring_type == "workstation" | ||||
|             and core.workstation_policy | ||||
|             and core.workstation_policy.alert_template | ||||
|             and core.workstation_policy.alert_template.is_active | ||||
|         ): | ||||
|             templates.append(core.workstation_policy.alert_template) | ||||
|  | ||||
|         # go through the templates and return the first one that isn't excluded | ||||
|         for template in templates: | ||||
|             # check if client, site, or agent has been excluded from template | ||||
|         # loop through all policies applied to agent and return an alert_template if found | ||||
|         processed_policies = list() | ||||
|         for key, policy in policies.items(): | ||||
|             # default alert_template will override a default policy with alert template applied | ||||
|             if ( | ||||
|                 client.pk | ||||
|                 in template.excluded_clients.all().values_list("pk", flat=True) | ||||
|                 or site.pk in template.excluded_sites.all().values_list("pk", flat=True) | ||||
|                 or self.pk | ||||
|                 in template.excluded_agents.all() | ||||
|                 .only("pk") | ||||
|                 .values_list("pk", flat=True) | ||||
|                 "default" in key | ||||
|                 and core.alert_template | ||||
|                 and core.alert_template.is_active | ||||
|                 and not core.alert_template.is_agent_excluded(self) | ||||
|             ): | ||||
|                 continue | ||||
|  | ||||
|             # check if template is excluding desktops | ||||
|                 self.alert_template = core.alert_template | ||||
|                 self.save(update_fields=["alert_template"]) | ||||
|                 return core.alert_template | ||||
|             elif ( | ||||
|                 self.monitoring_type == "workstation" and template.exclude_workstations | ||||
|                 policy | ||||
|                 and policy.active | ||||
|                 and policy.pk not in processed_policies | ||||
|                 and policy.alert_template | ||||
|                 and policy.alert_template.is_active | ||||
|                 and not policy.alert_template.is_agent_excluded(self) | ||||
|             ): | ||||
|                 continue | ||||
|  | ||||
|             # check if template is excluding servers | ||||
|             elif self.monitoring_type == "server" and template.exclude_servers: | ||||
|                 continue | ||||
|  | ||||
|             else: | ||||
|                 # save alert_template to agent cache field | ||||
|                 self.alert_template = template | ||||
|                 self.save() | ||||
|  | ||||
|                 return template | ||||
|                 self.alert_template = policy.alert_template | ||||
|                 self.save(update_fields=["alert_template"]) | ||||
|                 return policy.alert_template | ||||
|             elif ( | ||||
|                 "site" in key | ||||
|                 and self.site.alert_template | ||||
|                 and self.site.alert_template.is_active | ||||
|                 and not self.site.alert_template.is_agent_excluded(self) | ||||
|             ): | ||||
|                 self.alert_template = self.site.alert_template | ||||
|                 self.save(update_fields=["alert_template"]) | ||||
|                 return self.site.alert_template | ||||
|             elif ( | ||||
|                 "client" in key | ||||
|                 and self.site.client.alert_template | ||||
|                 and self.site.client.alert_template.is_active | ||||
|                 and not self.site.client.alert_template.is_agent_excluded(self) | ||||
|             ): | ||||
|                 self.alert_template = self.site.client.alert_template | ||||
|                 self.save(update_fields=["alert_template"]) | ||||
|                 return self.site.client.alert_template | ||||
|  | ||||
|         # no alert templates found or agent has been excluded | ||||
|         self.alert_template = None | ||||
|         self.save() | ||||
|         self.save(update_fields=["alert_template"]) | ||||
|  | ||||
|         return None | ||||
|  | ||||
| @@ -640,7 +620,7 @@ class Agent(BaseAuditModel): | ||||
|             key1 = key[0:48] | ||||
|             key2 = key[48:] | ||||
|             msg = '{{"a":{}, "u":"{}","time":{}}}'.format( | ||||
|                 action, user, int(time.time()) | ||||
|                 action, user.lower(), int(time.time()) | ||||
|             ) | ||||
|             iv = get_random_bytes(16) | ||||
|  | ||||
| @@ -657,8 +637,10 @@ class Agent(BaseAuditModel): | ||||
|         except Exception: | ||||
|             return "err" | ||||
|  | ||||
|     def _do_nats_debug(self, agent, message): | ||||
|         DebugLog.error(agent=agent, log_type="agent_issues", message=message) | ||||
|  | ||||
|     async def nats_cmd(self, data: dict, timeout: int = 30, wait: bool = True): | ||||
|         nc = NATS() | ||||
|         options = { | ||||
|             "servers": f"tls://{settings.ALLOWED_HOSTS[0]}:4222", | ||||
|             "user": "tacticalrmm", | ||||
| @@ -666,8 +648,9 @@ class Agent(BaseAuditModel): | ||||
|             "connect_timeout": 3, | ||||
|             "max_reconnect_attempts": 2, | ||||
|         } | ||||
|  | ||||
|         try: | ||||
|             await nc.connect(**options) | ||||
|             nc = await nats.connect(**options) | ||||
|         except: | ||||
|             return "natsdown" | ||||
|  | ||||
| @@ -676,14 +659,16 @@ class Agent(BaseAuditModel): | ||||
|                 msg = await nc.request( | ||||
|                     self.agent_id, msgpack.dumps(data), timeout=timeout | ||||
|                 ) | ||||
|             except ErrTimeout: | ||||
|             except TimeoutError: | ||||
|                 ret = "timeout" | ||||
|             else: | ||||
|                 try: | ||||
|                     ret = msgpack.loads(msg.data)  # type: ignore | ||||
|                 except Exception as e: | ||||
|                     logger.error(e) | ||||
|                     ret = str(e) | ||||
|                     await sync_to_async(self._do_nats_debug, thread_sensitive=False)( | ||||
|                         agent=self, message=ret | ||||
|                     ) | ||||
|  | ||||
|             await nc.close() | ||||
|             return ret | ||||
| @@ -695,12 +680,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: | ||||
| @@ -715,7 +697,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) | ||||
| @@ -731,36 +713,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 | ||||
| @@ -780,7 +732,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}, " | ||||
| @@ -795,7 +747,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}, " | ||||
| @@ -810,7 +762,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, | ||||
|         ) | ||||
| @@ -819,36 +771,15 @@ 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, | ||||
|         ) | ||||
|  | ||||
|  | ||||
| RECOVERY_CHOICES = [ | ||||
|     ("salt", "Salt"), | ||||
|     ("mesh", "Mesh"), | ||||
|     ("command", "Command"), | ||||
|     ("rpc", "Nats RPC"), | ||||
|     ("checkrunner", "Checkrunner"), | ||||
| ] | ||||
|  | ||||
|  | ||||
| class RecoveryAction(models.Model): | ||||
|     agent = models.ForeignKey( | ||||
|         Agent, | ||||
|         related_name="recoveryactions", | ||||
|         on_delete=models.CASCADE, | ||||
|     ) | ||||
|     mode = models.CharField(max_length=50, choices=RECOVERY_CHOICES, default="mesh") | ||||
|     command = models.TextField(null=True, blank=True) | ||||
|     last_run = models.DateTimeField(null=True, blank=True) | ||||
|  | ||||
|     def __str__(self): | ||||
|         return f"{self.agent.hostname} - {self.mode}" | ||||
|  | ||||
|  | ||||
| class Note(models.Model): | ||||
|     objects = PermissionQuerySet.as_manager() | ||||
|  | ||||
|     agent = models.ForeignKey( | ||||
|         Agent, | ||||
|         related_name="notes", | ||||
| @@ -869,6 +800,8 @@ class Note(models.Model): | ||||
|  | ||||
|  | ||||
| class AgentCustomField(models.Model): | ||||
|     objects = PermissionQuerySet.as_manager() | ||||
|  | ||||
|     agent = models.ForeignKey( | ||||
|         Agent, | ||||
|         related_name="custom_fields", | ||||
| @@ -891,7 +824,7 @@ class AgentCustomField(models.Model): | ||||
|     ) | ||||
|  | ||||
|     def __str__(self): | ||||
|         return self.field | ||||
|         return self.field.name | ||||
|  | ||||
|     @property | ||||
|     def value(self): | ||||
| @@ -901,3 +834,59 @@ class AgentCustomField(models.Model): | ||||
|             return self.bool_value | ||||
|         else: | ||||
|             return self.string_value | ||||
|  | ||||
|     def save_to_field(self, value): | ||||
|         if self.field.type in [ | ||||
|             "text", | ||||
|             "number", | ||||
|             "single", | ||||
|             "datetime", | ||||
|         ]: | ||||
|             self.string_value = value | ||||
|             self.save() | ||||
|         elif self.field.type == "multiple": | ||||
|             self.multiple_value = value.split(",") | ||||
|             self.save() | ||||
|         elif self.field.type == "checkbox": | ||||
|             self.bool_value = bool(value) | ||||
|             self.save() | ||||
|  | ||||
|  | ||||
| AGENT_HISTORY_TYPES = ( | ||||
|     ("task_run", "Task Run"), | ||||
|     ("script_run", "Script Run"), | ||||
|     ("cmd_run", "CMD Run"), | ||||
| ) | ||||
|  | ||||
| AGENT_HISTORY_STATUS = (("success", "Success"), ("failure", "Failure")) | ||||
|  | ||||
|  | ||||
| class AgentHistory(models.Model): | ||||
|     objects = PermissionQuerySet.as_manager() | ||||
|  | ||||
|     agent = models.ForeignKey( | ||||
|         Agent, | ||||
|         related_name="history", | ||||
|         on_delete=models.CASCADE, | ||||
|     ) | ||||
|     time = models.DateTimeField(auto_now_add=True) | ||||
|     type = models.CharField( | ||||
|         max_length=50, choices=AGENT_HISTORY_TYPES, default="cmd_run" | ||||
|     ) | ||||
|     command = models.TextField(null=True, blank=True, default="") | ||||
|     status = models.CharField( | ||||
|         max_length=50, choices=AGENT_HISTORY_STATUS, default="success" | ||||
|     ) | ||||
|     username = models.CharField(max_length=255, default="system") | ||||
|     results = models.TextField(null=True, blank=True) | ||||
|     script = models.ForeignKey( | ||||
|         "scripts.Script", | ||||
|         null=True, | ||||
|         blank=True, | ||||
|         related_name="history", | ||||
|         on_delete=models.SET_NULL, | ||||
|     ) | ||||
|     script_results = models.JSONField(null=True, blank=True) | ||||
|  | ||||
|     def __str__(self): | ||||
|         return f"{self.agent.hostname} - {self.type}" | ||||
|   | ||||
							
								
								
									
										123
									
								
								api/tacticalrmm/agents/permissions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								api/tacticalrmm/agents/permissions.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,123 @@ | ||||
| from rest_framework import permissions | ||||
|  | ||||
| from tacticalrmm.permissions import _has_perm, _has_perm_on_agent | ||||
|  | ||||
|  | ||||
| class AgentPerms(permissions.BasePermission): | ||||
|     def has_permission(self, r, view): | ||||
|         if r.method == "GET": | ||||
|             if "agent_id" in view.kwargs.keys(): | ||||
|                 return _has_perm(r, "can_list_agents") and _has_perm_on_agent( | ||||
|                     r.user, view.kwargs["agent_id"] | ||||
|                 ) | ||||
|             else: | ||||
|                 return _has_perm(r, "can_list_agents") | ||||
|         elif r.method == "DELETE": | ||||
|             return _has_perm(r, "can_uninstall_agents") and _has_perm_on_agent( | ||||
|                 r.user, view.kwargs["agent_id"] | ||||
|             ) | ||||
|         else: | ||||
|             if r.path == "/agents/maintenance/bulk/": | ||||
|                 return _has_perm(r, "can_edit_agent") | ||||
|             else: | ||||
|                 return _has_perm(r, "can_edit_agent") and _has_perm_on_agent( | ||||
|                     r.user, view.kwargs["agent_id"] | ||||
|                 ) | ||||
|  | ||||
|  | ||||
| class RecoverAgentPerms(permissions.BasePermission): | ||||
|     def has_permission(self, r, view): | ||||
|         return _has_perm(r, "can_recover_agents") and _has_perm_on_agent( | ||||
|             r.user, view.kwargs["agent_id"] | ||||
|         ) | ||||
|  | ||||
|  | ||||
| class MeshPerms(permissions.BasePermission): | ||||
|     def has_permission(self, r, view): | ||||
|         return _has_perm(r, "can_use_mesh") and _has_perm_on_agent( | ||||
|             r.user, view.kwargs["agent_id"] | ||||
|         ) | ||||
|  | ||||
|  | ||||
| class UpdateAgentPerms(permissions.BasePermission): | ||||
|     def has_permission(self, r, view): | ||||
|         return _has_perm(r, "can_update_agents") | ||||
|  | ||||
|  | ||||
| class PingAgentPerms(permissions.BasePermission): | ||||
|     def has_permission(self, r, view): | ||||
|         return _has_perm(r, "can_ping_agents") and _has_perm_on_agent( | ||||
|             r.user, view.kwargs["agent_id"] | ||||
|         ) | ||||
|  | ||||
|  | ||||
| class ManageProcPerms(permissions.BasePermission): | ||||
|     def has_permission(self, r, view): | ||||
|         return _has_perm(r, "can_manage_procs") and _has_perm_on_agent( | ||||
|             r.user, view.kwargs["agent_id"] | ||||
|         ) | ||||
|  | ||||
|  | ||||
| class EvtLogPerms(permissions.BasePermission): | ||||
|     def has_permission(self, r, view): | ||||
|         return _has_perm(r, "can_view_eventlogs") and _has_perm_on_agent( | ||||
|             r.user, view.kwargs["agent_id"] | ||||
|         ) | ||||
|  | ||||
|  | ||||
| class SendCMDPerms(permissions.BasePermission): | ||||
|     def has_permission(self, r, view): | ||||
|         return _has_perm(r, "can_send_cmd") and _has_perm_on_agent( | ||||
|             r.user, view.kwargs["agent_id"] | ||||
|         ) | ||||
|  | ||||
|  | ||||
| class RebootAgentPerms(permissions.BasePermission): | ||||
|     def has_permission(self, r, view): | ||||
|         return _has_perm(r, "can_reboot_agents") and _has_perm_on_agent( | ||||
|             r.user, view.kwargs["agent_id"] | ||||
|         ) | ||||
|  | ||||
|  | ||||
| class InstallAgentPerms(permissions.BasePermission): | ||||
|     def has_permission(self, r, view): | ||||
|         return _has_perm(r, "can_install_agents") | ||||
|  | ||||
|  | ||||
| class RunScriptPerms(permissions.BasePermission): | ||||
|     def has_permission(self, r, view): | ||||
|         return _has_perm(r, "can_run_scripts") and _has_perm_on_agent( | ||||
|             r.user, view.kwargs["agent_id"] | ||||
|         ) | ||||
|  | ||||
|  | ||||
| class AgentNotesPerms(permissions.BasePermission): | ||||
|     def has_permission(self, r, view): | ||||
|  | ||||
|         # permissions for GET /agents/notes/ endpoint | ||||
|         if r.method == "GET": | ||||
|  | ||||
|             # permissions for /agents/<agent_id>/notes endpoint | ||||
|             if "agent_id" in view.kwargs.keys(): | ||||
|                 return _has_perm(r, "can_list_notes") and _has_perm_on_agent( | ||||
|                     r.user, view.kwargs["agent_id"] | ||||
|                 ) | ||||
|             else: | ||||
|                 return _has_perm(r, "can_list_notes") | ||||
|         else: | ||||
|             return _has_perm(r, "can_manage_notes") | ||||
|  | ||||
|  | ||||
| class RunBulkPerms(permissions.BasePermission): | ||||
|     def has_permission(self, r, view): | ||||
|         return _has_perm(r, "can_run_bulk") | ||||
|  | ||||
|  | ||||
| class AgentHistoryPerms(permissions.BasePermission): | ||||
|     def has_permission(self, r, view): | ||||
|         if "agent_id" in view.kwargs.keys(): | ||||
|             return _has_perm(r, "can_list_agent_history") and _has_perm_on_agent( | ||||
|                 r.user, view.kwargs["agent_id"] | ||||
|             ) | ||||
|         else: | ||||
|             return _has_perm(r, "can_list_agent_history") | ||||
| @@ -1,15 +1,30 @@ | ||||
| import pytz | ||||
| from rest_framework import serializers | ||||
|  | ||||
| from clients.serializers import ClientSerializer | ||||
| from winupdate.serializers import WinUpdatePolicySerializer | ||||
|  | ||||
| from .models import Agent, AgentCustomField, Note | ||||
| from .models import Agent, AgentCustomField, AgentHistory, Note | ||||
|  | ||||
|  | ||||
| class AgentCustomFieldSerializer(serializers.ModelSerializer): | ||||
|     class Meta: | ||||
|         model = AgentCustomField | ||||
|         fields = ( | ||||
|             "id", | ||||
|             "field", | ||||
|             "agent", | ||||
|             "value", | ||||
|             "string_value", | ||||
|             "bool_value", | ||||
|             "multiple_value", | ||||
|         ) | ||||
|         extra_kwargs = { | ||||
|             "string_value": {"write_only": True}, | ||||
|             "bool_value": {"write_only": True}, | ||||
|             "multiple_value": {"write_only": True}, | ||||
|         } | ||||
|  | ||||
|  | ||||
| class AgentSerializer(serializers.ModelSerializer): | ||||
|     # for vue | ||||
|     patches_pending = serializers.ReadOnlyField(source="has_patches_pending") | ||||
|     winupdatepolicy = WinUpdatePolicySerializer(many=True, read_only=True) | ||||
|     status = serializers.ReadOnlyField() | ||||
|     cpu_model = serializers.ReadOnlyField() | ||||
| @@ -20,33 +35,48 @@ class AgentSerializer(serializers.ModelSerializer): | ||||
|     checks = serializers.ReadOnlyField() | ||||
|     timezone = serializers.ReadOnlyField() | ||||
|     all_timezones = serializers.SerializerMethodField() | ||||
|     client_name = serializers.ReadOnlyField(source="client.name") | ||||
|     client = serializers.ReadOnlyField(source="client.name") | ||||
|     site_name = serializers.ReadOnlyField(source="site.name") | ||||
|     custom_fields = AgentCustomFieldSerializer(many=True, read_only=True) | ||||
|     patches_last_installed = serializers.ReadOnlyField() | ||||
|     last_seen = serializers.ReadOnlyField() | ||||
|     applied_policies = serializers.SerializerMethodField() | ||||
|     effective_patch_policy = serializers.SerializerMethodField() | ||||
|     alert_template = serializers.SerializerMethodField() | ||||
|  | ||||
|     def get_alert_template(self, obj): | ||||
|         from alerts.serializers import AlertTemplateSerializer | ||||
|  | ||||
|         return ( | ||||
|             AlertTemplateSerializer(obj.alert_template).data | ||||
|             if obj.alert_template | ||||
|             else None | ||||
|         ) | ||||
|  | ||||
|     def get_effective_patch_policy(self, obj): | ||||
|         return WinUpdatePolicySerializer(obj.get_patch_policy()).data | ||||
|  | ||||
|     def get_applied_policies(self, obj): | ||||
|         from automation.serializers import PolicySerializer | ||||
|  | ||||
|         policies = obj.get_agent_policies() | ||||
|  | ||||
|         # need to serialize model objects manually | ||||
|         for key, policy in policies.items(): | ||||
|             if policy: | ||||
|                 policies[key] = PolicySerializer(policy).data | ||||
|  | ||||
|         return policies | ||||
|  | ||||
|     def get_all_timezones(self, obj): | ||||
|         return pytz.all_timezones | ||||
|  | ||||
|     class Meta: | ||||
|         model = Agent | ||||
|         exclude = [ | ||||
|             "last_seen", | ||||
|         ] | ||||
|  | ||||
|  | ||||
| class AgentOverdueActionSerializer(serializers.ModelSerializer): | ||||
|     class Meta: | ||||
|         model = Agent | ||||
|         fields = [ | ||||
|             "pk", | ||||
|             "overdue_email_alert", | ||||
|             "overdue_text_alert", | ||||
|             "overdue_dashboard_alert", | ||||
|         ] | ||||
|         exclude = ["id"] | ||||
|  | ||||
|  | ||||
| class AgentTableSerializer(serializers.ModelSerializer): | ||||
|     patches_pending = serializers.ReadOnlyField(source="has_patches_pending") | ||||
|     pending_actions = serializers.SerializerMethodField() | ||||
|     status = serializers.ReadOnlyField() | ||||
|     checks = serializers.ReadOnlyField() | ||||
|     last_seen = serializers.SerializerMethodField() | ||||
| @@ -69,9 +99,6 @@ class AgentTableSerializer(serializers.ModelSerializer): | ||||
|                 "always_alert": obj.alert_template.agent_always_alert, | ||||
|             } | ||||
|  | ||||
|     def get_pending_actions(self, obj): | ||||
|         return obj.pendingactions.filter(status="pending").count() | ||||
|  | ||||
|     def get_last_seen(self, obj) -> str: | ||||
|         if obj.time_zone is not None: | ||||
|             agent_tz = pytz.timezone(obj.time_zone) | ||||
| @@ -94,17 +121,16 @@ class AgentTableSerializer(serializers.ModelSerializer): | ||||
|     class Meta: | ||||
|         model = Agent | ||||
|         fields = [ | ||||
|             "id", | ||||
|             "agent_id", | ||||
|             "alert_template", | ||||
|             "hostname", | ||||
|             "agent_id", | ||||
|             "site_name", | ||||
|             "client_name", | ||||
|             "monitoring_type", | ||||
|             "description", | ||||
|             "needs_reboot", | ||||
|             "patches_pending", | ||||
|             "pending_actions", | ||||
|             "has_patches_pending", | ||||
|             "pending_actions_count", | ||||
|             "status", | ||||
|             "overdue_text_alert", | ||||
|             "overdue_email_alert", | ||||
| @@ -116,67 +142,14 @@ class AgentTableSerializer(serializers.ModelSerializer): | ||||
|             "logged_username", | ||||
|             "italic", | ||||
|             "policy", | ||||
|             "block_policy_inheritance", | ||||
|             "plat", | ||||
|             "goarch", | ||||
|         ] | ||||
|         depth = 2 | ||||
|  | ||||
|  | ||||
| class AgentCustomFieldSerializer(serializers.ModelSerializer): | ||||
|     class Meta: | ||||
|         model = AgentCustomField | ||||
|         fields = ( | ||||
|             "id", | ||||
|             "field", | ||||
|             "agent", | ||||
|             "value", | ||||
|             "string_value", | ||||
|             "bool_value", | ||||
|             "multiple_value", | ||||
|         ) | ||||
|         extra_kwargs = { | ||||
|             "string_value": {"write_only": True}, | ||||
|             "bool_value": {"write_only": True}, | ||||
|             "multiple_value": {"write_only": True}, | ||||
|         } | ||||
|  | ||||
|  | ||||
| class AgentEditSerializer(serializers.ModelSerializer): | ||||
|     winupdatepolicy = WinUpdatePolicySerializer(many=True, read_only=True) | ||||
|     all_timezones = serializers.SerializerMethodField() | ||||
|     client = ClientSerializer(read_only=True) | ||||
|     custom_fields = AgentCustomFieldSerializer(many=True, read_only=True) | ||||
|  | ||||
|     def get_all_timezones(self, obj): | ||||
|         return pytz.all_timezones | ||||
|  | ||||
|     class Meta: | ||||
|         model = Agent | ||||
|         fields = [ | ||||
|             "id", | ||||
|             "hostname", | ||||
|             "client", | ||||
|             "site", | ||||
|             "monitoring_type", | ||||
|             "description", | ||||
|             "time_zone", | ||||
|             "timezone", | ||||
|             "check_interval", | ||||
|             "overdue_time", | ||||
|             "offline_time", | ||||
|             "overdue_text_alert", | ||||
|             "overdue_email_alert", | ||||
|             "all_timezones", | ||||
|             "winupdatepolicy", | ||||
|             "policy", | ||||
|             "custom_fields", | ||||
|         ] | ||||
|  | ||||
|  | ||||
| class WinAgentSerializer(serializers.ModelSerializer): | ||||
|     # for the windows agent | ||||
|     patches_pending = serializers.ReadOnlyField(source="has_patches_pending") | ||||
|     winupdatepolicy = WinUpdatePolicySerializer(many=True, read_only=True) | ||||
|     status = serializers.ReadOnlyField() | ||||
|  | ||||
|     class Meta: | ||||
|         model = Agent | ||||
|         fields = "__all__" | ||||
| @@ -189,24 +162,38 @@ class AgentHostnameSerializer(serializers.ModelSerializer): | ||||
|     class Meta: | ||||
|         model = Agent | ||||
|         fields = ( | ||||
|             "id", | ||||
|             "hostname", | ||||
|             "pk", | ||||
|             "agent_id", | ||||
|             "client", | ||||
|             "site", | ||||
|         ) | ||||
|  | ||||
|  | ||||
| class NoteSerializer(serializers.ModelSerializer): | ||||
| class AgentNoteSerializer(serializers.ModelSerializer): | ||||
|     username = serializers.ReadOnlyField(source="user.username") | ||||
|     agent_id = serializers.ReadOnlyField(source="agent.agent_id") | ||||
|  | ||||
|     class Meta: | ||||
|         model = Note | ||||
|         fields = "__all__" | ||||
|         fields = ("pk", "entry_time", "agent", "user", "note", "username", "agent_id") | ||||
|         extra_kwargs = {"agent": {"write_only": True}, "user": {"write_only": True}} | ||||
|  | ||||
|  | ||||
| class NotesSerializer(serializers.ModelSerializer): | ||||
|     notes = NoteSerializer(many=True, read_only=True) | ||||
| class AgentHistorySerializer(serializers.ModelSerializer): | ||||
|     time = serializers.SerializerMethodField(read_only=True) | ||||
|     script_name = serializers.ReadOnlyField(source="script.name") | ||||
|  | ||||
|     class Meta: | ||||
|         model = AgentHistory | ||||
|         fields = "__all__" | ||||
|  | ||||
|     def get_time(self, history): | ||||
|         tz = self.context["default_tz"] | ||||
|         return history.time.astimezone(tz).strftime("%m %d %Y %H:%M:%S") | ||||
|  | ||||
|  | ||||
| class AgentAuditSerializer(serializers.ModelSerializer): | ||||
|     class Meta: | ||||
|         model = Agent | ||||
|         fields = ["hostname", "pk", "notes"] | ||||
|         exclude = ["disks", "services", "wmi_detail"] | ||||
|   | ||||
| @@ -1,66 +1,58 @@ | ||||
| import asyncio | ||||
| import datetime as dt | ||||
| import random | ||||
| import urllib.parse | ||||
| from time import sleep | ||||
| from typing import Union | ||||
|  | ||||
| from agents.models import Agent | ||||
| from agents.utils import get_agent_url | ||||
| from core.models import CoreSettings | ||||
| from django.conf import settings | ||||
| from django.utils import timezone as djangotime | ||||
| from loguru import logger | ||||
| from logs.models import DebugLog, PendingAction | ||||
| from packaging import version as pyver | ||||
|  | ||||
| from agents.models import Agent | ||||
| from core.models import CodeSignToken, CoreSettings | ||||
| from logs.models import PendingAction | ||||
| from scripts.models import Script | ||||
|  | ||||
| from tacticalrmm.celery import app | ||||
| from tacticalrmm.utils import run_nats_api_cmd | ||||
|  | ||||
| logger.configure(**settings.LOG_CONFIG) | ||||
|  | ||||
|  | ||||
| def agent_update(pk: int, codesigntoken: str = None) -> str: | ||||
|     from agents.utils import get_exegen_url | ||||
| def agent_update(agent_id: str, force: bool = False) -> str: | ||||
|  | ||||
|     agent = Agent.objects.get(pk=pk) | ||||
|     agent = Agent.objects.get(agent_id=agent_id) | ||||
|  | ||||
|     if pyver.parse(agent.version) <= pyver.parse("1.3.0"): | ||||
|         return "not supported" | ||||
|  | ||||
|     # skip if we can't determine the arch | ||||
|     if agent.arch is None: | ||||
|         logger.warning( | ||||
|             f"Unable to determine arch on {agent.hostname}. Skipping agent update." | ||||
|         DebugLog.warning( | ||||
|             agent=agent, | ||||
|             log_type="agent_issues", | ||||
|             message=f"Unable to determine arch on {agent.hostname}({agent.agent_id}). Skipping agent update.", | ||||
|         ) | ||||
|         return "noarch" | ||||
|  | ||||
|     version = settings.LATEST_AGENT_VER | ||||
|     inno = agent.win_inno_exe | ||||
|     url = get_agent_url(agent.arch, agent.plat) | ||||
|  | ||||
|     if codesigntoken is not None and pyver.parse(version) >= pyver.parse("1.5.0"): | ||||
|         base_url = get_exegen_url() + "/api/v1/winagents/?" | ||||
|         params = {"version": version, "arch": agent.arch, "token": codesigntoken} | ||||
|         url = base_url + urllib.parse.urlencode(params) | ||||
|     else: | ||||
|         url = agent.winagent_dl | ||||
|  | ||||
|     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", | ||||
| @@ -75,16 +67,21 @@ def agent_update(pk: int, codesigntoken: str = None) -> str: | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| def send_agent_update_task(pks: list[int]) -> None: | ||||
|     try: | ||||
|         codesigntoken = CodeSignToken.objects.first().token | ||||
|     except: | ||||
|         codesigntoken = None | ||||
|  | ||||
|     chunks = (pks[i : i + 30] for i in range(0, len(pks), 30)) | ||||
| def force_code_sign(agent_ids: list[str]) -> None: | ||||
|     chunks = (agent_ids[i : i + 50] for i in range(0, len(agent_ids), 50)) | ||||
|     for chunk in chunks: | ||||
|         for pk in chunk: | ||||
|             agent_update(pk, codesigntoken) | ||||
|         for agent_id in chunk: | ||||
|             agent_update(agent_id=agent_id, force=True) | ||||
|             sleep(0.05) | ||||
|         sleep(4) | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| def send_agent_update_task(agent_ids: list[str]) -> None: | ||||
|     chunks = (agent_ids[i : i + 50] for i in range(0, len(agent_ids), 50)) | ||||
|     for chunk in chunks: | ||||
|         for agent_id in chunk: | ||||
|             agent_update(agent_id) | ||||
|             sleep(0.05) | ||||
|         sleep(4) | ||||
|  | ||||
| @@ -92,25 +89,20 @@ def send_agent_update_task(pks: list[int]) -> None: | ||||
| @app.task | ||||
| def auto_self_agent_update_task() -> None: | ||||
|     core = CoreSettings.objects.first() | ||||
|     if not core.agent_auto_update: | ||||
|     if not core.agent_auto_update:  # type:ignore | ||||
|         return | ||||
|  | ||||
|     try: | ||||
|         codesigntoken = CodeSignToken.objects.first().token | ||||
|     except: | ||||
|         codesigntoken = None | ||||
|  | ||||
|     q = Agent.objects.only("pk", "version") | ||||
|     pks: list[int] = [ | ||||
|         i.pk | ||||
|     q = Agent.objects.only("agent_id", "version") | ||||
|     agent_ids: list[str] = [ | ||||
|         i.agent_id | ||||
|         for i in q | ||||
|         if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER) | ||||
|     ] | ||||
|  | ||||
|     chunks = (pks[i : i + 30] for i in range(0, len(pks), 30)) | ||||
|     chunks = (agent_ids[i : i + 30] for i in range(0, len(agent_ids), 30)) | ||||
|     for chunk in chunks: | ||||
|         for pk in chunk: | ||||
|             agent_update(pk, codesigntoken) | ||||
|         for agent_id in chunk: | ||||
|             agent_update(agent_id) | ||||
|             sleep(0.05) | ||||
|         sleep(4) | ||||
|  | ||||
| @@ -195,6 +187,7 @@ def agent_outages_task() -> None: | ||||
|  | ||||
|     agents = Agent.objects.only( | ||||
|         "pk", | ||||
|         "agent_id", | ||||
|         "last_seen", | ||||
|         "offline_time", | ||||
|         "overdue_time", | ||||
| @@ -215,14 +208,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() | ||||
| @@ -238,43 +241,68 @@ 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=str(e)) | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| def monitor_agents_task() -> None: | ||||
|     agents = Agent.objects.only( | ||||
|         "pk", "agent_id", "last_seen", "overdue_time", "offline_time" | ||||
| def clear_faults_task(older_than_days: int) -> None: | ||||
|     # https://github.com/amidaware/tacticalrmm/issues/484 | ||||
|     agents = Agent.objects.exclude(last_seen__isnull=True).filter( | ||||
|         last_seen__lt=djangotime.now() - djangotime.timedelta(days=older_than_days) | ||||
|     ) | ||||
|     ids = [i.agent_id for i in agents if i.status != "online"] | ||||
|     run_nats_api_cmd("monitor", ids) | ||||
|     for agent in agents: | ||||
|         if agent.agentchecks.exists(): | ||||
|             for check in agent.agentchecks.all(): | ||||
|                 # reset check status | ||||
|                 check.status = "passing" | ||||
|                 check.save(update_fields=["status"]) | ||||
|                 if check.alert.filter(resolved=False).exists(): | ||||
|                     check.alert.get(resolved=False).resolve() | ||||
|  | ||||
|         # reset overdue alerts | ||||
|         agent.overdue_email_alert = False | ||||
|         agent.overdue_text_alert = False | ||||
|         agent.overdue_dashboard_alert = False | ||||
|         agent.save( | ||||
|             update_fields=[ | ||||
|                 "overdue_email_alert", | ||||
|                 "overdue_text_alert", | ||||
|                 "overdue_dashboard_alert", | ||||
|             ] | ||||
|         ) | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| 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) | ||||
| 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" | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,32 +1,43 @@ | ||||
| from autotasks.views import GetAddAutoTasks | ||||
| from checks.views import GetAddChecks | ||||
| from django.urls import path | ||||
| from logs.views import PendingActions | ||||
|  | ||||
| from . import views | ||||
|  | ||||
| urlpatterns = [ | ||||
|     path("listagents/", views.AgentsTableList.as_view()), | ||||
|     path("listagentsnodetail/", views.list_agents_no_detail), | ||||
|     path("<int:pk>/agenteditdetails/", views.agent_edit_details), | ||||
|     path("overdueaction/", views.overdue_action), | ||||
|     path("sendrawcmd/", views.send_raw_cmd), | ||||
|     path("<pk>/agentdetail/", views.agent_detail), | ||||
|     path("<int:pk>/meshcentral/", views.meshcentral), | ||||
|     path("<str:arch>/getmeshexe/", views.get_mesh_exe), | ||||
|     path("uninstall/", views.uninstall), | ||||
|     path("editagent/", views.edit_agent), | ||||
|     path("<pk>/geteventlog/<logtype>/<days>/", views.get_event_log), | ||||
|     path("getagentversions/", views.get_agent_versions), | ||||
|     path("updateagents/", views.update_agents), | ||||
|     path("<pk>/getprocs/", views.get_processes), | ||||
|     path("<pk>/<pid>/killproc/", views.kill_proc), | ||||
|     path("reboot/", views.Reboot.as_view()), | ||||
|     path("installagent/", views.install_agent), | ||||
|     path("<int:pk>/ping/", views.ping), | ||||
|     path("recover/", views.recover), | ||||
|     path("runscript/", views.run_script), | ||||
|     path("<int:pk>/recovermesh/", views.recover_mesh), | ||||
|     path("<int:pk>/notes/", views.GetAddNotes.as_view()), | ||||
|     path("<int:pk>/note/", views.GetEditDeleteNote.as_view()), | ||||
|     path("bulk/", views.bulk), | ||||
|     path("maintenance/", views.agent_maintenance), | ||||
|     path("<int:pk>/wmi/", views.WMI.as_view()), | ||||
|     # agent views | ||||
|     path("", views.GetAgents.as_view()), | ||||
|     path("<agent:agent_id>/", views.GetUpdateDeleteAgent.as_view()), | ||||
|     path("<agent:agent_id>/cmd/", views.send_raw_cmd), | ||||
|     path("<agent:agent_id>/runscript/", views.run_script), | ||||
|     path("<agent:agent_id>/wmi/", views.WMI.as_view()), | ||||
|     path("<agent:agent_id>/recover/", views.recover), | ||||
|     path("<agent:agent_id>/reboot/", views.Reboot.as_view()), | ||||
|     path("<agent:agent_id>/ping/", views.ping), | ||||
|     # alias for checks get view | ||||
|     path("<agent:agent_id>/checks/", GetAddChecks.as_view()), | ||||
|     # alias for autotasks get view | ||||
|     path("<agent:agent_id>/tasks/", GetAddAutoTasks.as_view()), | ||||
|     # alias for pending actions get view | ||||
|     path("<agent:agent_id>/pendingactions/", PendingActions.as_view()), | ||||
|     # agent remote background | ||||
|     path("<agent:agent_id>/meshcentral/", views.AgentMeshCentral.as_view()), | ||||
|     path("<agent:agent_id>/meshcentral/recover/", views.AgentMeshCentral.as_view()), | ||||
|     path("<agent:agent_id>/processes/", views.AgentProcesses.as_view()), | ||||
|     path("<agent:agent_id>/processes/<int:pid>/", views.AgentProcesses.as_view()), | ||||
|     path("<agent:agent_id>/eventlog/<str:logtype>/<int:days>/", views.get_event_log), | ||||
|     # agent history | ||||
|     path("history/", views.AgentHistoryView.as_view()), | ||||
|     path("<agent:agent_id>/history/", views.AgentHistoryView.as_view()), | ||||
|     # agent notes | ||||
|     path("notes/", views.GetAddNotes.as_view()), | ||||
|     path("notes/<int:pk>/", views.GetEditDeleteNote.as_view()), | ||||
|     path("<agent:agent_id>/notes/", views.GetAddNotes.as_view()), | ||||
|     # bulk actions | ||||
|     path("maintenance/bulk/", views.agent_maintenance), | ||||
|     path("actions/bulk/", views.bulk), | ||||
|     path("versions/", views.get_agent_versions), | ||||
|     path("update/", views.update_agents), | ||||
|     path("installer/", views.install_agent), | ||||
| ] | ||||
|   | ||||
| @@ -1,37 +1,88 @@ | ||||
| import random | ||||
| import asyncio | ||||
| import tempfile | ||||
| import urllib.parse | ||||
|  | ||||
| import requests | ||||
| from core.models import CodeSignToken, CoreSettings | ||||
| from core.utils import get_mesh_device_id, get_mesh_ws_url | ||||
| from django.conf import settings | ||||
| from django.http import FileResponse | ||||
|  | ||||
| from tacticalrmm.constants import MeshAgentIdent | ||||
|  | ||||
|  | ||||
| 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 | ||||
| def get_agent_url(arch: str, plat: str) -> str: | ||||
|  | ||||
|         if r.status_code == 200: | ||||
|             return url | ||||
|  | ||||
|     return random.choice(urls) | ||||
|  | ||||
|  | ||||
| def get_winagent_url(arch: str) -> str: | ||||
|     from core.models import CodeSignToken | ||||
|     if plat == "windows": | ||||
|         endpoint = "winagents" | ||||
|         dl_url = settings.DL_32 if arch == "32" else settings.DL_64 | ||||
|     else: | ||||
|         endpoint = "linuxagents" | ||||
|         dl_url = "" | ||||
|  | ||||
|     try: | ||||
|         codetoken = CodeSignToken.objects.first().token | ||||
|         base_url = get_exegen_url() + "/api/v1/winagents/?" | ||||
|         params = { | ||||
|             "version": settings.LATEST_AGENT_VER, | ||||
|             "arch": arch, | ||||
|             "token": codetoken, | ||||
|         } | ||||
|         dl_url = base_url + urllib.parse.urlencode(params) | ||||
|         t: CodeSignToken = CodeSignToken.objects.first()  # type: ignore | ||||
|         if t.is_valid: | ||||
|             base_url = settings.EXE_GEN_URL + f"/api/v1/{endpoint}/?" | ||||
|             params = { | ||||
|                 "version": settings.LATEST_AGENT_VER, | ||||
|                 "arch": arch, | ||||
|                 "token": t.token, | ||||
|             } | ||||
|             dl_url = base_url + urllib.parse.urlencode(params) | ||||
|     except: | ||||
|         dl_url = settings.DL_64 if arch == "64" else settings.DL_32 | ||||
|         pass | ||||
|  | ||||
|     return dl_url | ||||
|  | ||||
|  | ||||
| def generate_linux_install( | ||||
|     client: str, | ||||
|     site: str, | ||||
|     agent_type: str, | ||||
|     arch: str, | ||||
|     token: str, | ||||
|     api: str, | ||||
|     download_url: str, | ||||
| ) -> FileResponse: | ||||
|  | ||||
|     match arch: | ||||
|         case "amd64": | ||||
|             arch_id = MeshAgentIdent.LINUX64 | ||||
|         case "386": | ||||
|             arch_id = MeshAgentIdent.LINUX32 | ||||
|         case "arm64": | ||||
|             arch_id = MeshAgentIdent.LINUX_ARM_64 | ||||
|         case "arm": | ||||
|             arch_id = MeshAgentIdent.LINUX_ARM_HF | ||||
|  | ||||
|     core: CoreSettings = CoreSettings.objects.first()  # type: ignore | ||||
|  | ||||
|     uri = get_mesh_ws_url() | ||||
|     mesh_id = asyncio.run(get_mesh_device_id(uri, core.mesh_device_group)) | ||||
|     mesh_dl = f"{core.mesh_site}/meshagents?id={mesh_id}&installflags=0&meshinstall={arch_id}"  # type: ignore | ||||
|  | ||||
|     sh = settings.LINUX_AGENT_SCRIPT | ||||
|     with open(sh, "r") as f: | ||||
|         text = f.read() | ||||
|  | ||||
|     replace = { | ||||
|         "agentDLChange": download_url, | ||||
|         "meshDLChange": mesh_dl, | ||||
|         "clientIDChange": client, | ||||
|         "siteIDChange": site, | ||||
|         "agentTypeChange": agent_type, | ||||
|         "tokenChange": token, | ||||
|         "apiURLChange": api, | ||||
|     } | ||||
|  | ||||
|     for i, j in replace.items(): | ||||
|         text = text.replace(i, j) | ||||
|  | ||||
|     with tempfile.NamedTemporaryFile() as fp: | ||||
|         with open(fp.name, "w") as f: | ||||
|             f.write(text) | ||||
|             f.write("\n") | ||||
|  | ||||
|         return FileResponse( | ||||
|             open(fp.name, "rb"), as_attachment=True, filename="linux_agent_install.sh" | ||||
|         ) | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										33
									
								
								api/tacticalrmm/alerts/migrations/0007_auto_20210721_0423.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								api/tacticalrmm/alerts/migrations/0007_auto_20210721_0423.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | ||||
| # Generated by Django 3.2.1 on 2021-07-21 04:23 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('alerts', '0006_auto_20210217_1736'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='alerttemplate', | ||||
|             name='created_by', | ||||
|             field=models.CharField(blank=True, max_length=100, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='alerttemplate', | ||||
|             name='created_time', | ||||
|             field=models.DateTimeField(auto_now_add=True, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='alerttemplate', | ||||
|             name='modified_by', | ||||
|             field=models.CharField(blank=True, max_length=100, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='alerttemplate', | ||||
|             name='modified_time', | ||||
|             field=models.DateTimeField(auto_now=True, null=True), | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										28
									
								
								api/tacticalrmm/alerts/migrations/0008_auto_20210721_1757.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								api/tacticalrmm/alerts/migrations/0008_auto_20210721_1757.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | ||||
| # Generated by Django 3.2.1 on 2021-07-21 17:57 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('alerts', '0007_auto_20210721_0423'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='alerttemplate', | ||||
|             name='agent_script_actions', | ||||
|             field=models.BooleanField(blank=True, default=None, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='alerttemplate', | ||||
|             name='check_script_actions', | ||||
|             field=models.BooleanField(blank=True, default=None, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='alerttemplate', | ||||
|             name='task_script_actions', | ||||
|             field=models.BooleanField(blank=True, default=None, null=True), | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										28
									
								
								api/tacticalrmm/alerts/migrations/0009_auto_20210721_1810.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								api/tacticalrmm/alerts/migrations/0009_auto_20210721_1810.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | ||||
| # Generated by Django 3.2.1 on 2021-07-21 18:10 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('alerts', '0008_auto_20210721_1757'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name='alerttemplate', | ||||
|             name='agent_script_actions', | ||||
|             field=models.BooleanField(blank=True, default=True, null=True), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name='alerttemplate', | ||||
|             name='check_script_actions', | ||||
|             field=models.BooleanField(blank=True, default=True, null=True), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name='alerttemplate', | ||||
|             name='task_script_actions', | ||||
|             field=models.BooleanField(blank=True, default=True, null=True), | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										23
									
								
								api/tacticalrmm/alerts/migrations/0010_auto_20210917_1954.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								api/tacticalrmm/alerts/migrations/0010_auto_20210917_1954.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| # Generated by Django 3.2.6 on 2021-09-17 19:54 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("alerts", "0009_auto_20210721_1810"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name="alerttemplate", | ||||
|             name="created_by", | ||||
|             field=models.CharField(blank=True, max_length=255, null=True), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name="alerttemplate", | ||||
|             name="modified_by", | ||||
|             field=models.CharField(blank=True, max_length=255, null=True), | ||||
|         ), | ||||
|     ] | ||||
| @@ -1,20 +1,21 @@ | ||||
| from __future__ import annotations | ||||
|  | ||||
| import re | ||||
| from typing import TYPE_CHECKING, Union | ||||
|  | ||||
| from django.conf import settings | ||||
| from django.contrib.postgres.fields import ArrayField | ||||
| from django.db import models | ||||
| from django.db.models.fields import BooleanField, PositiveIntegerField | ||||
| from django.utils import timezone as djangotime | ||||
| from loguru import logger | ||||
| from logs.models import BaseAuditModel, DebugLog | ||||
|  | ||||
| from tacticalrmm.models import PermissionQuerySet | ||||
|  | ||||
| if TYPE_CHECKING: | ||||
|     from agents.models import Agent | ||||
|     from autotasks.models import AutomatedTask | ||||
|     from checks.models import Check | ||||
|  | ||||
| logger.configure(**settings.LOG_CONFIG) | ||||
|  | ||||
| SEVERITY_CHOICES = [ | ||||
|     ("info", "Informational"), | ||||
| @@ -31,6 +32,8 @@ ALERT_TYPE_CHOICES = [ | ||||
|  | ||||
|  | ||||
| class Alert(models.Model): | ||||
|     objects = PermissionQuerySet.as_manager() | ||||
|  | ||||
|     agent = models.ForeignKey( | ||||
|         "agents.Agent", | ||||
|         related_name="agent", | ||||
| @@ -172,6 +175,7 @@ class Alert(models.Model): | ||||
|                 always_email = alert_template.agent_always_email | ||||
|                 always_text = alert_template.agent_always_text | ||||
|                 alert_interval = alert_template.agent_periodic_alert_days | ||||
|                 run_script_action = alert_template.agent_script_actions | ||||
|  | ||||
|             if instance.should_create_alert(alert_template): | ||||
|                 alert = cls.create_or_return_availability_alert(instance) | ||||
| @@ -208,6 +212,7 @@ class Alert(models.Model): | ||||
|                 always_email = alert_template.check_always_email | ||||
|                 always_text = alert_template.check_always_text | ||||
|                 alert_interval = alert_template.check_periodic_alert_days | ||||
|                 run_script_action = alert_template.check_script_actions | ||||
|  | ||||
|             if instance.should_create_alert(alert_template): | ||||
|                 alert = cls.create_or_return_check_alert(instance) | ||||
| @@ -241,6 +246,7 @@ class Alert(models.Model): | ||||
|                 always_email = alert_template.task_always_email | ||||
|                 always_text = alert_template.task_always_text | ||||
|                 alert_interval = alert_template.task_periodic_alert_days | ||||
|                 run_script_action = alert_template.task_script_actions | ||||
|  | ||||
|             if instance.should_create_alert(alert_template): | ||||
|                 alert = cls.create_or_return_task_alert(instance) | ||||
| @@ -294,10 +300,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 +319,10 @@ class Alert(models.Model): | ||||
|                 alert.action_run = djangotime.now() | ||||
|                 alert.save() | ||||
|             else: | ||||
|                 logger.error( | ||||
|                     f"Failure action: {alert_template.action.name} failed to run on any agent for {agent.hostname} failure alert" | ||||
|                 DebugLog.error( | ||||
|                     agent=agent, | ||||
|                     log_type="scripting", | ||||
|                     message=f"Failure action: {alert_template.action.name} failed to run on any agent for {agent.hostname}({agent.pk}) failure alert", | ||||
|                 ) | ||||
|  | ||||
|     @classmethod | ||||
| @@ -344,6 +352,7 @@ class Alert(models.Model): | ||||
|             if alert_template: | ||||
|                 email_on_resolved = alert_template.agent_email_on_resolved | ||||
|                 text_on_resolved = alert_template.agent_text_on_resolved | ||||
|                 run_script_action = alert_template.agent_script_actions | ||||
|  | ||||
|         elif isinstance(instance, Check): | ||||
|             from checks.tasks import ( | ||||
| @@ -362,6 +371,7 @@ class Alert(models.Model): | ||||
|             if alert_template: | ||||
|                 email_on_resolved = alert_template.check_email_on_resolved | ||||
|                 text_on_resolved = alert_template.check_text_on_resolved | ||||
|                 run_script_action = alert_template.check_script_actions | ||||
|  | ||||
|         elif isinstance(instance, AutomatedTask): | ||||
|             from autotasks.tasks import ( | ||||
| @@ -380,6 +390,7 @@ class Alert(models.Model): | ||||
|             if alert_template: | ||||
|                 email_on_resolved = alert_template.task_email_on_resolved | ||||
|                 text_on_resolved = alert_template.task_text_on_resolved | ||||
|                 run_script_action = alert_template.task_script_actions | ||||
|  | ||||
|         else: | ||||
|             return | ||||
| @@ -402,11 +413,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 +436,45 @@ 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) | ||||
|  | ||||
|                 # check if attr exists and isn't a function | ||||
|                 if hasattr(self, name) and not callable(getattr(self, name)): | ||||
|                     value = f"'{getattr(self, name)}'" | ||||
|                 else: | ||||
|                     continue | ||||
|  | ||||
|                 try: | ||||
|                     temp_args.append(re.sub("\\{\\{.*\\}\\}", value, arg))  # type: ignore | ||||
|                 except Exception as e: | ||||
|                     DebugLog.error(log_type="scripting", message=str(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 +531,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 +555,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 +579,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 +598,24 @@ class AlertTemplate(models.Model): | ||||
|     def __str__(self): | ||||
|         return self.name | ||||
|  | ||||
|     def is_agent_excluded(self, agent): | ||||
|         return ( | ||||
|             agent in self.excluded_agents.all() | ||||
|             or agent.site in self.excluded_sites.all() | ||||
|             or agent.client in self.excluded_clients.all() | ||||
|             or agent.monitoring_type == "workstation" | ||||
|             and self.exclude_workstations | ||||
|             or agent.monitoring_type == "server" | ||||
|             and self.exclude_servers | ||||
|         ) | ||||
|  | ||||
|     @staticmethod | ||||
|     def serialize(alert_template): | ||||
|         # serializes the agent and returns json | ||||
|         from .serializers import AlertTemplateAuditSerializer | ||||
|  | ||||
|         return AlertTemplateAuditSerializer(alert_template).data | ||||
|  | ||||
|     @property | ||||
|     def has_agent_settings(self) -> bool: | ||||
|         return ( | ||||
|   | ||||
							
								
								
									
										55
									
								
								api/tacticalrmm/alerts/permissions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								api/tacticalrmm/alerts/permissions.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,55 @@ | ||||
| from django.shortcuts import get_object_or_404 | ||||
| from rest_framework import permissions | ||||
|  | ||||
| from tacticalrmm.permissions import _has_perm, _has_perm_on_agent | ||||
|  | ||||
|  | ||||
| def _has_perm_on_alert(user, id: int): | ||||
|     from alerts.models import Alert | ||||
|  | ||||
|     role = user.role | ||||
|     if user.is_superuser or (role and getattr(role, "is_superuser")): | ||||
|         return True | ||||
|  | ||||
|     # make sure non-superusers with empty roles aren't permitted | ||||
|     elif not role: | ||||
|         return False | ||||
|  | ||||
|     alert = get_object_or_404(Alert, id=id) | ||||
|  | ||||
|     if alert.agent: | ||||
|         agent_id = alert.agent.agent_id | ||||
|     elif alert.assigned_check: | ||||
|         agent_id = alert.assigned_check.agent.agent_id | ||||
|     elif alert.assigned_task: | ||||
|         agent_id = alert.assigned_task.agent.agent_id | ||||
|     else: | ||||
|         return True | ||||
|  | ||||
|     return _has_perm_on_agent(user, agent_id) | ||||
|  | ||||
|  | ||||
| class AlertPerms(permissions.BasePermission): | ||||
|     def has_permission(self, r, view): | ||||
|         if r.method == "GET" or r.method == "PATCH": | ||||
|             if "pk" in view.kwargs.keys(): | ||||
|                 return _has_perm(r, "can_list_alerts") and _has_perm_on_alert( | ||||
|                     r.user, view.kwargs["pk"] | ||||
|                 ) | ||||
|             else: | ||||
|                 return _has_perm(r, "can_list_alerts") | ||||
|         else: | ||||
|             if "pk" in view.kwargs.keys(): | ||||
|                 return _has_perm(r, "can_manage_alerts") and _has_perm_on_alert( | ||||
|                     r.user, view.kwargs["pk"] | ||||
|                 ) | ||||
|             else: | ||||
|                 return _has_perm(r, "can_manage_alerts") | ||||
|  | ||||
|  | ||||
| class AlertTemplatePerms(permissions.BasePermission): | ||||
|     def has_permission(self, r, view): | ||||
|         if r.method == "GET": | ||||
|             return _has_perm(r, "can_list_alerttemplates") | ||||
|         else: | ||||
|             return _has_perm(r, "can_manage_alerttemplates") | ||||
| @@ -1,8 +1,8 @@ | ||||
| from automation.serializers import PolicySerializer | ||||
| from clients.serializers import ClientMinimumSerializer, SiteMinimumSerializer | ||||
| from rest_framework.fields import SerializerMethodField | ||||
| from rest_framework.serializers import ModelSerializer, ReadOnlyField | ||||
|  | ||||
| from automation.serializers import PolicySerializer | ||||
| from clients.serializers import ClientSerializer, SiteSerializer | ||||
| from tacticalrmm.utils import get_default_timezone | ||||
|  | ||||
| from .models import Alert, AlertTemplate | ||||
| @@ -10,12 +10,29 @@ from .models import Alert, AlertTemplate | ||||
|  | ||||
| class AlertSerializer(ModelSerializer): | ||||
|  | ||||
|     hostname = SerializerMethodField(read_only=True) | ||||
|     client = SerializerMethodField(read_only=True) | ||||
|     site = SerializerMethodField(read_only=True) | ||||
|     alert_time = SerializerMethodField(read_only=True) | ||||
|     resolve_on = SerializerMethodField(read_only=True) | ||||
|     snoozed_until = SerializerMethodField(read_only=True) | ||||
|     hostname = SerializerMethodField() | ||||
|     agent_id = SerializerMethodField() | ||||
|     client = SerializerMethodField() | ||||
|     site = SerializerMethodField() | ||||
|     alert_time = SerializerMethodField() | ||||
|     resolve_on = SerializerMethodField() | ||||
|     snoozed_until = SerializerMethodField() | ||||
|  | ||||
|     def get_agent_id(self, instance): | ||||
|         if instance.alert_type == "availability": | ||||
|             return instance.agent.agent_id if instance.agent else "" | ||||
|         elif instance.alert_type == "check": | ||||
|             return ( | ||||
|                 instance.assigned_check.agent.agent_id | ||||
|                 if instance.assigned_check | ||||
|                 else "" | ||||
|             ) | ||||
|         elif instance.alert_type == "task": | ||||
|             return ( | ||||
|                 instance.assigned_task.agent.agent_id if instance.assigned_task else "" | ||||
|             ) | ||||
|         else: | ||||
|             return "" | ||||
|  | ||||
|     def get_hostname(self, instance): | ||||
|         if instance.alert_type == "availability": | ||||
| @@ -113,9 +130,15 @@ class AlertTemplateSerializer(ModelSerializer): | ||||
|  | ||||
| class AlertTemplateRelationSerializer(ModelSerializer): | ||||
|     policies = PolicySerializer(read_only=True, many=True) | ||||
|     clients = ClientSerializer(read_only=True, many=True) | ||||
|     sites = SiteSerializer(read_only=True, many=True) | ||||
|     clients = ClientMinimumSerializer(read_only=True, many=True) | ||||
|     sites = SiteMinimumSerializer(read_only=True, many=True) | ||||
|  | ||||
|     class Meta: | ||||
|         model = AlertTemplate | ||||
|         fields = "__all__" | ||||
|  | ||||
|  | ||||
| class AlertTemplateAuditSerializer(ModelSerializer): | ||||
|     class Meta: | ||||
|         model = AlertTemplate | ||||
|         fields = "__all__" | ||||
|   | ||||
| @@ -1,11 +1,11 @@ | ||||
| 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 +22,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,13 +1,14 @@ | ||||
| from datetime import datetime, timedelta | ||||
| from itertools import cycle | ||||
| from unittest.mock import patch | ||||
|  | ||||
| from alerts.tasks import cache_agents_alert_template | ||||
| from core.models import CoreSettings | ||||
| from core.tasks import cache_db_fields_task | ||||
| from django.conf import settings | ||||
| from django.utils import timezone as djangotime | ||||
| from model_bakery import baker, seq | ||||
|  | ||||
| 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 | ||||
| @@ -17,6 +18,8 @@ from .serializers import ( | ||||
|     AlertTemplateSerializer, | ||||
| ) | ||||
|  | ||||
| base_url = "/alerts" | ||||
|  | ||||
|  | ||||
| class TestAlertsViews(TacticalTestCase): | ||||
|     def setUp(self): | ||||
| @@ -24,7 +27,7 @@ class TestAlertsViews(TacticalTestCase): | ||||
|         self.setup_coresettings() | ||||
|  | ||||
|     def test_get_alerts(self): | ||||
|         url = "/alerts/alerts/" | ||||
|         url = "/alerts/" | ||||
|  | ||||
|         # create check, task, and agent to test each serializer function | ||||
|         check = baker.make_recipe("checks.diskspace_check") | ||||
| @@ -117,7 +120,7 @@ class TestAlertsViews(TacticalTestCase): | ||||
|         self.check_not_authenticated("patch", url) | ||||
|  | ||||
|     def test_add_alert(self): | ||||
|         url = "/alerts/alerts/" | ||||
|         url = "/alerts/" | ||||
|  | ||||
|         agent = baker.make_recipe("agents.agent") | ||||
|         data = { | ||||
| @@ -134,11 +137,11 @@ class TestAlertsViews(TacticalTestCase): | ||||
|  | ||||
|     def test_get_alert(self): | ||||
|         # returns 404 for invalid alert pk | ||||
|         resp = self.client.get("/alerts/alerts/500/", format="json") | ||||
|         resp = self.client.get("/alerts/500/", format="json") | ||||
|         self.assertEqual(resp.status_code, 404) | ||||
|  | ||||
|         alert = baker.make("alerts.Alert") | ||||
|         url = f"/alerts/alerts/{alert.pk}/"  # type: ignore | ||||
|         url = f"/alerts/{alert.pk}/"  # type: ignore | ||||
|  | ||||
|         resp = self.client.get(url, format="json") | ||||
|         serializer = AlertSerializer(alert) | ||||
| @@ -150,16 +153,15 @@ class TestAlertsViews(TacticalTestCase): | ||||
|  | ||||
|     def test_update_alert(self): | ||||
|         # returns 404 for invalid alert pk | ||||
|         resp = self.client.put("/alerts/alerts/500/", format="json") | ||||
|         resp = self.client.put("/alerts/500/", format="json") | ||||
|         self.assertEqual(resp.status_code, 404) | ||||
|  | ||||
|         alert = baker.make("alerts.Alert", resolved=False, snoozed=False) | ||||
|  | ||||
|         url = f"/alerts/alerts/{alert.pk}/"  # type: ignore | ||||
|         url = f"/alerts/{alert.pk}/"  # type: ignore | ||||
|  | ||||
|         # test resolving alert | ||||
|         data = { | ||||
|             "id": alert.pk,  # type: ignore | ||||
|             "type": "resolve", | ||||
|         } | ||||
|         resp = self.client.put(url, data, format="json") | ||||
| @@ -168,26 +170,26 @@ class TestAlertsViews(TacticalTestCase): | ||||
|         self.assertTrue(Alert.objects.get(pk=alert.pk).resolved_on)  # type: ignore | ||||
|  | ||||
|         # test snoozing alert | ||||
|         data = {"id": alert.pk, "type": "snooze", "snooze_days": "30"}  # type: ignore | ||||
|         data = {"type": "snooze", "snooze_days": "30"}  # type: ignore | ||||
|         resp = self.client.put(url, data, format="json") | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|         self.assertTrue(Alert.objects.get(pk=alert.pk).snoozed)  # type: ignore | ||||
|         self.assertTrue(Alert.objects.get(pk=alert.pk).snooze_until)  # type: ignore | ||||
|  | ||||
|         # test snoozing alert without snooze_days | ||||
|         data = {"id": alert.pk, "type": "snooze"}  # type: ignore | ||||
|         data = {"type": "snooze"}  # type: ignore | ||||
|         resp = self.client.put(url, data, format="json") | ||||
|         self.assertEqual(resp.status_code, 400) | ||||
|  | ||||
|         # test unsnoozing alert | ||||
|         data = {"id": alert.pk, "type": "unsnooze"}  # type: ignore | ||||
|         data = {"type": "unsnooze"}  # type: ignore | ||||
|         resp = self.client.put(url, data, format="json") | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|         self.assertFalse(Alert.objects.get(pk=alert.pk).snoozed)  # type: ignore | ||||
|         self.assertFalse(Alert.objects.get(pk=alert.pk).snooze_until)  # type: ignore | ||||
|  | ||||
|         # test invalid type | ||||
|         data = {"id": alert.pk, "type": "invalid"}  # type: ignore | ||||
|         data = {"type": "invalid"}  # type: ignore | ||||
|         resp = self.client.put(url, data, format="json") | ||||
|         self.assertEqual(resp.status_code, 400) | ||||
|  | ||||
| @@ -195,13 +197,13 @@ class TestAlertsViews(TacticalTestCase): | ||||
|  | ||||
|     def test_delete_alert(self): | ||||
|         # returns 404 for invalid alert pk | ||||
|         resp = self.client.put("/alerts/alerts/500/", format="json") | ||||
|         resp = self.client.put("/alerts/500/", format="json") | ||||
|         self.assertEqual(resp.status_code, 404) | ||||
|  | ||||
|         alert = baker.make("alerts.Alert") | ||||
|  | ||||
|         # test delete alert | ||||
|         url = f"/alerts/alerts/{alert.pk}/"  # type: ignore | ||||
|         url = f"/alerts/{alert.pk}/"  # type: ignore | ||||
|         resp = self.client.delete(url, format="json") | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|  | ||||
| @@ -243,7 +245,7 @@ class TestAlertsViews(TacticalTestCase): | ||||
|         self.assertTrue(Alert.objects.filter(snoozed=False).exists()) | ||||
|  | ||||
|     def test_get_alert_templates(self): | ||||
|         url = "/alerts/alerttemplates/" | ||||
|         url = "/alerts/templates/" | ||||
|  | ||||
|         alert_templates = baker.make("alerts.AlertTemplate", _quantity=3) | ||||
|         resp = self.client.get(url, format="json") | ||||
| @@ -255,7 +257,7 @@ class TestAlertsViews(TacticalTestCase): | ||||
|         self.check_not_authenticated("get", url) | ||||
|  | ||||
|     def test_add_alert_template(self): | ||||
|         url = "/alerts/alerttemplates/" | ||||
|         url = "/alerts/templates/" | ||||
|  | ||||
|         data = { | ||||
|             "name": "Test Template", | ||||
| @@ -268,11 +270,11 @@ class TestAlertsViews(TacticalTestCase): | ||||
|  | ||||
|     def test_get_alert_template(self): | ||||
|         # returns 404 for invalid alert template pk | ||||
|         resp = self.client.get("/alerts/alerttemplates/500/", format="json") | ||||
|         resp = self.client.get("/alerts/templates/500/", format="json") | ||||
|         self.assertEqual(resp.status_code, 404) | ||||
|  | ||||
|         alert_template = baker.make("alerts.AlertTemplate") | ||||
|         url = f"/alerts/alerttemplates/{alert_template.pk}/"  # type: ignore | ||||
|         url = f"/alerts/templates/{alert_template.pk}/"  # type: ignore | ||||
|  | ||||
|         resp = self.client.get(url, format="json") | ||||
|         serializer = AlertTemplateSerializer(alert_template) | ||||
| @@ -284,16 +286,15 @@ class TestAlertsViews(TacticalTestCase): | ||||
|  | ||||
|     def test_update_alert_template(self): | ||||
|         # returns 404 for invalid alert pk | ||||
|         resp = self.client.put("/alerts/alerttemplates/500/", format="json") | ||||
|         resp = self.client.put("/alerts/templates/500/", format="json") | ||||
|         self.assertEqual(resp.status_code, 404) | ||||
|  | ||||
|         alert_template = baker.make("alerts.AlertTemplate") | ||||
|  | ||||
|         url = f"/alerts/alerttemplates/{alert_template.pk}/"  # type: ignore | ||||
|         url = f"/alerts/templates/{alert_template.pk}/"  # type: ignore | ||||
|  | ||||
|         # test data | ||||
|         data = { | ||||
|             "id": alert_template.pk,  # type: ignore | ||||
|             "agent_email_on_resolved": True, | ||||
|             "agent_text_on_resolved": True, | ||||
|             "agent_include_desktops": True, | ||||
| @@ -309,13 +310,13 @@ class TestAlertsViews(TacticalTestCase): | ||||
|  | ||||
|     def test_delete_alert_template(self): | ||||
|         # returns 404 for invalid alert pk | ||||
|         resp = self.client.put("/alerts/alerttemplates/500/", format="json") | ||||
|         resp = self.client.put("/alerts/templates/500/", format="json") | ||||
|         self.assertEqual(resp.status_code, 404) | ||||
|  | ||||
|         alert_template = baker.make("alerts.AlertTemplate") | ||||
|  | ||||
|         # test delete alert | ||||
|         url = f"/alerts/alerttemplates/{alert_template.pk}/"  # type: ignore | ||||
|         url = f"/alerts/templates/{alert_template.pk}/"  # type: ignore | ||||
|         resp = self.client.delete(url, format="json") | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|  | ||||
| @@ -330,10 +331,10 @@ class TestAlertsViews(TacticalTestCase): | ||||
|         baker.make("clients.Site", alert_template=alert_template, _quantity=3) | ||||
|         baker.make("automation.Policy", alert_template=alert_template) | ||||
|         core = CoreSettings.objects.first() | ||||
|         core.alert_template = alert_template | ||||
|         core.save() | ||||
|         core.alert_template = alert_template  # type: ignore | ||||
|         core.save()  # type: ignore | ||||
|  | ||||
|         url = f"/alerts/alerttemplates/{alert_template.pk}/related/"  # type: ignore | ||||
|         url = f"/alerts/templates/{alert_template.pk}/related/"  # type: ignore | ||||
|  | ||||
|         resp = self.client.get(url, format="json") | ||||
|         serializer = AlertTemplateRelationSerializer(alert_template) | ||||
| @@ -403,16 +404,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 | ||||
| @@ -675,25 +676,14 @@ class TestAlertTasks(TacticalTestCase): | ||||
|         url = "/api/v3/checkin/" | ||||
|  | ||||
|         agent_template_text.version = settings.LATEST_AGENT_VER | ||||
|         agent_template_text.last_seen = djangotime.now() | ||||
|         agent_template_text.save() | ||||
|  | ||||
|         agent_template_email.version = settings.LATEST_AGENT_VER | ||||
|         agent_template_email.last_seen = djangotime.now() | ||||
|         agent_template_email.save() | ||||
|  | ||||
|         data = { | ||||
|             "agent_id": agent_template_text.agent_id, | ||||
|             "version": settings.LATEST_AGENT_VER, | ||||
|         } | ||||
|  | ||||
|         resp = self.client.patch(url, data, format="json") | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|  | ||||
|         data = { | ||||
|             "agent_id": agent_template_email.agent_id, | ||||
|             "version": settings.LATEST_AGENT_VER, | ||||
|         } | ||||
|  | ||||
|         resp = self.client.patch(url, data, format="json") | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|         cache_db_fields_task() | ||||
|  | ||||
|         recovery_sms.assert_called_with( | ||||
|             pk=Alert.objects.get(agent=agent_template_text).pk | ||||
| @@ -1272,17 +1262,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 +1305,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 +1319,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 +1339,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 | ||||
| @@ -1361,15 +1352,7 @@ class TestAlertTasks(TacticalTestCase): | ||||
|         agent.last_seen = djangotime.now() | ||||
|         agent.save() | ||||
|  | ||||
|         url = "/api/v3/checkin/" | ||||
|  | ||||
|         data = { | ||||
|             "agent_id": agent.agent_id, | ||||
|             "version": settings.LATEST_AGENT_VER, | ||||
|         } | ||||
|  | ||||
|         resp = self.client.patch(url, data, format="json") | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|         cache_db_fields_task() | ||||
|  | ||||
|         # this is what data should be | ||||
|         data = { | ||||
| @@ -1387,3 +1370,199 @@ class TestAlertTasks(TacticalTestCase): | ||||
|         self.assertEqual(alert.resolved_action_execution_time, "5.0000") | ||||
|         self.assertEqual(alert.resolved_action_stdout, "success!") | ||||
|         self.assertEqual(alert.resolved_action_stderr, "") | ||||
|  | ||||
|     def test_parse_script_args(self): | ||||
|         alert = baker.make("alerts.Alert") | ||||
|  | ||||
|         args = ["-Parameter", "-Another {{alert.id}}"] | ||||
|  | ||||
|         # test default value | ||||
|         self.assertEqual( | ||||
|             ["-Parameter", f"-Another '{alert.id}'"],  # type: ignore | ||||
|             alert.parse_script_args(args=args),  # type: ignore | ||||
|         ) | ||||
|  | ||||
|     def test_prune_resolved_alerts(self): | ||||
|         from .tasks import prune_resolved_alerts | ||||
|  | ||||
|         # setup data | ||||
|         resolved_alerts = baker.make( | ||||
|             "alerts.Alert", | ||||
|             resolved=True, | ||||
|             _quantity=25, | ||||
|         ) | ||||
|  | ||||
|         alerts = baker.make( | ||||
|             "alerts.Alert", | ||||
|             resolved=False, | ||||
|             _quantity=25, | ||||
|         ) | ||||
|  | ||||
|         days = 0 | ||||
|         for alert in resolved_alerts:  # type: ignore | ||||
|             alert.alert_time = djangotime.now() - djangotime.timedelta(days=days) | ||||
|             alert.save() | ||||
|             days = days + 5 | ||||
|  | ||||
|         days = 0 | ||||
|         for alert in alerts:  # type: ignore | ||||
|             alert.alert_time = djangotime.now() - djangotime.timedelta(days=days) | ||||
|             alert.save() | ||||
|             days = days + 5 | ||||
|  | ||||
|         # delete AgentHistory older than 30 days | ||||
|         prune_resolved_alerts(30) | ||||
|  | ||||
|         self.assertEqual(Alert.objects.count(), 31) | ||||
|  | ||||
|  | ||||
| class TestAlertPermissions(TacticalTestCase): | ||||
|     def setUp(self): | ||||
|         self.setup_coresettings() | ||||
|         self.client_setup() | ||||
|  | ||||
|     def test_get_alerts_permissions(self): | ||||
|         agent = baker.make_recipe("agents.agent") | ||||
|         agent1 = baker.make_recipe("agents.agent") | ||||
|         agent2 = baker.make_recipe("agents.agent") | ||||
|         agents = [agent, agent1, agent2] | ||||
|         checks = baker.make("checks.Check", agent=cycle(agents), _quantity=3) | ||||
|         tasks = baker.make("autotasks.AutomatedTask", agent=cycle(agents), _quantity=3) | ||||
|         baker.make( | ||||
|             "alerts.Alert", alert_type="task", assigned_task=cycle(tasks), _quantity=3 | ||||
|         ) | ||||
|         baker.make( | ||||
|             "alerts.Alert", | ||||
|             alert_type="check", | ||||
|             assigned_check=cycle(checks), | ||||
|             _quantity=3, | ||||
|         ) | ||||
|         baker.make( | ||||
|             "alerts.Alert", alert_type="availability", agent=cycle(agents), _quantity=3 | ||||
|         ) | ||||
|         baker.make("alerts.Alert", alert_type="custom", _quantity=4) | ||||
|  | ||||
|         # test super user access | ||||
|         r = self.check_authorized_superuser("patch", f"{base_url}/") | ||||
|         self.assertEqual(len(r.data), 13)  # type: ignore | ||||
|  | ||||
|         user = self.create_user_with_roles([]) | ||||
|         self.client.force_authenticate(user=user)  # type: ignore | ||||
|  | ||||
|         self.check_not_authorized("patch", f"{base_url}/") | ||||
|  | ||||
|         # add list software role to user | ||||
|         user.role.can_list_alerts = True | ||||
|         user.role.save() | ||||
|  | ||||
|         r = self.check_authorized("patch", f"{base_url}/") | ||||
|         self.assertEqual(len(r.data), 13)  # type: ignore | ||||
|  | ||||
|         # test limiting to client | ||||
|         user.role.can_view_clients.set([agent.client]) | ||||
|         r = self.check_authorized("patch", f"{base_url}/") | ||||
|         self.assertEqual(len(r.data), 7)  # type: ignore | ||||
|  | ||||
|         # test limiting to site | ||||
|         user.role.can_view_clients.clear() | ||||
|         user.role.can_view_sites.set([agent1.site]) | ||||
|         r = self.client.patch(f"{base_url}/") | ||||
|         self.assertEqual(len(r.data), 7)  # type: ignore | ||||
|  | ||||
|         # test limiting to site and client | ||||
|         user.role.can_view_clients.set([agent2.client]) | ||||
|         r = self.client.patch(f"{base_url}/") | ||||
|         self.assertEqual(len(r.data), 10)  # type: ignore | ||||
|  | ||||
|     @patch("alerts.models.Alert.delete", return_value=1) | ||||
|     def test_edit_delete_get_alert_permissions(self, delete): | ||||
|         agent = baker.make_recipe("agents.agent") | ||||
|         agent1 = baker.make_recipe("agents.agent") | ||||
|         agent2 = baker.make_recipe("agents.agent") | ||||
|         agents = [agent, agent1, agent2] | ||||
|         checks = baker.make("checks.Check", agent=cycle(agents), _quantity=3) | ||||
|         tasks = baker.make("autotasks.AutomatedTask", agent=cycle(agents), _quantity=3) | ||||
|         alert_tasks = baker.make( | ||||
|             "alerts.Alert", alert_type="task", assigned_task=cycle(tasks), _quantity=3 | ||||
|         ) | ||||
|         alert_checks = baker.make( | ||||
|             "alerts.Alert", | ||||
|             alert_type="check", | ||||
|             assigned_check=cycle(checks), | ||||
|             _quantity=3, | ||||
|         ) | ||||
|         alert_agents = baker.make( | ||||
|             "alerts.Alert", alert_type="availability", agent=cycle(agents), _quantity=3 | ||||
|         ) | ||||
|         alert_custom = baker.make("alerts.Alert", alert_type="custom", _quantity=4) | ||||
|  | ||||
|         # alert task url | ||||
|         task_url = f"{base_url}/{alert_tasks[0].id}/"  # for agent | ||||
|         unauthorized_task_url = f"{base_url}/{alert_tasks[1].id}/"  # for agent1 | ||||
|         # alert check url | ||||
|         check_url = f"{base_url}/{alert_checks[0].id}/"  # for agent | ||||
|         unauthorized_check_url = f"{base_url}/{alert_checks[1].id}/"  # for agent1 | ||||
|         # alert agent url | ||||
|         agent_url = f"{base_url}/{alert_agents[0].id}/"  # for agent | ||||
|         unauthorized_agent_url = f"{base_url}/{alert_agents[1].id}/"  # for agent1 | ||||
|         # custom alert url | ||||
|         custom_url = f"{base_url}/{alert_custom[0].id}/"  # no agent associated | ||||
|  | ||||
|         authorized_urls = [task_url, check_url, agent_url, custom_url] | ||||
|         unauthorized_urls = [ | ||||
|             unauthorized_agent_url, | ||||
|             unauthorized_check_url, | ||||
|             unauthorized_task_url, | ||||
|         ] | ||||
|  | ||||
|         for method in ["get", "put", "delete"]: | ||||
|  | ||||
|             # test superuser access | ||||
|             for url in authorized_urls: | ||||
|                 self.check_authorized_superuser(method, url) | ||||
|  | ||||
|             for url in unauthorized_urls: | ||||
|                 self.check_authorized_superuser(method, url) | ||||
|  | ||||
|             user = self.create_user_with_roles([]) | ||||
|             self.client.force_authenticate(user=user)  # type: ignore | ||||
|  | ||||
|             # test user without role | ||||
|             for url in authorized_urls: | ||||
|                 self.check_not_authorized(method, url) | ||||
|  | ||||
|             for url in unauthorized_urls: | ||||
|                 self.check_not_authorized(method, url) | ||||
|  | ||||
|             # add user to role and test | ||||
|             setattr( | ||||
|                 user.role, | ||||
|                 "can_list_alerts" if method == "get" else "can_manage_alerts", | ||||
|                 True, | ||||
|             ) | ||||
|             user.role.save() | ||||
|  | ||||
|             # test user with role | ||||
|             for url in authorized_urls: | ||||
|                 self.check_authorized(method, url) | ||||
|  | ||||
|             for url in unauthorized_urls: | ||||
|                 self.check_authorized(method, url) | ||||
|  | ||||
|             # limit user to client if agent check | ||||
|             user.role.can_view_clients.set([agent.client]) | ||||
|  | ||||
|             for url in authorized_urls: | ||||
|                 self.check_authorized(method, url) | ||||
|  | ||||
|             for url in unauthorized_urls: | ||||
|                 self.check_not_authorized(method, url) | ||||
|  | ||||
|             # limit user to client if agent check | ||||
|             user.role.can_view_sites.set([agent1.site]) | ||||
|  | ||||
|             for url in authorized_urls: | ||||
|                 self.check_authorized(method, url) | ||||
|  | ||||
|             for url in unauthorized_urls: | ||||
|                 self.check_authorized(method, url) | ||||
|   | ||||
| @@ -3,10 +3,10 @@ from django.urls import path | ||||
| from . import views | ||||
|  | ||||
| urlpatterns = [ | ||||
|     path("alerts/", views.GetAddAlerts.as_view()), | ||||
|     path("", views.GetAddAlerts.as_view()), | ||||
|     path("<int:pk>/", views.GetUpdateDeleteAlert.as_view()), | ||||
|     path("bulk/", views.BulkAlerts.as_view()), | ||||
|     path("alerts/<int:pk>/", views.GetUpdateDeleteAlert.as_view()), | ||||
|     path("alerttemplates/", views.GetAddAlertTemplates.as_view()), | ||||
|     path("alerttemplates/<int:pk>/", views.GetUpdateDeleteAlertTemplate.as_view()), | ||||
|     path("alerttemplates/<int:pk>/related/", views.RelatedAlertTemplate.as_view()), | ||||
|     path("templates/", views.GetAddAlertTemplates.as_view()), | ||||
|     path("templates/<int:pk>/", views.GetUpdateDeleteAlertTemplate.as_view()), | ||||
|     path("templates/<int:pk>/related/", views.RelatedAlertTemplate.as_view()), | ||||
| ] | ||||
|   | ||||
| @@ -3,12 +3,14 @@ from datetime import datetime as dt | ||||
| from django.db.models import Q | ||||
| from django.shortcuts import get_object_or_404 | ||||
| from django.utils import timezone as djangotime | ||||
| from rest_framework.permissions import IsAuthenticated | ||||
| from rest_framework.response import Response | ||||
| from rest_framework.views import APIView | ||||
|  | ||||
| from tacticalrmm.utils import notify_error | ||||
|  | ||||
| from .models import Alert, AlertTemplate | ||||
| from .permissions import AlertPerms, AlertTemplatePerms | ||||
| from .serializers import ( | ||||
|     AlertSerializer, | ||||
|     AlertTemplateRelationSerializer, | ||||
| @@ -18,6 +20,8 @@ from .tasks import cache_agents_alert_template | ||||
|  | ||||
|  | ||||
| class GetAddAlerts(APIView): | ||||
|     permission_classes = [IsAuthenticated, AlertPerms] | ||||
|  | ||||
|     def patch(self, request): | ||||
|  | ||||
|         # top 10 alerts for dashboard icon | ||||
| @@ -88,7 +92,8 @@ class GetAddAlerts(APIView): | ||||
|                 ) | ||||
|  | ||||
|             alerts = ( | ||||
|                 Alert.objects.filter(clientFilter) | ||||
|                 Alert.objects.filter_by_role(request.user) | ||||
|                 .filter(clientFilter) | ||||
|                 .filter(severityFilter) | ||||
|                 .filter(resolvedFilter) | ||||
|                 .filter(snoozedFilter) | ||||
| @@ -97,7 +102,7 @@ class GetAddAlerts(APIView): | ||||
|             return Response(AlertSerializer(alerts, many=True).data) | ||||
|  | ||||
|         else: | ||||
|             alerts = Alert.objects.all() | ||||
|             alerts = Alert.objects.filter_by_role(request.user) | ||||
|             return Response(AlertSerializer(alerts, many=True).data) | ||||
|  | ||||
|     def post(self, request): | ||||
| @@ -109,9 +114,10 @@ class GetAddAlerts(APIView): | ||||
|  | ||||
|  | ||||
| class GetUpdateDeleteAlert(APIView): | ||||
|     permission_classes = [IsAuthenticated, AlertPerms] | ||||
|  | ||||
|     def get(self, request, pk): | ||||
|         alert = get_object_or_404(Alert, pk=pk) | ||||
|  | ||||
|         return Response(AlertSerializer(alert).data) | ||||
|  | ||||
|     def put(self, request, pk): | ||||
| @@ -163,6 +169,8 @@ class GetUpdateDeleteAlert(APIView): | ||||
|  | ||||
|  | ||||
| class BulkAlerts(APIView): | ||||
|     permission_classes = [IsAuthenticated, AlertPerms] | ||||
|  | ||||
|     def post(self, request): | ||||
|         if request.data["bulk_action"] == "resolve": | ||||
|             Alert.objects.filter(id__in=request.data["alerts"]).update( | ||||
| @@ -185,9 +193,10 @@ class BulkAlerts(APIView): | ||||
|  | ||||
|  | ||||
| class GetAddAlertTemplates(APIView): | ||||
|     permission_classes = [IsAuthenticated, AlertTemplatePerms] | ||||
|  | ||||
|     def get(self, request): | ||||
|         alert_templates = AlertTemplate.objects.all() | ||||
|  | ||||
|         return Response(AlertTemplateSerializer(alert_templates, many=True).data) | ||||
|  | ||||
|     def post(self, request): | ||||
| @@ -202,6 +211,8 @@ class GetAddAlertTemplates(APIView): | ||||
|  | ||||
|  | ||||
| class GetUpdateDeleteAlertTemplate(APIView): | ||||
|     permission_classes = [IsAuthenticated, AlertTemplatePerms] | ||||
|  | ||||
|     def get(self, request, pk): | ||||
|         alert_template = get_object_or_404(AlertTemplate, pk=pk) | ||||
|  | ||||
| @@ -231,6 +242,8 @@ class GetUpdateDeleteAlertTemplate(APIView): | ||||
|  | ||||
|  | ||||
| class RelatedAlertTemplate(APIView): | ||||
|     permission_classes = [IsAuthenticated, AlertTemplatePerms] | ||||
|  | ||||
|     def get(self, request, pk): | ||||
|         alert_template = get_object_or_404(AlertTemplate, pk=pk) | ||||
|         return Response(AlertTemplateRelationSerializer(alert_template).data) | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import json | ||||
| import os | ||||
| from unittest.mock import patch | ||||
|  | ||||
| from autotasks.models import AutomatedTask | ||||
| from django.conf import settings | ||||
| from django.utils import timezone as djangotime | ||||
| from model_bakery import baker | ||||
| @@ -129,77 +129,138 @@ class TestAPIv3(TacticalTestCase): | ||||
|         self.assertIsInstance(r.json()["check_interval"], int) | ||||
|         self.assertEqual(len(r.json()["checks"]), 15) | ||||
|  | ||||
|     def test_checkin_patch(self): | ||||
|         from logs.models import PendingAction | ||||
|     def test_task_runner_get(self): | ||||
|         from autotasks.serializers import TaskGOGetSerializer | ||||
|  | ||||
|         url = "/api/v3/checkin/" | ||||
|         agent_updated = baker.make_recipe("agents.agent", version="1.3.0") | ||||
|         PendingAction.objects.create( | ||||
|             agent=agent_updated, | ||||
|             action_type="agentupdate", | ||||
|             details={ | ||||
|                 "url": agent_updated.winagent_dl, | ||||
|                 "version": agent_updated.version, | ||||
|                 "inno": agent_updated.win_inno_exe, | ||||
|             }, | ||||
|         ) | ||||
|         action = agent_updated.pendingactions.filter(action_type="agentupdate").first() | ||||
|         self.assertEqual(action.status, "pending") | ||||
|  | ||||
|         # test agent failed to update and still on same version | ||||
|         payload = { | ||||
|             "func": "hello", | ||||
|             "agent_id": agent_updated.agent_id, | ||||
|             "version": "1.3.0", | ||||
|         } | ||||
|         r = self.client.patch(url, payload, format="json") | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|         action = agent_updated.pendingactions.filter(action_type="agentupdate").first() | ||||
|         self.assertEqual(action.status, "pending") | ||||
|  | ||||
|         # test agent successful update | ||||
|         payload["version"] = settings.LATEST_AGENT_VER | ||||
|         r = self.client.patch(url, payload, format="json") | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|         action = agent_updated.pendingactions.filter(action_type="agentupdate").first() | ||||
|         self.assertEqual(action.status, "completed") | ||||
|         action.delete() | ||||
|  | ||||
|     @patch("apiv3.views.reload_nats") | ||||
|     def test_agent_recovery(self, reload_nats): | ||||
|         reload_nats.return_value = "ok" | ||||
|         r = self.client.get("/api/v3/34jahsdkjasncASDjhg2b3j4r/recover/") | ||||
|         r = self.client.get("/api/v3/500/asdf9df9dfdf/taskrunner/") | ||||
|         self.assertEqual(r.status_code, 404) | ||||
|  | ||||
|         agent = baker.make_recipe("agents.online_agent") | ||||
|         url = f"/api/v3/{agent.agent_id}/recovery/" | ||||
|         # 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(r.json(), {"mode": "pass", "shellcmd": ""}) | ||||
|         reload_nats.assert_not_called() | ||||
|         self.assertEqual(TaskGOGetSerializer(task).data, r.data)  # type: ignore | ||||
|  | ||||
|         baker.make("agents.RecoveryAction", agent=agent, mode="mesh") | ||||
|         r = self.client.get(url) | ||||
|     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.assertEqual(r.json(), {"mode": "mesh", "shellcmd": ""}) | ||||
|         reload_nats.assert_not_called() | ||||
|         self.assertTrue(AutomatedTask.objects.get(pk=task.pk).status == "passing")  # type: ignore | ||||
|  | ||||
|         baker.make( | ||||
|             "agents.RecoveryAction", | ||||
|             agent=agent, | ||||
|             mode="command", | ||||
|             command="shutdown /r /t 5 /f", | ||||
|         # 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" | ||||
|         ) | ||||
|         r = self.client.get(url) | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|         self.assertEqual( | ||||
|             r.json(), {"mode": "command", "shellcmd": "shutdown /r /t 5 /f"} | ||||
|         multiple = baker.make( | ||||
|             "core.CustomField", model="agent", type="multiple", name="Test2" | ||||
|         ) | ||||
|         reload_nats.assert_not_called() | ||||
|  | ||||
|         baker.make("agents.RecoveryAction", agent=agent, mode="rpc") | ||||
|         r = self.client.get(url) | ||||
|         # 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.assertEqual(r.json(), {"mode": "rpc", "shellcmd": ""}) | ||||
|         reload_nats.assert_called_once() | ||||
|         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 | ||||
|   | ||||
| @@ -19,5 +19,5 @@ urlpatterns = [ | ||||
|     path("winupdates/", views.WinUpdates.as_view()), | ||||
|     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()), | ||||
| ] | ||||
|   | ||||
| @@ -1,33 +1,30 @@ | ||||
| import asyncio | ||||
| import os | ||||
| import time | ||||
|  | ||||
| from accounts.models import User | ||||
| from agents.models import Agent, AgentHistory | ||||
| from agents.serializers import AgentHistorySerializer | ||||
| from autotasks.models import AutomatedTask | ||||
| from autotasks.serializers import TaskGOGetSerializer, TaskRunnerPatchSerializer | ||||
| from checks.models import Check | ||||
| from checks.serializers import CheckRunnerGetSerializer | ||||
| from core.models import CoreSettings | ||||
| from core.utils import download_mesh_agent, get_mesh_device_id, get_mesh_ws_url | ||||
| 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 logs.models import DebugLog, PendingAction | ||||
| from packaging import version as pyver | ||||
| from rest_framework.authentication import TokenAuthentication | ||||
| from rest_framework.authtoken.models import Token | ||||
| from rest_framework.permissions import IsAuthenticated | ||||
| 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 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 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) | ||||
| from tacticalrmm.constants import MeshAgentIdent | ||||
| from tacticalrmm.utils import notify_error, reload_nats | ||||
|  | ||||
|  | ||||
| class CheckIn(APIView): | ||||
| @@ -35,89 +32,6 @@ class CheckIn(APIView): | ||||
|     authentication_classes = [TokenAuthentication] | ||||
|     permission_classes = [IsAuthenticated] | ||||
|  | ||||
|     def patch(self, request): | ||||
|         from alerts.models import Alert | ||||
|  | ||||
|         updated = False | ||||
|         agent = get_object_or_404(Agent, agent_id=request.data["agent_id"]) | ||||
|         if pyver.parse(request.data["version"]) > pyver.parse( | ||||
|             agent.version | ||||
|         ) or pyver.parse(request.data["version"]) == pyver.parse( | ||||
|             settings.LATEST_AGENT_VER | ||||
|         ): | ||||
|             updated = True | ||||
|         agent.version = request.data["version"] | ||||
|         agent.last_seen = djangotime.now() | ||||
|         agent.save(update_fields=["version", "last_seen"]) | ||||
|  | ||||
|         # change agent update pending status to completed if agent has just updated | ||||
|         if ( | ||||
|             updated | ||||
|             and agent.pendingactions.filter(  # type: ignore | ||||
|                 action_type="agentupdate", status="pending" | ||||
|             ).exists() | ||||
|         ): | ||||
|             agent.pendingactions.filter(  # type: ignore | ||||
|                 action_type="agentupdate", status="pending" | ||||
|             ).update(status="completed") | ||||
|  | ||||
|         # handles any alerting actions | ||||
|         if Alert.objects.filter(agent=agent, resolved=False).exists(): | ||||
|             Alert.handle_alert_resolve(agent) | ||||
|  | ||||
|         # get any pending actions | ||||
|         if agent.pendingactions.filter(status="pending").exists():  # type: ignore | ||||
|             agent.handle_pending_actions() | ||||
|  | ||||
|         return Response("ok") | ||||
|  | ||||
|     def put(self, request): | ||||
|         agent = get_object_or_404(Agent, agent_id=request.data["agent_id"]) | ||||
|         serializer = WinAgentSerializer(instance=agent, data=request.data, partial=True) | ||||
|  | ||||
|         if request.data["func"] == "disks": | ||||
|             disks = request.data["disks"] | ||||
|             new = [] | ||||
|             for disk in disks: | ||||
|                 tmp = {} | ||||
|                 for _, _ in disk.items(): | ||||
|                     tmp["device"] = disk["device"] | ||||
|                     tmp["fstype"] = disk["fstype"] | ||||
|                     tmp["total"] = bytes2human(disk["total"]) | ||||
|                     tmp["used"] = bytes2human(disk["used"]) | ||||
|                     tmp["free"] = bytes2human(disk["free"]) | ||||
|                     tmp["percent"] = int(disk["percent"]) | ||||
|                 new.append(tmp) | ||||
|  | ||||
|             serializer.is_valid(raise_exception=True) | ||||
|             serializer.save(disks=new) | ||||
|             return Response("ok") | ||||
|  | ||||
|         if request.data["func"] == "loggedonuser": | ||||
|             if request.data["logged_in_username"] != "None": | ||||
|                 serializer.is_valid(raise_exception=True) | ||||
|                 serializer.save(last_logged_in_user=request.data["logged_in_username"]) | ||||
|                 return Response("ok") | ||||
|  | ||||
|         if request.data["func"] == "software": | ||||
|             raw: SoftwareList = request.data["software"] | ||||
|             if not isinstance(raw, list): | ||||
|                 return notify_error("err") | ||||
|  | ||||
|             sw = filter_software(raw) | ||||
|             if not InstalledSoftware.objects.filter(agent=agent).exists(): | ||||
|                 InstalledSoftware(agent=agent, software=sw).save() | ||||
|             else: | ||||
|                 s = agent.installedsoftware_set.first()  # type: ignore | ||||
|                 s.software = sw | ||||
|                 s.save(update_fields=["software"]) | ||||
|  | ||||
|             return Response("ok") | ||||
|  | ||||
|         serializer.is_valid(raise_exception=True) | ||||
|         serializer.save() | ||||
|         return Response("ok") | ||||
|  | ||||
|     # called once during tacticalagent windows service startup | ||||
|     def post(self, request): | ||||
|         agent = get_object_or_404(Agent, agent_id=request.data["agent_id"]) | ||||
| @@ -159,22 +73,26 @@ class WinUpdates(APIView): | ||||
|  | ||||
|     def put(self, request): | ||||
|         agent = get_object_or_404(Agent, agent_id=request.data["agent_id"]) | ||||
|  | ||||
|         needs_reboot: bool = request.data["needs_reboot"] | ||||
|         agent.needs_reboot = needs_reboot | ||||
|         agent.save(update_fields=["needs_reboot"]) | ||||
|  | ||||
|         reboot_policy: str = agent.get_patch_policy().reboot_after_install | ||||
|         reboot = False | ||||
|  | ||||
|         if reboot_policy == "always": | ||||
|             reboot = True | ||||
|  | ||||
|         if request.data["needs_reboot"]: | ||||
|             if reboot_policy == "required": | ||||
|                 reboot = True | ||||
|             elif reboot_policy == "never": | ||||
|                 agent.needs_reboot = True | ||||
|                 agent.save(update_fields=["needs_reboot"]) | ||||
|         elif needs_reboot and reboot_policy == "required": | ||||
|             reboot = True | ||||
|  | ||||
|         if reboot: | ||||
|             asyncio.run(agent.nats_cmd({"func": "rebootnow"}, wait=False)) | ||||
|             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") | ||||
| @@ -236,14 +154,6 @@ class WinUpdates(APIView): | ||||
|                 ).save() | ||||
|  | ||||
|         agent.delete_superseded_updates() | ||||
|  | ||||
|         # more superseded updates cleanup | ||||
|         if pyver.parse(agent.version) <= pyver.parse("1.4.2"): | ||||
|             for u in agent.winupdates.filter(  # type: ignore | ||||
|                 date_installed__isnull=True, result="failed" | ||||
|             ).exclude(installed=True): | ||||
|                 u.delete() | ||||
|  | ||||
|         return Response("ok") | ||||
|  | ||||
|  | ||||
| @@ -296,10 +206,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 +223,14 @@ class CheckRunner(APIView): | ||||
|  | ||||
|     def patch(self, request): | ||||
|         check = get_object_or_404(Check, pk=request.data["id"]) | ||||
|  | ||||
|         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 +250,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 +264,27 @@ 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" | ||||
|         AgentHistory.objects.create( | ||||
|             agent=agent, | ||||
|             type="task_run", | ||||
|             script=task.script, | ||||
|             script_results=request.data, | ||||
|         ) | ||||
|  | ||||
|         # check if task is a collector and update the custom field | ||||
|         if task.custom_field: | ||||
|             if not task.stderr: | ||||
|  | ||||
|                 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 +294,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,28 +313,36 @@ 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" | ||||
|         mesh_exe = os.path.join(settings.EXE_DIR, exe) | ||||
|         match request.data: | ||||
|             case {"arch": "64", "plat": "windows"}: | ||||
|                 arch = MeshAgentIdent.WIN64 | ||||
|             case {"arch": "32", "plat": "windows"}: | ||||
|                 arch = MeshAgentIdent.WIN32 | ||||
|             case _: | ||||
|                 return notify_error("Arch not specified") | ||||
|  | ||||
|         if not os.path.exists(mesh_exe): | ||||
|             return notify_error("Mesh Agent executable not found") | ||||
|         core: CoreSettings = CoreSettings.objects.first()  # type: ignore | ||||
|  | ||||
|         if settings.DEBUG: | ||||
|             with open(mesh_exe, "rb") as f: | ||||
|                 response = HttpResponse( | ||||
|                     f.read(), | ||||
|                     content_type="application/vnd.microsoft.portable-executable", | ||||
|                 ) | ||||
|                 response["Content-Disposition"] = f"inline; filename={exe}" | ||||
|                 return response | ||||
|         try: | ||||
|             uri = get_mesh_ws_url() | ||||
|             mesh_id = asyncio.run(get_mesh_device_id(uri, core.mesh_device_group)) | ||||
|         except: | ||||
|             return notify_error("Unable to connect to mesh to get group id information") | ||||
|  | ||||
|         if settings.DOCKER_BUILD: | ||||
|             dl_url = f"{settings.MESH_WS_URL.replace('ws://', 'http://')}/meshagents?id={arch}&meshid={mesh_id}&installflags=0" | ||||
|         else: | ||||
|             response = HttpResponse() | ||||
|             response["Content-Disposition"] = f"attachment; filename={exe}" | ||||
|             response["X-Accel-Redirect"] = f"/private/exe/{exe}" | ||||
|             return response | ||||
|             dl_url = ( | ||||
|                 f"{core.mesh_site}/meshagents?id={arch}&meshid={mesh_id}&installflags=0" | ||||
|             ) | ||||
|  | ||||
|         try: | ||||
|             return download_mesh_agent(dl_url) | ||||
|         except: | ||||
|             return notify_error("Unable to download mesh agent exe") | ||||
|  | ||||
|  | ||||
| class NewAgent(APIView): | ||||
| @@ -435,11 +363,11 @@ class NewAgent(APIView): | ||||
|             monitoring_type=request.data["monitoring_type"], | ||||
|             description=request.data["description"], | ||||
|             mesh_node_id=request.data["mesh_node_id"], | ||||
|             goarch=request.data["goarch"], | ||||
|             plat=request.data["plat"], | ||||
|             last_seen=djangotime.now(), | ||||
|         ) | ||||
|         agent.save() | ||||
|         agent.salt_id = f"{agent.hostname}-{agent.pk}" | ||||
|         agent.save(update_fields=["salt_id"]) | ||||
|  | ||||
|         user = User.objects.create_user(  # type: ignore | ||||
|             username=request.data["agent_id"], | ||||
| @@ -464,15 +392,11 @@ 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( | ||||
|             { | ||||
|                 "pk": agent.pk, | ||||
|                 "saltid": f"{agent.hostname}-{agent.pk}", | ||||
|                 "token": token.key, | ||||
|             } | ||||
|         ) | ||||
|         ret = {"pk": agent.pk, "token": token.key} | ||||
|         return Response(ret) | ||||
|  | ||||
|  | ||||
| class Software(APIView): | ||||
| @@ -481,11 +405,7 @@ class Software(APIView): | ||||
|  | ||||
|     def post(self, request): | ||||
|         agent = get_object_or_404(Agent, agent_id=request.data["agent_id"]) | ||||
|         raw: SoftwareList = request.data["software"] | ||||
|         if not isinstance(raw, list): | ||||
|             return notify_error("err") | ||||
|  | ||||
|         sw = filter_software(raw) | ||||
|         sw = request.data["software"] | ||||
|         if not InstalledSoftware.objects.filter(agent=agent).exists(): | ||||
|             InstalledSoftware(agent=agent, software=sw).save() | ||||
|         else: | ||||
| @@ -546,25 +466,14 @@ class ChocoResult(APIView): | ||||
|         return Response("ok") | ||||
|  | ||||
|  | ||||
| class AgentRecovery(APIView): | ||||
| class AgentHistoryResult(APIView): | ||||
|     authentication_classes = [TokenAuthentication] | ||||
|     permission_classes = [IsAuthenticated] | ||||
|  | ||||
|     def get(self, request, agentid): | ||||
|         agent = get_object_or_404(Agent, agent_id=agentid) | ||||
|         recovery = agent.recoveryactions.filter(last_run=None).last()  # type: ignore | ||||
|         ret = {"mode": "pass", "shellcmd": ""} | ||||
|         if recovery is None: | ||||
|             return Response(ret) | ||||
|  | ||||
|         recovery.last_run = djangotime.now() | ||||
|         recovery.save(update_fields=["last_run"]) | ||||
|  | ||||
|         ret["mode"] = recovery.mode | ||||
|  | ||||
|         if recovery.mode == "command": | ||||
|             ret["shellcmd"] = recovery.command | ||||
|         elif recovery.mode == "rpc": | ||||
|             reload_nats() | ||||
|  | ||||
|         return Response(ret) | ||||
|     def patch(self, request, agentid, pk): | ||||
|         _ = get_object_or_404(Agent, agent_id=agentid) | ||||
|         hist = get_object_or_404(AgentHistory, pk=pk) | ||||
|         s = AgentHistorySerializer(instance=hist, data=request.data, partial=True) | ||||
|         s.is_valid(raise_exception=True) | ||||
|         s.save() | ||||
|         return Response("ok") | ||||
|   | ||||
| @@ -0,0 +1,23 @@ | ||||
| # Generated by Django 3.2.6 on 2021-09-17 19:54 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("automation", "0008_auto_20210302_0415"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name="policy", | ||||
|             name="created_by", | ||||
|             field=models.CharField(blank=True, max_length=255, null=True), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name="policy", | ||||
|             name="modified_by", | ||||
|             field=models.CharField(blank=True, max_length=255, null=True), | ||||
|         ), | ||||
|     ] | ||||
| @@ -1,7 +1,6 @@ | ||||
| from django.db import models | ||||
|  | ||||
| from agents.models import Agent | ||||
| from core.models import CoreSettings | ||||
| from django.db import models | ||||
| from logs.models import BaseAuditModel | ||||
|  | ||||
|  | ||||
| @@ -29,17 +28,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 +49,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 +64,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 +93,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,109 +125,49 @@ 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() | ||||
|         added_task_pks = list() | ||||
|  | ||||
|         agent_tasks_parent_pks = [ | ||||
|             task.parent_task for task in agent.autotasks.filter(managed_by_policy=True) | ||||
|         ] | ||||
|  | ||||
|         # Get policies applied to agent and agent site and client | ||||
|         client = agent.client | ||||
|         site = agent.site | ||||
|         policies = agent.get_agent_policies() | ||||
|  | ||||
|         default_policy = None | ||||
|         client_policy = None | ||||
|         site_policy = None | ||||
|         agent_policy = agent.policy | ||||
|         processed_policies = list() | ||||
|  | ||||
|         # Get the Client/Site policy based on if the agent is server or workstation | ||||
|         if agent.monitoring_type == "server": | ||||
|             default_policy = CoreSettings.objects.first().server_policy | ||||
|             client_policy = client.server_policy | ||||
|             site_policy = site.server_policy | ||||
|         elif agent.monitoring_type == "workstation": | ||||
|             default_policy = CoreSettings.objects.first().workstation_policy | ||||
|             client_policy = client.workstation_policy | ||||
|             site_policy = site.workstation_policy | ||||
|  | ||||
|         if ( | ||||
|             agent_policy | ||||
|             and agent_policy.active | ||||
|             and not agent_policy.is_agent_excluded(agent) | ||||
|         ): | ||||
|             for task in agent_policy.autotasks.all(): | ||||
|                 if task.pk not in added_task_pks: | ||||
|         for _, policy in policies.items(): | ||||
|             if policy and policy.active and policy.pk not in processed_policies: | ||||
|                 processed_policies.append(policy.pk) | ||||
|                 for task in policy.autotasks.all(): | ||||
|                     tasks.append(task) | ||||
|                     added_task_pks.append(task.pk) | ||||
|         if ( | ||||
|             site_policy | ||||
|             and site_policy.active | ||||
|             and not site_policy.is_agent_excluded(agent) | ||||
|         ): | ||||
|             for task in site_policy.autotasks.all(): | ||||
|                 if task.pk not in added_task_pks: | ||||
|                     tasks.append(task) | ||||
|                     added_task_pks.append(task.pk) | ||||
|         if ( | ||||
|             client_policy | ||||
|             and client_policy.active | ||||
|             and not client_policy.is_agent_excluded(agent) | ||||
|         ): | ||||
|             for task in client_policy.autotasks.all(): | ||||
|                 if task.pk not in added_task_pks: | ||||
|                     tasks.append(task) | ||||
|                     added_task_pks.append(task.pk) | ||||
|  | ||||
|         if ( | ||||
|             default_policy | ||||
|             and default_policy.active | ||||
|             and not default_policy.is_agent_excluded(agent) | ||||
|         ): | ||||
|             for task in default_policy.autotasks.all(): | ||||
|                 if task.pk not in added_task_pks: | ||||
|                     tasks.append(task) | ||||
|                     added_task_pks.append(task.pk) | ||||
|  | ||||
|         # remove policy tasks from agent not included in policy | ||||
|         for task in agent.autotasks.filter( | ||||
|             parent_task__in=[ | ||||
|                 taskpk | ||||
|                 for taskpk in agent_tasks_parent_pks | ||||
|                 if taskpk not in added_task_pks | ||||
|                 if taskpk not in [task.pk for task in tasks] | ||||
|             ] | ||||
|         ): | ||||
|             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 [task.pk for task in tasks]] | ||||
|         ).update(sync_status="notsynced") | ||||
|  | ||||
|         return [task for task in tasks if task.pk not in agent_tasks_parent_pks] | ||||
|  | ||||
| @@ -234,75 +182,24 @@ class Policy(BaseAuditModel): | ||||
|         ] | ||||
|  | ||||
|         # Get policies applied to agent and agent site and client | ||||
|         client = agent.client | ||||
|         site = agent.site | ||||
|  | ||||
|         default_policy = None | ||||
|         client_policy = None | ||||
|         site_policy = None | ||||
|         agent_policy = agent.policy | ||||
|  | ||||
|         if agent.monitoring_type == "server": | ||||
|             default_policy = CoreSettings.objects.first().server_policy | ||||
|             client_policy = client.server_policy | ||||
|             site_policy = site.server_policy | ||||
|         elif agent.monitoring_type == "workstation": | ||||
|             default_policy = CoreSettings.objects.first().workstation_policy | ||||
|             client_policy = client.workstation_policy | ||||
|             site_policy = site.workstation_policy | ||||
|         policies = agent.get_agent_policies() | ||||
|  | ||||
|         # 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() | ||||
|         policy_checks = list() | ||||
|  | ||||
|         if ( | ||||
|             agent_policy | ||||
|             and agent_policy.active | ||||
|             and not agent_policy.is_agent_excluded(agent) | ||||
|         ): | ||||
|             if agent_policy.enforced: | ||||
|                 for check in agent_policy.policychecks.all(): | ||||
|                     enforced_checks.append(check) | ||||
|             else: | ||||
|                 for check in agent_policy.policychecks.all(): | ||||
|                     policy_checks.append(check) | ||||
|         processed_policies = list() | ||||
|  | ||||
|         if ( | ||||
|             site_policy | ||||
|             and site_policy.active | ||||
|             and not site_policy.is_agent_excluded(agent) | ||||
|         ): | ||||
|             if site_policy.enforced: | ||||
|                 for check in site_policy.policychecks.all(): | ||||
|                     enforced_checks.append(check) | ||||
|             else: | ||||
|                 for check in site_policy.policychecks.all(): | ||||
|                     policy_checks.append(check) | ||||
|  | ||||
|         if ( | ||||
|             client_policy | ||||
|             and client_policy.active | ||||
|             and not client_policy.is_agent_excluded(agent) | ||||
|         ): | ||||
|             if client_policy.enforced: | ||||
|                 for check in client_policy.policychecks.all(): | ||||
|                     enforced_checks.append(check) | ||||
|             else: | ||||
|                 for check in client_policy.policychecks.all(): | ||||
|                     policy_checks.append(check) | ||||
|  | ||||
|         if ( | ||||
|             default_policy | ||||
|             and default_policy.active | ||||
|             and not default_policy.is_agent_excluded(agent) | ||||
|         ): | ||||
|             if default_policy.enforced: | ||||
|                 for check in default_policy.policychecks.all(): | ||||
|                     enforced_checks.append(check) | ||||
|             else: | ||||
|                 for check in default_policy.policychecks.all(): | ||||
|                     policy_checks.append(check) | ||||
|         for _, policy in policies.items(): | ||||
|             if policy and policy.active and policy.pk not in processed_policies: | ||||
|                 processed_policies.append(policy.pk) | ||||
|                 if policy.enforced: | ||||
|                     for check in policy.policychecks.all(): | ||||
|                         enforced_checks.append(check) | ||||
|                 else: | ||||
|                     for check in policy.policychecks.all(): | ||||
|                         policy_checks.append(check) | ||||
|  | ||||
|         # Sorted Checks already added | ||||
|         added_diskspace_checks = list() | ||||
| @@ -324,7 +221,7 @@ class Policy(BaseAuditModel): | ||||
|  | ||||
|         # Loop over checks in with enforced policies first, then non-enforced policies | ||||
|         for check in enforced_checks + agent_checks + policy_checks: | ||||
|             if check.check_type == "diskspace": | ||||
|             if check.check_type == "diskspace" and agent.plat == "windows": | ||||
|                 # Check if drive letter was already added | ||||
|                 if check.disk not in added_diskspace_checks: | ||||
|                     added_diskspace_checks.append(check.disk) | ||||
| @@ -346,7 +243,7 @@ class Policy(BaseAuditModel): | ||||
|                     check.overriden_by_policy = True | ||||
|                     check.save() | ||||
|  | ||||
|             if check.check_type == "cpuload": | ||||
|             if check.check_type == "cpuload" and agent.plat == "windows": | ||||
|                 # Check if cpuload list is empty | ||||
|                 if not added_cpuload_checks: | ||||
|                     added_cpuload_checks.append(check) | ||||
| @@ -357,7 +254,7 @@ class Policy(BaseAuditModel): | ||||
|                     check.overriden_by_policy = True | ||||
|                     check.save() | ||||
|  | ||||
|             if check.check_type == "memory": | ||||
|             if check.check_type == "memory" and agent.plat == "windows": | ||||
|                 # Check if memory check list is empty | ||||
|                 if not added_memory_checks: | ||||
|                     added_memory_checks.append(check) | ||||
| @@ -368,7 +265,7 @@ class Policy(BaseAuditModel): | ||||
|                     check.overriden_by_policy = True | ||||
|                     check.save() | ||||
|  | ||||
|             if check.check_type == "winsvc": | ||||
|             if check.check_type == "winsvc" and agent.plat == "windows": | ||||
|                 # Check if service name was already added | ||||
|                 if check.svc_name not in added_winsvc_checks: | ||||
|                     added_winsvc_checks.append(check.svc_name) | ||||
| @@ -379,7 +276,9 @@ class Policy(BaseAuditModel): | ||||
|                     check.overriden_by_policy = True | ||||
|                     check.save() | ||||
|  | ||||
|             if check.check_type == "script": | ||||
|             if check.check_type == "script" and agent.is_supported_script( | ||||
|                 check.script.supported_platforms | ||||
|             ): | ||||
|                 # Check if script id was already added | ||||
|                 if check.script.id not in added_script_checks: | ||||
|                     added_script_checks.append(check.script.id) | ||||
| @@ -390,7 +289,7 @@ class Policy(BaseAuditModel): | ||||
|                     check.overriden_by_policy = True | ||||
|                     check.save() | ||||
|  | ||||
|             if check.check_type == "eventlog": | ||||
|             if check.check_type == "eventlog" and agent.plat == "windows": | ||||
|                 # Check if events were already added | ||||
|                 if [check.log_name, check.event_id] not in added_eventlog_checks: | ||||
|                     added_eventlog_checks.append([check.log_name, check.event_id]) | ||||
| @@ -412,11 +311,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 _has_perm(r, "can_list_automation_policies") | ||||
|         else: | ||||
|             return _has_perm(r, "can_manage_automation_policies") | ||||
| @@ -1,14 +1,13 @@ | ||||
| from agents.serializers import AgentHostnameSerializer | ||||
| from autotasks.models import AutomatedTask | ||||
| from checks.models import Check | ||||
| from clients.models import Client | ||||
| from clients.serializers import ClientMinimumSerializer, SiteMinimumSerializer | ||||
| from rest_framework.serializers import ( | ||||
|     ModelSerializer, | ||||
|     ReadOnlyField, | ||||
|     SerializerMethodField, | ||||
| ) | ||||
|  | ||||
| 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 winupdate.serializers import WinUpdatePolicySerializer | ||||
|  | ||||
| from .models import Policy | ||||
| @@ -21,25 +20,70 @@ class PolicySerializer(ModelSerializer): | ||||
|  | ||||
|  | ||||
| class PolicyTableSerializer(ModelSerializer): | ||||
|  | ||||
|     default_server_policy = ReadOnlyField(source="is_default_server_policy") | ||||
|     default_workstation_policy = ReadOnlyField(source="is_default_workstation_policy") | ||||
|     agents_count = SerializerMethodField(read_only=True) | ||||
|     winupdatepolicy = WinUpdatePolicySerializer(many=True, read_only=True) | ||||
|     alert_template = ReadOnlyField(source="alert_template.id") | ||||
|     excluded_clients = ClientSerializer(many=True) | ||||
|     excluded_sites = SiteSerializer(many=True) | ||||
|     excluded_agents = AgentHostnameSerializer(many=True) | ||||
|  | ||||
|     class Meta: | ||||
|         model = Policy | ||||
|         fields = "__all__" | ||||
|         depth = 1 | ||||
|  | ||||
|     def get_agents_count(self, policy): | ||||
|         return policy.related_agents().count() | ||||
|  | ||||
|  | ||||
| class PolicyRelatedSerializer(ModelSerializer): | ||||
|     workstation_clients = SerializerMethodField() | ||||
|     server_clients = SerializerMethodField() | ||||
|     workstation_sites = SerializerMethodField() | ||||
|     server_sites = SerializerMethodField() | ||||
|     agents = SerializerMethodField() | ||||
|  | ||||
|     def get_agents(self, policy): | ||||
|         return AgentHostnameSerializer( | ||||
|             policy.agents.filter_by_role(self.context["user"]).only( | ||||
|                 "agent_id", "hostname" | ||||
|             ), | ||||
|             many=True, | ||||
|         ).data | ||||
|  | ||||
|     def get_workstation_clients(self, policy): | ||||
|         return ClientMinimumSerializer( | ||||
|             policy.workstation_clients.filter_by_role(self.context["user"]), many=True | ||||
|         ).data | ||||
|  | ||||
|     def get_server_clients(self, policy): | ||||
|         return ClientMinimumSerializer( | ||||
|             policy.server_clients.filter_by_role(self.context["user"]), many=True | ||||
|         ).data | ||||
|  | ||||
|     def get_workstation_sites(self, policy): | ||||
|         return SiteMinimumSerializer( | ||||
|             policy.workstation_sites.filter_by_role(self.context["user"]), many=True | ||||
|         ).data | ||||
|  | ||||
|     def get_server_sites(self, policy): | ||||
|         return SiteMinimumSerializer( | ||||
|             policy.server_sites.filter_by_role(self.context["user"]), many=True | ||||
|         ).data | ||||
|  | ||||
|     class Meta: | ||||
|         model = Policy | ||||
|         fields = ( | ||||
|             "pk", | ||||
|             "name", | ||||
|             "workstation_clients", | ||||
|             "workstation_sites", | ||||
|             "server_clients", | ||||
|             "server_sites", | ||||
|             "agents", | ||||
|             "is_default_server_policy", | ||||
|             "is_default_workstation_policy", | ||||
|         ) | ||||
|  | ||||
|  | ||||
| class PolicyOverviewSerializer(ModelSerializer): | ||||
|     class Meta: | ||||
|         model = Client | ||||
| @@ -48,7 +92,6 @@ class PolicyOverviewSerializer(ModelSerializer): | ||||
|  | ||||
|  | ||||
| class PolicyCheckStatusSerializer(ModelSerializer): | ||||
|  | ||||
|     hostname = ReadOnlyField(source="agent.hostname") | ||||
|  | ||||
|     class Meta: | ||||
| @@ -57,7 +100,6 @@ class PolicyCheckStatusSerializer(ModelSerializer): | ||||
|  | ||||
|  | ||||
| class PolicyTaskStatusSerializer(ModelSerializer): | ||||
|  | ||||
|     hostname = ReadOnlyField(source="agent.hostname") | ||||
|  | ||||
|     class Meta: | ||||
| @@ -65,26 +107,7 @@ class PolicyTaskStatusSerializer(ModelSerializer): | ||||
|         fields = "__all__" | ||||
|  | ||||
|  | ||||
| class PolicyCheckSerializer(ModelSerializer): | ||||
| class PolicyAuditSerializer(ModelSerializer): | ||||
|     class Meta: | ||||
|         model = Check | ||||
|         fields = ( | ||||
|             "id", | ||||
|             "check_type", | ||||
|             "readable_desc", | ||||
|             "assignedtask", | ||||
|             "text_alert", | ||||
|             "email_alert", | ||||
|             "dashboard_alert", | ||||
|         ) | ||||
|         depth = 1 | ||||
|  | ||||
|  | ||||
| class AutoTasksFieldSerializer(ModelSerializer): | ||||
|     assigned_check = PolicyCheckSerializer(read_only=True) | ||||
|     script = ReadOnlyField(source="script.id") | ||||
|  | ||||
|     class Meta: | ||||
|         model = AutomatedTask | ||||
|         model = Policy | ||||
|         fields = "__all__" | ||||
|         depth = 1 | ||||
|   | ||||
| @@ -1,169 +1,155 @@ | ||||
| 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() | ||||
|  | ||||
|         agent.set_alert_template() | ||||
|  | ||||
| @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,20 +1,17 @@ | ||||
| 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 tacticalrmm.test import TacticalTestCase | ||||
| from model_bakery import baker, seq | ||||
| from winupdate.models import WinUpdatePolicy | ||||
|  | ||||
| from tacticalrmm.test import TacticalTestCase | ||||
|  | ||||
| from .serializers import ( | ||||
|     AutoTasksFieldSerializer, | ||||
|     PolicyCheckSerializer, | ||||
|     PolicyCheckStatusSerializer, | ||||
|     PolicyOverviewSerializer, | ||||
|     PolicySerializer, | ||||
|     PolicyTableSerializer, | ||||
|     PolicyTaskStatusSerializer, | ||||
| ) | ||||
|  | ||||
| @@ -27,12 +24,10 @@ class TestPolicyViews(TacticalTestCase): | ||||
|     def test_get_all_policies(self): | ||||
|         url = "/automation/policies/" | ||||
|  | ||||
|         policies = baker.make("automation.Policy", _quantity=3) | ||||
|         baker.make("automation.Policy", _quantity=3) | ||||
|         resp = self.client.get(url, format="json") | ||||
|         serializer = PolicyTableSerializer(policies, many=True) | ||||
|  | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|         self.assertEqual(resp.data, serializer.data)  # type: ignore | ||||
|         self.assertEqual(len(resp.data), 3) | ||||
|  | ||||
|         self.check_not_authenticated("get", url) | ||||
|  | ||||
| @@ -52,7 +47,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 +69,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_recipe("autotasks.task", 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 +87,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 +119,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 +131,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,43 +172,11 @@ 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) | ||||
|  | ||||
|     def test_get_all_policy_tasks(self): | ||||
|         # create policy with tasks | ||||
|         policy = baker.make("automation.Policy") | ||||
|         tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3) | ||||
|         url = f"/automation/{policy.pk}/policyautomatedtasks/"  # type: ignore | ||||
|  | ||||
|         resp = self.client.get(url, format="json") | ||||
|         serializer = AutoTasksFieldSerializer(tasks, many=True) | ||||
|  | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|         self.assertEqual(resp.data, serializer.data)  # type: ignore | ||||
|         self.assertEqual(len(resp.data), 3)  # type: ignore | ||||
|  | ||||
|         self.check_not_authenticated("get", url) | ||||
|  | ||||
|     def test_get_all_policy_checks(self): | ||||
|  | ||||
|         # setup data | ||||
|         policy = baker.make("automation.Policy") | ||||
|         checks = self.create_checks(policy=policy) | ||||
|  | ||||
|         url = f"/automation/{policy.pk}/policychecks/"  # type: ignore | ||||
|  | ||||
|         resp = self.client.get(url, format="json") | ||||
|         serializer = PolicyCheckSerializer(checks, many=True) | ||||
|  | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|         self.assertEqual(resp.data, serializer.data)  # type: ignore | ||||
|         self.assertEqual(len(resp.data), 7)  # type: ignore | ||||
|  | ||||
|         self.check_not_authenticated("get", url) | ||||
|  | ||||
|     def test_get_policy_check_status(self): | ||||
|         # setup data | ||||
|         site = baker.make("clients.Site") | ||||
| @@ -194,14 +189,14 @@ class TestPolicyViews(TacticalTestCase): | ||||
|             managed_by_policy=True, | ||||
|             parent_check=policy_diskcheck.pk, | ||||
|         ) | ||||
|         url = f"/automation/policycheckstatus/{policy_diskcheck.pk}/check/" | ||||
|         url = f"/automation/checks/{policy_diskcheck.pk}/status/" | ||||
|  | ||||
|         resp = self.client.patch(url, format="json") | ||||
|         resp = self.client.get(url, format="json") | ||||
|         serializer = PolicyCheckStatusSerializer([managed_check], many=True) | ||||
|  | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|         self.assertEqual(resp.data, serializer.data)  # type: ignore | ||||
|         self.check_not_authenticated("patch", url) | ||||
|         self.check_not_authenticated("get", url) | ||||
|  | ||||
|     def test_policy_overview(self): | ||||
|         from clients.models import Client | ||||
| @@ -254,43 +249,44 @@ class TestPolicyViews(TacticalTestCase): | ||||
|  | ||||
|         # policy with a task | ||||
|         policy = baker.make("automation.Policy") | ||||
|         task = baker.make("autotasks.AutomatedTask", policy=policy) | ||||
|         task = baker.make_recipe("autotasks.task", policy=policy) | ||||
|  | ||||
|         # create policy managed tasks | ||||
|         policy_tasks = baker.make( | ||||
|             "autotasks.AutomatedTask", parent_task=task.id, _quantity=5  # type: ignore | ||||
|         policy_tasks = baker.make_recipe( | ||||
|             "autotasks.task", parent_task=task.id, _quantity=5  # type: ignore | ||||
|         ) | ||||
|  | ||||
|         url = f"/automation/policyautomatedtaskstatus/{task.id}/task/"  # type: ignore | ||||
|         url = f"/automation/tasks/{task.id}/status/"  # type: ignore | ||||
|  | ||||
|         serializer = PolicyTaskStatusSerializer(policy_tasks, many=True) | ||||
|         resp = self.client.patch(url, format="json") | ||||
|         resp = self.client.get(url, format="json") | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|         self.assertEqual(resp.data, serializer.data)  # type: ignore | ||||
|         self.assertEqual(len(resp.data), 5)  # type: ignore | ||||
|  | ||||
|         self.check_not_authenticated("patch", url) | ||||
|         self.check_not_authenticated("get", url) | ||||
|  | ||||
|     @patch("automation.tasks.run_win_policy_autotask_task.delay") | ||||
|     @patch("automation.tasks.run_win_policy_autotasks_task.delay") | ||||
|     def test_run_win_task(self, mock_task): | ||||
|  | ||||
|         # create managed policy tasks | ||||
|         tasks = baker.make( | ||||
|             "autotasks.AutomatedTask", | ||||
|         tasks = baker.make_recipe( | ||||
|             "autotasks.task", | ||||
|             managed_by_policy=True, | ||||
|             parent_task=1, | ||||
|             _quantity=6, | ||||
|         ) | ||||
|         url = "/automation/runwintask/1/" | ||||
|         resp = self.client.put(url, format="json") | ||||
|  | ||||
|         url = "/automation/tasks/1/run/" | ||||
|         resp = self.client.post(url, format="json") | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|  | ||||
|         mock_task.assert_called_once_with([task.pk for task in tasks])  # type: ignore | ||||
|         mock_task.assert_called()  # type: ignore | ||||
|  | ||||
|         self.check_not_authenticated("put", url) | ||||
|         self.check_not_authenticated("post", url) | ||||
|  | ||||
|     def test_create_new_patch_policy(self): | ||||
|         url = "/automation/winupdatepolicy/" | ||||
|         url = "/automation/patchpolicy/" | ||||
|  | ||||
|         # test policy doesn't exist | ||||
|         data = {"policy": 500} | ||||
| @@ -321,15 +317,14 @@ class TestPolicyViews(TacticalTestCase): | ||||
|     def test_update_patch_policy(self): | ||||
|  | ||||
|         # test policy doesn't exist | ||||
|         resp = self.client.put("/automation/winupdatepolicy/500/", format="json") | ||||
|         resp = self.client.put("/automation/patchpolicy/500/", format="json") | ||||
|         self.assertEqual(resp.status_code, 404) | ||||
|  | ||||
|         policy = baker.make("automation.Policy") | ||||
|         patch_policy = baker.make("winupdate.WinUpdatePolicy", policy=policy) | ||||
|         url = f"/automation/winupdatepolicy/{patch_policy.pk}/"  # type: ignore | ||||
|         url = f"/automation/patchpolicy/{patch_policy.pk}/"  # type: ignore | ||||
|  | ||||
|         data = { | ||||
|             "id": patch_policy.pk,  # type: ignore | ||||
|             "policy": policy.pk,  # type: ignore | ||||
|             "critical": "approve", | ||||
|             "important": "approve", | ||||
| @@ -345,7 +340,7 @@ class TestPolicyViews(TacticalTestCase): | ||||
|         self.check_not_authenticated("put", url) | ||||
|  | ||||
|     def test_reset_patch_policy(self): | ||||
|         url = "/automation/winupdatepolicy/reset/" | ||||
|         url = "/automation/patchpolicy/reset/" | ||||
|  | ||||
|         inherit_fields = { | ||||
|             "critical": "inherit", | ||||
| @@ -374,7 +369,7 @@ class TestPolicyViews(TacticalTestCase): | ||||
|         # test reset agents in site | ||||
|         data = {"site": sites[0].id}  # type: ignore | ||||
|  | ||||
|         resp = self.client.patch(url, data, format="json") | ||||
|         resp = self.client.post(url, data, format="json") | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|  | ||||
|         agents = Agent.objects.filter(site=sites[0])  # type: ignore | ||||
| @@ -386,7 +381,7 @@ class TestPolicyViews(TacticalTestCase): | ||||
|         # test reset agents in client | ||||
|         data = {"client": clients[1].id}  # type: ignore | ||||
|  | ||||
|         resp = self.client.patch(url, data, format="json") | ||||
|         resp = self.client.post(url, data, format="json") | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|  | ||||
|         agents = Agent.objects.filter(site__client=clients[1])  # type: ignore | ||||
| @@ -398,7 +393,7 @@ class TestPolicyViews(TacticalTestCase): | ||||
|         # test reset all agents | ||||
|         data = {} | ||||
|  | ||||
|         resp = self.client.patch(url, data, format="json") | ||||
|         resp = self.client.post(url, data, format="json") | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|  | ||||
|         agents = Agent.objects.all() | ||||
| @@ -406,17 +401,17 @@ class TestPolicyViews(TacticalTestCase): | ||||
|             for k, v in inherit_fields.items(): | ||||
|                 self.assertEqual(getattr(agent.winupdatepolicy.get(), k), v) | ||||
|  | ||||
|         self.check_not_authenticated("patch", url) | ||||
|         self.check_not_authenticated("post", url) | ||||
|  | ||||
|     def test_delete_patch_policy(self): | ||||
|         # test patch policy doesn't exist | ||||
|         resp = self.client.delete("/automation/winupdatepolicy/500/", format="json") | ||||
|         resp = self.client.delete("/automation/patchpolicy/500/", format="json") | ||||
|         self.assertEqual(resp.status_code, 404) | ||||
|  | ||||
|         winupdate_policy = baker.make_recipe( | ||||
|             "winupdate.winupdate_policy", policy__name="Test Policy" | ||||
|         ) | ||||
|         url = f"/automation/winupdatepolicy/{winupdate_policy.pk}/" | ||||
|         url = f"/automation/patchpolicy/{winupdate_policy.pk}/" | ||||
|  | ||||
|         resp = self.client.delete(url, format="json") | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
| @@ -426,7 +421,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 +436,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) | ||||
|  | ||||
| @@ -471,7 +466,7 @@ class TestPolicyTasks(TacticalTestCase): | ||||
|  | ||||
|         # Add Client to Policy | ||||
|         policy.server_clients.add(server_agents[13].client)  # type: ignore | ||||
|         policy.workstation_clients.add(workstation_agents[15].client)  # type: ignore | ||||
|         policy.workstation_clients.add(workstation_agents[13].client)  # type: ignore | ||||
|  | ||||
|         resp = self.client.get( | ||||
|             f"/automation/policies/{policy.pk}/related/", format="json"  # type: ignore | ||||
| @@ -479,25 +474,31 @@ class TestPolicyTasks(TacticalTestCase): | ||||
|  | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|         self.assertEquals(len(resp.data["server_clients"]), 1)  # type: ignore | ||||
|         self.assertEquals(len(resp.data["server_sites"]), 5)  # type: ignore | ||||
|         self.assertEquals(len(resp.data["server_sites"]), 0)  # type: ignore | ||||
|         self.assertEquals(len(resp.data["workstation_clients"]), 1)  # type: ignore | ||||
|         self.assertEquals(len(resp.data["workstation_sites"]), 5)  # type: ignore | ||||
|         self.assertEquals(len(resp.data["agents"]), 10)  # type: ignore | ||||
|         self.assertEquals(len(resp.data["workstation_sites"]), 0)  # type: ignore | ||||
|         self.assertEquals(len(resp.data["agents"]), 0)  # type: ignore | ||||
|  | ||||
|         # Add Site to Policy and the agents and sites length shouldn't change | ||||
|         policy.server_sites.add(server_agents[13].site)  # type: ignore | ||||
|         policy.workstation_sites.add(workstation_agents[15].site)  # type: ignore | ||||
|         self.assertEquals(len(resp.data["server_sites"]), 5)  # type: ignore | ||||
|         self.assertEquals(len(resp.data["workstation_sites"]), 5)  # type: ignore | ||||
|         self.assertEquals(len(resp.data["agents"]), 10)  # type: ignore | ||||
|         # Add Site to Policy | ||||
|         policy.server_sites.add(server_agents[10].site)  # type: ignore | ||||
|         policy.workstation_sites.add(workstation_agents[10].site)  # type: ignore | ||||
|         resp = self.client.get( | ||||
|             f"/automation/policies/{policy.pk}/related/", format="json"  # type: ignore | ||||
|         ) | ||||
|         self.assertEquals(len(resp.data["server_sites"]), 1)  # type: ignore | ||||
|         self.assertEquals(len(resp.data["workstation_sites"]), 1)  # type: ignore | ||||
|         self.assertEquals(len(resp.data["agents"]), 0)  # type: ignore | ||||
|  | ||||
|         # Add Agent to Policy and the agents length shouldn't change | ||||
|         policy.agents.add(server_agents[13])  # type: ignore | ||||
|         policy.agents.add(workstation_agents[15])  # type: ignore | ||||
|         self.assertEquals(len(resp.data["agents"]), 10)  # type: ignore | ||||
|         # Add Agent to Policy | ||||
|         policy.agents.add(server_agents[2])  # type: ignore | ||||
|         policy.agents.add(workstation_agents[2])  # type: ignore | ||||
|         resp = self.client.get( | ||||
|             f"/automation/policies/{policy.pk}/related/", format="json"  # type: ignore | ||||
|         ) | ||||
|         self.assertEquals(len(resp.data["agents"]), 2)  # type: ignore | ||||
|  | ||||
|     def test_generating_agent_policy_checks(self): | ||||
|         from .tasks import generate_agent_checks_from_policies_task | ||||
|         from .tasks import generate_agent_checks_task | ||||
|  | ||||
|         # setup data | ||||
|         policy = baker.make("automation.Policy", active=True) | ||||
| @@ -505,7 +506,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 +546,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 +556,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,20 +567,19 @@ 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) | ||||
|         self.create_checks(policy=policy) | ||||
|  | ||||
|         baker.make( | ||||
|             "autotasks.AutomatedTask", policy=policy, name=seq("Task"), _quantity=3 | ||||
|         baker.make_recipe( | ||||
|             "autotasks.task", policy=policy, name=seq("Task"), _quantity=3 | ||||
|         ) | ||||
|  | ||||
|         server_agent = baker.make_recipe("agents.server_agent") | ||||
| @@ -596,16 +596,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 +618,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 +640,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 +662,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 +684,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 +706,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 +728,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 +750,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 +767,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 +783,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 +798,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 +812,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 +823,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,17 +854,18 @@ 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) | ||||
|         tasks = baker.make( | ||||
|             "autotasks.AutomatedTask", policy=policy, name=seq("Task"), _quantity=3 | ||||
|         tasks = baker.make_recipe( | ||||
|             "autotasks.task", policy=policy, name=seq("Task"), _quantity=3 | ||||
|         ) | ||||
|         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 +884,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) | ||||
|         tasks = baker.make_recipe("autotasks.task", 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 generate_agent_checks_task, run_win_policy_autotasks_task | ||||
|  | ||||
|         policy = baker.make("automation.Policy", active=True) | ||||
|         tasks = baker.make_recipe("autotasks.task", 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 ( | ||||
|             generate_agent_checks_task, | ||||
|             update_policy_autotasks_fields_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 | ||||
|         tasks = baker.make_recipe( | ||||
|             "autotasks.task", | ||||
|             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,25 +960,31 @@ 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) | ||||
|         task = baker.make("autotasks.AutomatedTask", policy=policy) | ||||
|         task = baker.make_recipe("autotasks.task", policy=policy) | ||||
|         agent = baker.make_recipe( | ||||
|             "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 +998,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 +1019,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 +1040,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 +1067,88 @@ 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_recipe("autotasks.task", 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()) | ||||
|  | ||||
|  | ||||
| class TestAutomationPermission(TacticalTestCase): | ||||
|     def setUp(self): | ||||
|         self.client_setup() | ||||
|         self.setup_coresettings() | ||||
|   | ||||
| @@ -1,3 +1,5 @@ | ||||
| from autotasks.views import GetAddAutoTasks | ||||
| from checks.views import GetAddChecks | ||||
| from django.urls import path | ||||
|  | ||||
| from . import views | ||||
| @@ -8,12 +10,14 @@ urlpatterns = [ | ||||
|     path("policies/overview/", views.OverviewPolicy.as_view()), | ||||
|     path("policies/<int:pk>/", views.GetUpdateDeletePolicy.as_view()), | ||||
|     path("sync/", views.PolicySync.as_view()), | ||||
|     path("<int:pk>/policychecks/", views.PolicyCheck.as_view()), | ||||
|     path("<int:pk>/policyautomatedtasks/", views.PolicyAutoTask.as_view()), | ||||
|     path("policycheckstatus/<int:check>/check/", views.PolicyCheck.as_view()), | ||||
|     path("policyautomatedtaskstatus/<int:task>/task/", views.PolicyAutoTask.as_view()), | ||||
|     path("runwintask/<int:task>/", views.PolicyAutoTask.as_view()), | ||||
|     path("winupdatepolicy/", views.UpdatePatchPolicy.as_view()), | ||||
|     path("winupdatepolicy/<int:patchpolicy>/", views.UpdatePatchPolicy.as_view()), | ||||
|     path("winupdatepolicy/reset/", views.UpdatePatchPolicy.as_view()), | ||||
|     # alias to get policy checks | ||||
|     path("policies/<int:policy>/checks/", GetAddChecks.as_view()), | ||||
|     # alias to get policy tasks | ||||
|     path("policies/<int:policy>/tasks/", GetAddAutoTasks.as_view()), | ||||
|     path("checks/<int:check>/status/", views.PolicyCheck.as_view()), | ||||
|     path("tasks/<int:task>/status/", views.PolicyAutoTask.as_view()), | ||||
|     path("tasks/<int:task>/run/", views.PolicyAutoTask.as_view()), | ||||
|     path("patchpolicy/", views.UpdatePatchPolicy.as_view()), | ||||
|     path("patchpolicy/<int:pk>/", views.UpdatePatchPolicy.as_view()), | ||||
|     path("patchpolicy/reset/", views.ResetPatchPolicy.as_view()), | ||||
| ] | ||||
|   | ||||
| @@ -1,35 +1,41 @@ | ||||
| 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 tacticalrmm.utils import notify_error | ||||
| from django.shortcuts import get_object_or_404 | ||||
| from rest_framework.exceptions import PermissionDenied | ||||
| from rest_framework.permissions import IsAuthenticated | ||||
| from rest_framework.response import Response | ||||
| from rest_framework.views import APIView | ||||
| from winupdate.models import WinUpdatePolicy | ||||
| from winupdate.serializers import WinUpdatePolicySerializer | ||||
|  | ||||
| from tacticalrmm.permissions import _has_perm_on_client, _has_perm_on_site | ||||
| from tacticalrmm.utils import notify_error | ||||
|  | ||||
| from .models import Policy | ||||
| from .permissions import AutomationPolicyPerms | ||||
| from .serializers import ( | ||||
|     AutoTasksFieldSerializer, | ||||
|     PolicyCheckSerializer, | ||||
|     PolicyCheckStatusSerializer, | ||||
|     PolicyOverviewSerializer, | ||||
|     PolicyRelatedSerializer, | ||||
|     PolicySerializer, | ||||
|     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() | ||||
|  | ||||
|         return Response(PolicyTableSerializer(policies, many=True).data) | ||||
|         return Response( | ||||
|             PolicyTableSerializer( | ||||
|                 policies, context={"user": request.user}, many=True | ||||
|             ).data | ||||
|         ) | ||||
|  | ||||
|     def post(self, request): | ||||
|         serializer = PolicySerializer(data=request.data, partial=True) | ||||
| @@ -53,18 +59,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 +94,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") | ||||
|  | ||||
| @@ -89,29 +107,23 @@ class PolicySync(APIView): | ||||
|  | ||||
| class PolicyAutoTask(APIView): | ||||
|  | ||||
|     # tasks associated with policy | ||||
|     def get(self, request, pk): | ||||
|         tasks = AutomatedTask.objects.filter(policy=pk) | ||||
|         return Response(AutoTasksFieldSerializer(tasks, many=True).data) | ||||
|  | ||||
|     # get status of all tasks | ||||
|     def patch(self, request, task): | ||||
|     def get(self, request, task): | ||||
|         tasks = AutomatedTask.objects.filter(parent_task=task) | ||||
|         return Response(PolicyTaskStatusSerializer(tasks, many=True).data) | ||||
|  | ||||
|     # bulk run win tasks associated with policy | ||||
|     def put(self, request, task): | ||||
|         tasks = AutomatedTask.objects.filter(parent_task=task) | ||||
|         run_win_policy_autotask_task.delay([task.id for task in tasks]) | ||||
|     def post(self, request, task): | ||||
|         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): | ||||
|     def get(self, request, pk): | ||||
|         checks = Check.objects.filter(policy__pk=pk, agent=None) | ||||
|         return Response(PolicyCheckSerializer(checks, many=True).data) | ||||
|     permission_classes = [IsAuthenticated, AutomationPolicyPerms] | ||||
|  | ||||
|     def patch(self, request, check): | ||||
|     def get(self, request, check): | ||||
|         checks = Check.objects.filter(parent_check=check) | ||||
|         return Response(PolicyCheckStatusSerializer(checks, many=True).data) | ||||
|  | ||||
| @@ -126,8 +138,6 @@ class OverviewPolicy(APIView): | ||||
| class GetRelated(APIView): | ||||
|     def get(self, request, pk): | ||||
|  | ||||
|         response = {} | ||||
|  | ||||
|         policy = ( | ||||
|             Policy.objects.filter(pk=pk) | ||||
|             .prefetch_related( | ||||
| @@ -139,47 +149,13 @@ class GetRelated(APIView): | ||||
|             .first() | ||||
|         ) | ||||
|  | ||||
|         response["default_server_policy"] = policy.is_default_server_policy | ||||
|         response["default_workstation_policy"] = policy.is_default_workstation_policy | ||||
|  | ||||
|         response["server_clients"] = ClientSerializer( | ||||
|             policy.server_clients.all(), many=True | ||||
|         ).data | ||||
|         response["workstation_clients"] = ClientSerializer( | ||||
|             policy.workstation_clients.all(), many=True | ||||
|         ).data | ||||
|  | ||||
|         filtered_server_sites = list() | ||||
|         filtered_workstation_sites = list() | ||||
|  | ||||
|         for client in policy.server_clients.all(): | ||||
|             for site in client.sites.all(): | ||||
|                 if site not in policy.server_sites.all(): | ||||
|                     filtered_server_sites.append(site) | ||||
|  | ||||
|         response["server_sites"] = SiteSerializer( | ||||
|             filtered_server_sites + list(policy.server_sites.all()), many=True | ||||
|         ).data | ||||
|  | ||||
|         for client in policy.workstation_clients.all(): | ||||
|             for site in client.sites.all(): | ||||
|                 if site not in policy.workstation_sites.all(): | ||||
|                     filtered_workstation_sites.append(site) | ||||
|  | ||||
|         response["workstation_sites"] = SiteSerializer( | ||||
|             filtered_workstation_sites + list(policy.workstation_sites.all()), many=True | ||||
|         ).data | ||||
|  | ||||
|         response["agents"] = AgentHostnameSerializer( | ||||
|             policy.related_agents().only("pk", "hostname"), | ||||
|             many=True, | ||||
|         ).data | ||||
|  | ||||
|         return Response(response) | ||||
|         return Response( | ||||
|             PolicyRelatedSerializer(policy, context={"user": request.user}).data | ||||
|         ) | ||||
|  | ||||
|  | ||||
| class UpdatePatchPolicy(APIView): | ||||
|  | ||||
|     permission_classes = [IsAuthenticated, AutomationPolicyPerms] | ||||
|     # create new patch policy | ||||
|     def post(self, request): | ||||
|         policy = get_object_or_404(Policy, pk=request.data["policy"]) | ||||
| @@ -192,8 +168,8 @@ class UpdatePatchPolicy(APIView): | ||||
|         return Response("ok") | ||||
|  | ||||
|     # update patch policy | ||||
|     def put(self, request, patchpolicy): | ||||
|         policy = get_object_or_404(WinUpdatePolicy, pk=patchpolicy) | ||||
|     def put(self, request, pk): | ||||
|         policy = get_object_or_404(WinUpdatePolicy, pk=pk) | ||||
|  | ||||
|         serializer = WinUpdatePolicySerializer( | ||||
|             instance=policy, data=request.data, partial=True | ||||
| @@ -203,20 +179,41 @@ class UpdatePatchPolicy(APIView): | ||||
|  | ||||
|         return Response("ok") | ||||
|  | ||||
|     # bulk reset agent patch policy | ||||
|     def patch(self, request): | ||||
|     # delete patch policy | ||||
|     def delete(self, request, pk): | ||||
|         get_object_or_404(WinUpdatePolicy, pk=pk).delete() | ||||
|  | ||||
|         return Response("ok") | ||||
|  | ||||
|  | ||||
| class ResetPatchPolicy(APIView): | ||||
|     # bulk reset agent patch policy | ||||
|     def post(self, request): | ||||
|  | ||||
|         agents = None | ||||
|         if "client" in request.data: | ||||
|             agents = Agent.objects.prefetch_related("winupdatepolicy").filter( | ||||
|                 site__client_id=request.data["client"] | ||||
|             if not _has_perm_on_client(request.user, request.data["client"]): | ||||
|                 raise PermissionDenied() | ||||
|  | ||||
|             agents = ( | ||||
|                 Agent.objects.filter_by_role(request.user) | ||||
|                 .prefetch_related("winupdatepolicy") | ||||
|                 .filter(site__client_id=request.data["client"]) | ||||
|             ) | ||||
|         elif "site" in request.data: | ||||
|             agents = Agent.objects.prefetch_related("winupdatepolicy").filter( | ||||
|                 site_id=request.data["site"] | ||||
|             if not _has_perm_on_site(request.user, request.data["site"]): | ||||
|                 raise PermissionDenied() | ||||
|  | ||||
|             agents = ( | ||||
|                 Agent.objects.filter_by_role(request.user) | ||||
|                 .prefetch_related("winupdatepolicy") | ||||
|                 .filter(site_id=request.data["site"]) | ||||
|             ) | ||||
|         else: | ||||
|             agents = Agent.objects.prefetch_related("winupdatepolicy").only("pk") | ||||
|             agents = ( | ||||
|                 Agent.objects.filter_by_role(request.user) | ||||
|                 .prefetch_related("winupdatepolicy") | ||||
|                 .only("pk") | ||||
|             ) | ||||
|  | ||||
|         for agent in agents: | ||||
|             winupdatepolicy = agent.winupdatepolicy.get() | ||||
| @@ -241,10 +238,4 @@ class UpdatePatchPolicy(APIView): | ||||
|                 ] | ||||
|             ) | ||||
|  | ||||
|         return Response("ok") | ||||
|  | ||||
|     # delete patch policy | ||||
|     def delete(self, request, patchpolicy): | ||||
|         get_object_or_404(WinUpdatePolicy, pk=patchpolicy).delete() | ||||
|  | ||||
|         return Response("ok") | ||||
|         return Response("The patch policy on the affected agents has been reset.") | ||||
|   | ||||
							
								
								
									
										10
									
								
								api/tacticalrmm/autotasks/baker_recipes.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								api/tacticalrmm/autotasks/baker_recipes.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| from itertools import cycle | ||||
|  | ||||
| from model_bakery.recipe import Recipe, foreign_key, seq | ||||
|  | ||||
| script = Recipe("scripts.script") | ||||
|  | ||||
| task = Recipe( | ||||
|     "autotasks.AutomatedTask", | ||||
|     script=foreign_key(script), | ||||
| ) | ||||
| @@ -1,7 +1,6 @@ | ||||
| from django.core.management.base import BaseCommand | ||||
|  | ||||
| from agents.models import Agent | ||||
| from autotasks.tasks import remove_orphaned_win_tasks | ||||
| from django.core.management.base import BaseCommand | ||||
|  | ||||
|  | ||||
| class Command(BaseCommand): | ||||
|   | ||||
| @@ -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), | ||||
|         ), | ||||
|     ] | ||||
| @@ -0,0 +1,23 @@ | ||||
| # Generated by Django 3.2.6 on 2021-09-17 19:54 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("autotasks", "0022_automatedtask_collector_all_output"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name="automatedtask", | ||||
|             name="created_by", | ||||
|             field=models.CharField(blank=True, max_length=255, null=True), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name="automatedtask", | ||||
|             name="modified_by", | ||||
|             field=models.CharField(blank=True, max_length=255, null=True), | ||||
|         ), | ||||
|     ] | ||||
| @@ -0,0 +1,87 @@ | ||||
| # Generated by Django 3.2.9 on 2021-12-14 00:40 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('autotasks', '0023_auto_20210917_1954'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.RemoveField( | ||||
|             model_name='automatedtask', | ||||
|             name='run_time_days', | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='automatedtask', | ||||
|             name='actions', | ||||
|             field=models.JSONField(default=dict), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='automatedtask', | ||||
|             name='daily_interval', | ||||
|             field=models.PositiveSmallIntegerField(blank=True, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='automatedtask', | ||||
|             name='expire_date', | ||||
|             field=models.DateTimeField(blank=True, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='automatedtask', | ||||
|             name='monthly_days_of_month', | ||||
|             field=models.PositiveIntegerField(blank=True, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='automatedtask', | ||||
|             name='monthly_months_of_year', | ||||
|             field=models.PositiveIntegerField(blank=True, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='automatedtask', | ||||
|             name='monthly_weeks_of_month', | ||||
|             field=models.PositiveSmallIntegerField(blank=True, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='automatedtask', | ||||
|             name='random_task_delay', | ||||
|             field=models.CharField(blank=True, max_length=10, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='automatedtask', | ||||
|             name='stop_task_at_duration_end', | ||||
|             field=models.BooleanField(blank=True, default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='automatedtask', | ||||
|             name='task_instance_policy', | ||||
|             field=models.PositiveSmallIntegerField(blank=True, default=1), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='automatedtask', | ||||
|             name='task_repetition_duration', | ||||
|             field=models.CharField(blank=True, max_length=10, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='automatedtask', | ||||
|             name='task_repetition_interval', | ||||
|             field=models.CharField(blank=True, max_length=10, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='automatedtask', | ||||
|             name='weekly_interval', | ||||
|             field=models.PositiveSmallIntegerField(blank=True, null=True), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name='automatedtask', | ||||
|             name='task_type', | ||||
|             field=models.CharField(choices=[('daily', 'Daily'), ('weekly', 'Weekly'), ('monthly', 'Monthly'), ('monthlydow', 'Monthly Day of Week'), ('checkfailure', 'On Check Failure'), ('manual', 'Manual'), ('runonce', 'Run Once')], default='manual', max_length=100), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name='automatedtask', | ||||
|             name='timeout', | ||||
|             field=models.PositiveIntegerField(blank=True, default=120), | ||||
|         ), | ||||
|     ] | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user