Compare commits
717 Commits
0.0.1
...
4cb37f8347
Author | SHA1 | Date | |
---|---|---|---|
4cb37f8347 | |||
a2010b9517 | |||
c95736ce14 | |||
b46c61954c | |||
afe4266600 | |||
0c8d1c8da1 | |||
eac998b5cc | |||
5914782252 | |||
73d875c4ac | |||
8f439f0675 | |||
0f102c6aaf | |||
4852c6caeb | |||
3fffba2eba | |||
a1293984ea | |||
4876db50c1 | |||
425cc066af | |||
1086f517fa | |||
2fdbf87ddd | |||
86228836c7 | |||
a6e6c948a5 | |||
dcdfa8feb7 | |||
8388d2e695 | |||
29f269050f | |||
93c4fc2009 | |||
9d5464b5a9 | |||
7848397ae2 | |||
f298ce94bf | |||
3cace8943e | |||
aa40d68c88 | |||
c0f186db89 | |||
6b35e7808c | |||
467f6fca6b | |||
f86b2d5216 | |||
e29d8e1ec1 | |||
0fc5f41391 | |||
4b29448d84 | |||
e9fe4896df | |||
b9d32a2c16 | |||
d6bd99c5de | |||
7de5ab12bf | |||
3fe09fb8f9 | |||
eb6b03f731 | |||
40e3078a58 | |||
4ba79c6ae9 | |||
b5c31d81d3 | |||
c3b585d416 | |||
84d21f4af8 | |||
262e431834 | |||
cde2562048 | |||
67d853cf25 | |||
3ba6bb5b4b | |||
84d4f48c63 | |||
5e8bebbeb1 | |||
b66a8644a0 | |||
f0ae185fc5 | |||
43b7e413a6 | |||
04dc00d79d | |||
8e6fd58107 | |||
bfe9a95038 | |||
33687791ec | |||
57bc972b0f | |||
f437eeccb8 | |||
4e11ad67d0 | |||
40ba645a35 | |||
27e73e21d1 | |||
a6c0785de0 | |||
83328be22e | |||
c6ed5c8279 | |||
a4dc7f479a | |||
71726035dc | |||
c624a3617c | |||
cf00ab6234 | |||
e8684c5206 | |||
bb388a1969 | |||
d99f2d3c6f | |||
81a72773cb | |||
5fa88a5209 | |||
366579c12b | |||
fed0c5c3e5 | |||
c496d10c1a | |||
3993cc96a5 | |||
a4b37b34a9 | |||
2f55024f0b | |||
213644a51a | |||
281d839801 | |||
4fd157a785 | |||
968b3a0f92 | |||
f5ba608ed1 | |||
289668bb7f | |||
9e28722dba | |||
9b673f4a07 | |||
3a9e4b29b3 | |||
8d59462561 | |||
098e41e6a1 | |||
fc3f0b39e2 | |||
de53948cea | |||
823ebc0eb5 | |||
41414438d1 | |||
5704560beb | |||
8a48902b64 | |||
61fe059513 | |||
94576cc733 | |||
3a32c62119 | |||
9ea4fe1adc | |||
0798a672c2 | |||
f4e68529ba | |||
92a411baec | |||
034857d088 | |||
e5ce86a9bb | |||
5188b3d52e | |||
61b9435d1f | |||
8244676530 | |||
ec1e7cca85 | |||
72ab9253d7 | |||
4f89255c4f | |||
8d6d1d0d56 | |||
2d0c3a660a | |||
974a208869 | |||
7f225784c2 | |||
a3be95013c | |||
adefbf3960 | |||
9a1ca7a104 | |||
e84e80cd8f | |||
ebc266010a | |||
519277e18b | |||
a5a5874211 | |||
fa2b90ee7b | |||
5c74360842 | |||
8457f15eca | |||
5bc5a4b065 | |||
40350d166e | |||
9a94ba31e4 | |||
55197e7dcc | |||
a67bc70503 | |||
60538e1cec | |||
416e029c23 | |||
fe64c11927 | |||
1f8244ae40 | |||
9871cf248b | |||
a1759ecaaf | |||
30e0342f52 | |||
5a201ef548 | |||
7b26fac73d | |||
7c62309a31 | |||
621cbd2d71 | |||
d8e89bee10 | |||
4ee62aa399 | |||
f1201e8933 | |||
9acc4fdfcb | |||
6776612b66 | |||
af3e770760 | |||
fbe7e63cc9 | |||
aec460306b | |||
46c4419350 | |||
1f35893db9 | |||
935e10dc24 | |||
a4617c28f8 | |||
d4aaea4dbb | |||
a7168834ba | |||
329049e81d | |||
e25ec12cb0 | |||
5ae487cd3e | |||
4c42f77692 | |||
3aab7b57e8 | |||
931c9864db | |||
65bf994619 | |||
367c4bebb6 | |||
8c1be67974 | |||
789c035a03 | |||
1cf15f7339 | |||
77ff580f19 | |||
423ff11d4c | |||
9e4b5185b1 | |||
020441c41a | |||
d0a3b7b49d | |||
960fa5485d | |||
26db463044 | |||
1193f1d86d | |||
9bece0a811 | |||
f29ec63f46 | |||
e48278e6e9 | |||
c41c7ed1f0 | |||
c9190e9a7d | |||
0294f5ed65 | |||
ae4fdcfc58 | |||
a395f30bd4 | |||
c057ffdc9c | |||
6837c38303 | |||
ee8920a464 | |||
ccfdf005f7 | |||
0276f9454b | |||
45cc34284a | |||
7329a65ae7 | |||
9a529a64e2 | |||
f2640df0d3 | |||
7d172fb4af | |||
f848d01b34 | |||
44f20b28be | |||
2e22a484a0 | |||
a62a36ba82 | |||
c00cf16bc8 | |||
7784dfede9 | |||
03d350e302 | |||
9b79c9d7ff | |||
1d5c86f13b | |||
9e336d368d | |||
937e935949 | |||
860eaa6749 | |||
aab94431a9 | |||
7cfede45b8 | |||
65de93715d | |||
fea7ea3119 | |||
524a70ba18 | |||
29c4b4a0ca | |||
f5ae01b08d | |||
ee3dd68cfe | |||
25efa31493 | |||
4a6ce35332 | |||
332810ffd6 | |||
f0bbd22cf4 | |||
6bf681530d | |||
9dd2f6a341 | |||
23c640a460 | |||
3c6092f776 | |||
cb66b9303a | |||
2d80f02634 | |||
abe1ce6948 | |||
fb907283b0 | |||
86ed7318ec | |||
a2a8e12046 | |||
c1a8ee65f2 | |||
6a14f78bf7 | |||
90a01911da | |||
de3ed3a881 | |||
656807e410 | |||
f64be2ea33 | |||
ef9c596ec7 | |||
f22e886d92 | |||
a2c67541ec | |||
5f4231ab04 | |||
b0405c8fd0 | |||
b42bb3a30e | |||
27eb54cc37 | |||
a8e2c687b1 | |||
7aeba34787 | |||
090c4a5425 | |||
87a1f2aa20 | |||
70135eaa91 | |||
f47b97e2a0 | |||
67f20ecb66 | |||
3bceb66600 | |||
fe34b8274d | |||
a235aa7ec3 | |||
f69f883439 | |||
7b4ed7b135 | |||
b801c9a49e | |||
583e1767a1 | |||
241ba47c80 | |||
c2d673ca1b | |||
05c46df0a9 | |||
53284d456f | |||
6cfcf1580c | |||
4d3a238583 | |||
47d6a3beff | |||
111791438a | |||
ce2c6f3b13 | |||
e655f22fac | |||
66b8d9362d | |||
37d277e149 | |||
f686691232 | |||
802f2c410d | |||
be559d3d9d | |||
d6cfef3a0b | |||
4fdb3df06e | |||
7eb0651b89 | |||
6d3984f6e1 | |||
58b134ae30 | |||
50384044c8 | |||
2cda4228ce | |||
67585b9f89 | |||
4e42856027 | |||
58051f297f | |||
0a9a5b20fa | |||
a0874356fd | |||
5d8f5e3a51 | |||
19ae56d92c | |||
488a12df45 | |||
a94856879e | |||
94375dc30e | |||
108398da4b | |||
8abbf2ff9e | |||
27b62d1018 | |||
aef276b76c | |||
c15eca2e58 | |||
33b10f7109 | |||
9a40d095e8 | |||
991ddc3d7f | |||
35c11ed6f0 | |||
e4f5ec4892 | |||
109fc49d76 | |||
da8946fcb6 | |||
848661856a | |||
14acea31f2 | |||
2bbf78d888 | |||
afb5a709d7 | |||
ddead8eb56 | |||
e517c5fd76 | |||
a6e569eaef | |||
78216116df | |||
b20b426432 | |||
44afa4f7de | |||
2be4810ed4 | |||
f861295b1c | |||
320d3f1a13 | |||
3613318217 | |||
ceb1929d8c | |||
dbcb282548 | |||
5eec41fe57 | |||
e72eedf077 | |||
6286b06270 | |||
d2bf0e54d7 | |||
7f1a7eaa0d | |||
29b104a6ce | |||
1220ddbd00 | |||
da746b8977 | |||
34db5f863a | |||
92fe05d083 | |||
5280db8767 | |||
9eda12c232 | |||
54c34a95f5 | |||
b7a2bfc612 | |||
3b3ee9fc3d | |||
d64108331f | |||
372eefa5c4 | |||
733a31ad71 | |||
4a19bb2ecc | |||
29a8969288 | |||
0a1aba7ca8 | |||
eb8dca9806 | |||
8af5975428 | |||
4a10409551 | |||
ac70715752 | |||
8ccdf9a8f3 | |||
1200a87913 | |||
dfba01aed9 | |||
e8cb685da1 | |||
7798deaf27 | |||
8b47d95614 | |||
d4c07d08f1 | |||
7239f572a3 | |||
904234c581 | |||
fe1a9d07f7 | |||
c570fb114f | |||
ea1727f2c7 | |||
36d7e54547 | |||
a02fda8413 | |||
b5bc76b0ab | |||
36c13e18c7 | |||
6969b61164 | |||
85bf1b9907 | |||
ca8e0c07ea | |||
da93425c0b | |||
8a9899cf66 | |||
38db558be2 | |||
67b204e40c | |||
456fed80a9 | |||
87282ce41c | |||
4016d4c200 | |||
f36662ca82 | |||
3e340a47b8 | |||
60a22f5574 | |||
2eb50311b4 | |||
36fa364d04 | |||
65c6065ba1 | |||
505f4cfdd9 | |||
2252c86f71 | |||
dc4968ee7b | |||
3fb2706321 | |||
f05e51510a | |||
193dbf1e8b | |||
05bb6f8a51 | |||
6b851ded0e | |||
8d6826f7c0 | |||
fe0696fee6 | |||
11ec62feb6 | |||
e62a570be3 | |||
36962109d1 | |||
b3b5ad6372 | |||
23c43ed8dc | |||
1069211d1b | |||
460eff1f71 | |||
0c382a73e5 | |||
ae81ee8863 | |||
07e93243a0 | |||
579e44f834 | |||
156e446608 | |||
158eb17907 | |||
8b887575c9 | |||
d0e8e9a674 | |||
d8d75c7db0 | |||
3b743a847c | |||
95a08b2d2c | |||
b38984fcb9 | |||
3040d4afe7 | |||
fa28fd436e | |||
60ecb2e18f | |||
4ee6347306 | |||
adeffff42c | |||
c0173d6feb | |||
d100c311dd | |||
930e5aeb69 | |||
ff595b0cba | |||
a4bc4b1560 | |||
735ac287f9 | |||
eb6ae13c58 | |||
dd0c13a65f | |||
cb09252b7b | |||
b24cf33207 | |||
f053b9c6a8 | |||
4da47e9a70 | |||
cbe865d5ce | |||
063ffaed43 | |||
7e3f0e0541 | |||
35cc88857a | |||
569455c127 | |||
378ae32552 | |||
4cafa34d69 | |||
46bdd488ec | |||
6650434c63 | |||
2c1bbbfc15 | |||
dd30a57a9d | |||
18e84db63c | |||
23a06be3eb | |||
0a17329a71 | |||
0d18e974dd | |||
d1b6c96d72 | |||
df98fbaecc | |||
55f0db2217 | |||
7fe1260308 | |||
5873897184 | |||
62e605d417 | |||
1f35f44f20 | |||
7eee0a26a9 | |||
df27a7dfd3 | |||
c9098f5d2f | |||
5cb155e01f | |||
39bfbd25cb | |||
fff51e38d2 | |||
746b7ac747 | |||
d422f2feee | |||
a7d195dfcb | |||
fdeae217fa | |||
8061b7c8e2 | |||
3f68d67ba5 | |||
4151e0afdc | |||
89a5e0f4cc | |||
64f4c8f2e8 | |||
f41282d08b | |||
e257c11488 | |||
2dba8997e9 | |||
397ec56028 | |||
8dfb996b24 | |||
c3f3c1247e | |||
59b5fea639 | |||
95dc979419 | |||
33b1a6c91d | |||
fbdbede429 | |||
2bf692788c | |||
09cc1db665 | |||
e7c535c48d | |||
5f3b48ea98 | |||
6437170ee8 | |||
e9cd111af6 | |||
3fef74e700 | |||
776c5db8ca | |||
95f7cb2bfc | |||
8e338c7ca0 | |||
9b811ede26 | |||
c0a09d5d50 | |||
8572b3b3c4 | |||
7afca156c0 | |||
cb79854027 | |||
4b080251e9 | |||
3c36a988ad | |||
d379205bff | |||
ebf4cb7a5d | |||
dd0eaae6b3 | |||
a9ea173e74 | |||
e34d29987e | |||
b5669c8386 | |||
58e688e0a5 | |||
e3c2f712c1 | |||
0abcb4628e | |||
b9a2d2ac59 | |||
dd49f92a31 | |||
8bfc952f2e | |||
6e6bd1070e | |||
42fd648e4c | |||
3eb6627b40 | |||
9893e5f952 | |||
e35a2300e2 | |||
0aa78a4c51 | |||
46e1c97a44 | |||
84d895c214 | |||
cba28108e0 | |||
18339547ba | |||
97d38275a4 | |||
d2e9e1070e | |||
6880c5e90b | |||
608a38384d | |||
7f7f719731 | |||
26bea9edb2 | |||
cb7987f841 | |||
98885a32e7 | |||
8d786d4dea | |||
bc18a1b2bb | |||
6b37c952f8 | |||
d81d1ba32a | |||
01c6cd4bdf | |||
729a36ae40 | |||
8805823405 | |||
7dd2634fac | |||
b1cfb9fa59 | |||
550e6f4080 | |||
94116fa173 | |||
0e72668454 | |||
37ceffcb3b | |||
6dd6a33707 | |||
c656f5bce5 | |||
6997232198 | |||
6cb69c627f | |||
80c3af32d5 | |||
9d6bd6db83 | |||
2750750a0c | |||
664ad0ec7d | |||
353117aa74 | |||
c4fe218592 | |||
1c9d8b1c7e | |||
a3716b0158 | |||
752770ec32 | |||
256d6e6c45 | |||
bf69a30163 | |||
ece6b9e354 | |||
abbda7b400 | |||
935e119e64 | |||
da0d3a816d | |||
19d24b54a2 | |||
baa8bc40ec | |||
f453075d20 | |||
174f66a397 | |||
51e52e69a4 | |||
46af675f3c | |||
96777f1bea | |||
668e871e4f | |||
b2f7c83155 | |||
af809183c8 | |||
e66e9b8dca | |||
4c002bc259 | |||
90f95672aa | |||
7f3bf95b46 | |||
9f5e5d25ec | |||
62c0bb77fe | |||
7f4a036a32 | |||
abbd6a49d6 | |||
395f24f22c | |||
f36400dbb9 | |||
ee7977fe4a | |||
900412b317 | |||
249b9cbab9 | |||
8e2b3b4e2a | |||
f5d5529c17 | |||
d2dba2f7b8 | |||
3af254d9e8 | |||
23e661cef0 | |||
2fcbb1ead7 | |||
53baeb59c9 | |||
99a559fe6d | |||
ef463b845d | |||
a6a0da72b2 | |||
bf0fa3f41d | |||
66e8b29014 | |||
c83b883673 | |||
ac233e432f | |||
88f1007a74 | |||
9e1a024a12 | |||
724c52b777 | |||
f7444892d0 | |||
ae4ef9d14d | |||
b5470f2cef | |||
e16a4212cc | |||
6cbcd4aa56 | |||
9b2abecac3 | |||
41621c6a64 | |||
2689c35db3 | |||
dd063feae9 | |||
cf5a5f5e49 | |||
5bf2e03c9f | |||
5dadc3fe98 | |||
7ae7ffaef4 | |||
4562e921e9 | |||
4df25575e8 | |||
dec2942996 | |||
4d5f229fc7 | |||
725e6b8c92 | |||
8e0df948d5 | |||
cad2bfe7da | |||
fb041f77eb | |||
6b5acc0d57 | |||
6ac8e025b0 | |||
e93ce07d88 | |||
c52fd0802e | |||
bdf40d952e | |||
7afaa951d3 | |||
e8ab3a0aa5 | |||
191244ed40 | |||
5273b58afb | |||
2c81007c0a | |||
9ff8a7721a | |||
d7c0d304e3 | |||
fa97286dc8 | |||
812250e941 | |||
a0b5a08f0d | |||
377c78d6b8 | |||
ce18edaa39 | |||
95405283b9 | |||
dd8fea30f2 | |||
794f159a89 | |||
03b06bb2da | |||
aade1e80d7 | |||
0e69a0accc | |||
7d007f721a | |||
b14a28f1c8 | |||
b811eedb33 | |||
b0e69ee64b | |||
b1c4e570cf | |||
68b1e15e01 | |||
b2e1a460c8 | |||
9e801fa9eb | |||
7f35292f64 | |||
7302f99753 | |||
8b746bb9ff | |||
6f6031fb1e | |||
789b4a55d6 | |||
58a428e6fa | |||
9f0e03880b | |||
962ae2b8df | |||
fe797cc66f | |||
69870e7972 | |||
4b77e2e63d | |||
a96fc062f2 | |||
0c38155c44 | |||
f59ffa581c | |||
2d67f93d88 | |||
dd145bb536 | |||
d3cafe08aa | |||
195bb5e4ab | |||
f98e3bc9c2 | |||
9da9bb6c59 | |||
5a3450f3c0 | |||
903de5e33f | |||
4b214d0b8c | |||
736d3930df | |||
460f59d889 | |||
de83d7490b | |||
aaddfd0eef | |||
761afb6f2b | |||
50371267c1 | |||
7e3492c4d1 | |||
c43f41d958 | |||
102aa981ce | |||
50cc050adf | |||
4582c955b8 | |||
857aa7af72 | |||
2fe15778cb | |||
97fef07010 | |||
69aec7ba6a | |||
070ba47de2 | |||
a0f4940a09 | |||
44044d8510 | |||
b3b12638ad | |||
db5d7e18ad | |||
0d1b31f9f0 | |||
30e7c8de42 | |||
8e2542f9a5 | |||
ab07fa6bcf | |||
0edfba604a | |||
eb9eeff4ed | |||
ca68c2589a | |||
9d507d82df | |||
7445d8807c | |||
86046d6e92 | |||
fa5703cb79 | |||
c7986328f7 | |||
8a62c3f6ee | |||
c9f147d805 | |||
af858dcc43 | |||
2b5047db2d | |||
d715038a88 | |||
0446d39190 | |||
c021217811 | |||
af5175c4e1 | |||
f7bbb122e6 | |||
5ca58f1883 | |||
789777a270 | |||
dae7f3c47a | |||
96a99c9df1 | |||
65bd32dfad | |||
283ef9a714 | |||
71bcd192b3 | |||
7cdfdab1fc | |||
dd54eae8d7 | |||
9092445d0b | |||
5ef2b9a685 | |||
85b46034e3 | |||
1a8861846b | |||
81b170cabf | |||
2670b64d60 |
21
.cz.yaml
Normal file
21
.cz.yaml
Normal file
@ -0,0 +1,21 @@
|
||||
---
|
||||
commitizen:
|
||||
customize:
|
||||
change_type_map:
|
||||
feature: Features
|
||||
fix: Fixes
|
||||
refactor: Refactoring
|
||||
test: Tests
|
||||
change_type_order:
|
||||
- BREAKING CHANGE
|
||||
- feat
|
||||
- fix
|
||||
- test
|
||||
- refactor
|
||||
commit_parser: ^(?P<change_type>feat|fix|test|refactor|perf|BREAKING CHANGE)(?:\((?P<scope>[^()\r\n]*)\)|\()?(?P<breaking>!)?:\s(?P<message>.*)?
|
||||
name: cz_customize
|
||||
prerelease_offset: 1
|
||||
tag_format: $version
|
||||
update_changelog_on_bump: false
|
||||
version: 1.0.0-b14
|
||||
version_scheme: semver
|
12
.dockerignore
Normal file
12
.dockerignore
Normal file
@ -0,0 +1,12 @@
|
||||
.git
|
||||
.git*
|
||||
website-template/
|
||||
gitlab-ci/
|
||||
venv/
|
||||
docs/
|
||||
**/*.sqlite3
|
||||
**/static/
|
||||
__pycache__
|
||||
**__pycache__
|
||||
**.pyc
|
||||
** .pytest*
|
31
.github/workflows/bump.yaml
vendored
Normal file
31
.github/workflows/bump.yaml
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
---
|
||||
|
||||
name: 'Bump'
|
||||
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
CZ_PRE_RELEASE:
|
||||
default: none
|
||||
required: false
|
||||
description: Create Pre-Release {alpha,beta,rc,none}
|
||||
CZ_INCREMENT:
|
||||
default: none
|
||||
required: false
|
||||
description: Type of bump to conduct {MAJOR,MINOR,PATCH,none}
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
|
||||
|
||||
jobs:
|
||||
|
||||
bump:
|
||||
name: 'Bump'
|
||||
uses: nofusscomputing/action_bump/.github/workflows/bump.yaml@development
|
||||
with:
|
||||
CZ_PRE_RELEASE: ${{ inputs.CZ_PRE_RELEASE }}
|
||||
CZ_INCREMENT: ${{ inputs.CZ_INCREMENT }}
|
||||
secrets:
|
||||
WORKFLOW_TOKEN: ${{ secrets.WORKFLOW_TOKEN }}
|
109
.github/workflows/ci.yaml
vendored
Normal file
109
.github/workflows/ci.yaml
vendored
Normal file
@ -0,0 +1,109 @@
|
||||
---
|
||||
|
||||
name: 'CI'
|
||||
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
env:
|
||||
GIT_SYNC_URL: "https://${{ secrets.GITLAB_USERNAME_ROBOT }}:${{ secrets.GITLAB_TOKEN_ROBOT }}@gitlab.com/nofusscomputing/projects/centurion_erp.git"
|
||||
|
||||
jobs:
|
||||
|
||||
|
||||
docker:
|
||||
name: 'Docker'
|
||||
uses: nofusscomputing/action_docker/.github/workflows/docker.yaml@development
|
||||
with:
|
||||
DOCKER_BUILD_IMAGE_NAME: "nofusscomputing/centurion-erp"
|
||||
DOCKER_PUBLISH_REGISTRY: "docker.io"
|
||||
DOCKER_PUBLISH_IMAGE_NAME: "nofusscomputing/centurion-erp"
|
||||
secrets:
|
||||
DOCKER_PUBLISH_USERNAME: ${{ secrets.NFC_DOCKERHUB_USERNAME }}
|
||||
DOCKER_PUBLISH_PASSWORD: ${{ secrets.NFC_DOCKERHUB_TOKEN }}
|
||||
|
||||
|
||||
python:
|
||||
name: 'Python'
|
||||
uses: nofusscomputing/action_python/.github/workflows/python.yaml@development
|
||||
secrets:
|
||||
WORKFLOW_TOKEN: ${{ secrets.WORKFLOW_TOKEN }}
|
||||
|
||||
|
||||
gitlab-mirror:
|
||||
if: ${{ github.repository == 'nofusscomputing/centurion_erp' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
|
||||
- name: Checks
|
||||
shell: bash
|
||||
run: |
|
||||
if [ "0${{ env.GIT_SYNC_URL }}" == "0" ]; then
|
||||
|
||||
echo "[ERROR] you must define variable GIT_SYNC_URL for mirroring this repository.";
|
||||
|
||||
exit 1;
|
||||
|
||||
fi
|
||||
|
||||
|
||||
- name: clone
|
||||
shell: bash
|
||||
run: |
|
||||
|
||||
git clone --mirror https://github.com/${{ github.repository }} repo;
|
||||
|
||||
ls -la repo/
|
||||
|
||||
|
||||
- name: add remote
|
||||
shell: bash
|
||||
run: |
|
||||
|
||||
cd repo;
|
||||
|
||||
echo "**************************************** - git remote -v";
|
||||
|
||||
git remote -v;
|
||||
|
||||
echo "****************************************";
|
||||
|
||||
git remote add destination $GIT_SYNC_URL;
|
||||
|
||||
|
||||
- name: push branches
|
||||
shell: bash
|
||||
run: |
|
||||
|
||||
cd repo;
|
||||
|
||||
echo "**************************************** - git branch";
|
||||
|
||||
git branch;
|
||||
|
||||
echo "****************************************";
|
||||
|
||||
# git push destination --all --force;
|
||||
|
||||
git push destination --mirror || true;
|
||||
|
||||
|
||||
# - name: push tags
|
||||
# shell: bash
|
||||
# run: |
|
||||
|
||||
# cd repo;
|
||||
|
||||
# echo "**************************************** - git tag";
|
||||
|
||||
# git tag;
|
||||
|
||||
# echo "****************************************";
|
||||
|
||||
# git push destination --tags --force;
|
14
.github/workflows/pull-requests.yaml
vendored
Normal file
14
.github/workflows/pull-requests.yaml
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
---
|
||||
|
||||
name: Pull Requests
|
||||
|
||||
|
||||
on:
|
||||
pull_request: {}
|
||||
|
||||
|
||||
jobs:
|
||||
|
||||
pull-request:
|
||||
name: pull-request
|
||||
uses: nofusscomputing/action_pull_requests/.github/workflows/pull-requests.yaml@development
|
37
.github/workflows/triage.yaml
vendored
Normal file
37
.github/workflows/triage.yaml
vendored
Normal file
@ -0,0 +1,37 @@
|
||||
|
||||
---
|
||||
|
||||
name: Triage
|
||||
|
||||
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- transferred
|
||||
- milestoned
|
||||
- demilestoned
|
||||
- closed
|
||||
- assigned
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- edited
|
||||
- assigned
|
||||
- reopened
|
||||
- closed
|
||||
|
||||
|
||||
|
||||
jobs:
|
||||
|
||||
|
||||
project:
|
||||
name: Project
|
||||
uses: nofusscomputing/action_project/.github/workflows/project.yaml@development
|
||||
with:
|
||||
PROJECT_URL: https://github.com/orgs/nofusscomputing/projects/3
|
||||
secrets:
|
||||
WORKFLOW_TOKEN: ${{ secrets.WORKFLOW_TOKEN }}
|
||||
|
10
.gitignore
vendored
10
.gitignore
vendored
@ -1 +1,11 @@
|
||||
venv/**
|
||||
*/static/**
|
||||
__pycache__
|
||||
**.sqlite3
|
||||
**.sqlite
|
||||
**.coverage
|
||||
artifacts/
|
||||
**.tmp.*
|
||||
volumes/
|
||||
build/
|
||||
pages/
|
||||
|
313
.gitlab-ci.yml
Normal file
313
.gitlab-ci.yml
Normal file
@ -0,0 +1,313 @@
|
||||
---
|
||||
|
||||
variables:
|
||||
MY_PROJECT_ID: "57560288"
|
||||
# GIT_SYNC_URL: "https://$GITHUB_USERNAME_ROBOT:$GITHUB_TOKEN_ROBOT@github.com/NoFussComputing/centurion_erp.git"
|
||||
|
||||
# # Docker Build / Publish
|
||||
# DOCKER_IMAGE_BUILD_TARGET_PLATFORMS: "linux/amd64,linux/arm64"
|
||||
# DOCKER_IMAGE_BUILD_NAME: centurion-erp
|
||||
# DOCKER_IMAGE_BUILD_REGISTRY: $CI_REGISTRY_IMAGE
|
||||
# DOCKER_IMAGE_BUILD_TAG: $CI_COMMIT_SHA
|
||||
|
||||
# # Docker Publish
|
||||
# DOCKER_IMAGE_PUBLISH_NAME: centurion-erp
|
||||
# DOCKER_IMAGE_PUBLISH_REGISTRY: docker.io/nofusscomputing
|
||||
# DOCKER_IMAGE_PUBLISH_URL: https://hub.docker.com/r/nofusscomputing/$DOCKER_IMAGE_PUBLISH_NAME
|
||||
|
||||
# # Extra release commands
|
||||
# MY_COMMAND: ./.gitlab/additional_actions_bump.sh
|
||||
|
||||
# Docs NFC
|
||||
PAGES_ENVIRONMENT_PATH: projects/centurion_erp/
|
||||
|
||||
# RELEASE_ADDITIONAL_ACTIONS_BUMP: ./.gitlab/additional_actions_bump.sh
|
||||
|
||||
|
||||
include:
|
||||
# - local: .gitlab/pytest.gitlab-ci.yml
|
||||
# - local: .gitlab/unit-test.gitlab-ci.yml
|
||||
- project: nofusscomputing/projects/gitlab-ci
|
||||
ref: development
|
||||
file:
|
||||
- .gitlab-ci_common.yaml
|
||||
# - template/automagic.gitlab-ci.yaml
|
||||
- automation/.gitlab-ci-ansible.yaml
|
||||
- template/mkdocs-documentation.gitlab-ci.yaml
|
||||
- lint/ansible.gitlab-ci.yaml
|
||||
|
||||
|
||||
|
||||
# Update Git Submodules:
|
||||
# extends: .ansible_playbook_git_submodule
|
||||
|
||||
|
||||
# Docker Container:
|
||||
# extends: .build_docker_container
|
||||
# resource_group: build
|
||||
# needs: []
|
||||
# script:
|
||||
# - update-binfmts --display
|
||||
# - |
|
||||
|
||||
# echo "[DEBUG] building multiarch/specified arch image";
|
||||
|
||||
# docker buildx build --platform=$DOCKER_IMAGE_BUILD_TARGET_PLATFORMS . \
|
||||
# --label org.opencontainers.image.created="$(date '+%Y-%m-%d %H:%M:%S%:z')" \
|
||||
# --label org.opencontainers.image.documentation="$CI_PROJECT_URL" \
|
||||
# --label org.opencontainers.image.source="$CI_PROJECT_URL" \
|
||||
# --label org.opencontainers.image.revision="$CI_COMMIT_SHA" \
|
||||
# --push \
|
||||
# --build-arg CI_PROJECT_URL=$CI_PROJECT_URL \
|
||||
# --build-arg CI_COMMIT_SHA=$CI_COMMIT_SHA \
|
||||
# --build-arg CI_COMMIT_TAG=$CI_COMMIT_TAG \
|
||||
# --file $DOCKER_DOCKERFILE \
|
||||
# --tag $DOCKER_IMAGE_BUILD_REGISTRY/$DOCKER_IMAGE_BUILD_NAME:$DOCKER_IMAGE_BUILD_TAG;
|
||||
|
||||
# docker buildx imagetools inspect $DOCKER_IMAGE_BUILD_REGISTRY/$DOCKER_IMAGE_BUILD_NAME:$DOCKER_IMAGE_BUILD_TAG;
|
||||
|
||||
# # during docker multi platform build there are >=3 additional unknown images added to gitlab container registry. cleanup
|
||||
|
||||
# DOCKER_MULTI_ARCH_IMAGES=$(docker buildx imagetools inspect "$DOCKER_IMAGE_BUILD_REGISTRY/$DOCKER_IMAGE_BUILD_NAME:$DOCKER_IMAGE_BUILD_TAG" --format "{{ range .Manifest.Manifests }}{{ if ne (print .Platform) \"&{unknown unknown [] }\" }}$DOCKER_IMAGE_BUILD_REGISTRY/$DOCKER_IMAGE_BUILD_NAME:$DOCKER_IMAGE_BUILD_TAG@{{ println .Digest }}{{end}} {{end}}");
|
||||
|
||||
# docker buildx imagetools create $DOCKER_MULTI_ARCH_IMAGES --tag $DOCKER_IMAGE_BUILD_REGISTRY/$DOCKER_IMAGE_BUILD_NAME:$DOCKER_IMAGE_BUILD_TAG;
|
||||
|
||||
# docker buildx imagetools inspect $DOCKER_IMAGE_BUILD_REGISTRY/$DOCKER_IMAGE_BUILD_NAME:$DOCKER_IMAGE_BUILD_TAG;
|
||||
|
||||
# rules: # rules manually synced from docker/publish.gitlab-ci.yaml removing git tag
|
||||
|
||||
# # - if: # condition_master_branch_push
|
||||
# # $CI_COMMIT_BRANCH == "master" &&
|
||||
# # $CI_PIPELINE_SOURCE == "push"
|
||||
# # exists:
|
||||
# # - '{dockerfile,dockerfile.j2}'
|
||||
# # when: always
|
||||
|
||||
# - if:
|
||||
# $CI_COMMIT_AUTHOR =='nfc_bot <helpdesk@nofusscomputing.com>'
|
||||
# &&
|
||||
# $CI_COMMIT_BRANCH == "development"
|
||||
# when: never
|
||||
|
||||
# - if: # condition_not_master_or_dev_push
|
||||
# $CI_COMMIT_BRANCH != "master" &&
|
||||
# $CI_COMMIT_BRANCH != "development" &&
|
||||
# $CI_PIPELINE_SOURCE == "push"
|
||||
# exists:
|
||||
# - '{dockerfile,dockerfile.j2}'
|
||||
# changes:
|
||||
# paths:
|
||||
# - '{dockerfile,dockerfile.j2,includes/**/*}'
|
||||
# compare_to: 'development'
|
||||
# when: always
|
||||
|
||||
# - if: $CI_COMMIT_TAG
|
||||
# exists:
|
||||
# - '{dockerfile,dockerfile.j2}'
|
||||
# when: always
|
||||
|
||||
# - if: # condition_dev_branch_push
|
||||
# (
|
||||
# $CI_COMMIT_BRANCH == "development"
|
||||
# ||
|
||||
# $CI_COMMIT_BRANCH == "master"
|
||||
# )
|
||||
# &&
|
||||
# $CI_PIPELINE_SOURCE == "push"
|
||||
# exists:
|
||||
# - '{dockerfile,dockerfile.j2}'
|
||||
# allow_failure: true
|
||||
# when: on_success
|
||||
|
||||
# - when: never
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# .gitlab_release:
|
||||
# stage: release
|
||||
# image: registry.gitlab.com/gitlab-org/release-cli:latest
|
||||
# before_script:
|
||||
# - if [ "0$JOB_ROOT_DIR" == "0" ]; then ROOT_DIR=gitlab-ci; else ROOT_DIR=$JOB_ROOT_DIR ; fi
|
||||
# - echo "[DEBUG] ROOT_DIR[$ROOT_DIR]"
|
||||
# - mkdir -p "$CI_PROJECT_DIR/artifacts/$CI_JOB_STAGE/$CI_JOB_NAME"
|
||||
# - mkdir -p "$CI_PROJECT_DIR/artifacts/$CI_JOB_STAGE/tests"
|
||||
# - apk update
|
||||
# - apk add git curl
|
||||
# - apk add --update --no-cache python3 && ln -sf python3 /usr/bin/python
|
||||
# - python -m ensurepip && ln -sf pip3 /usr/bin/pip
|
||||
# - pip install --upgrade pip
|
||||
# - pip install -r $ROOT_DIR/gitlab_release/requirements.txt
|
||||
# # - pip install $ROOT_DIR/gitlab_release/python-module/cz_nfc/.
|
||||
# - pip install commitizen --force
|
||||
# - 'CLONE_URL="https://gitlab-ci-token:$GIT_COMMIT_TOKEN@gitlab.com/$CI_PROJECT_PATH.git"'
|
||||
# - echo "[DEBUG] CLONE_URL[$CLONE_URL]"
|
||||
# - git clone -b development $CLONE_URL repo
|
||||
# - cd repo
|
||||
# - git branch
|
||||
# - git config --global user.email "helpdesk@nofusscomputing.com"
|
||||
# - git config --global user.name "nfc_bot"
|
||||
# - git push --set-upstream origin development
|
||||
# - RELEASE_VERSION_CURRENT=$(cz version --project)
|
||||
# script:
|
||||
# - if [ "$CI_COMMIT_BRANCH" == "development" ] ; then RELEASE_CHANGELOG=$(cz bump --changelog --changelog-to-stdout --prerelease beta); else RELEASE_CHANGELOG=$(cz bump --changelog --changelog-to-stdout); fi
|
||||
# - RELEASE_VERSION_NEW=$(cz version --project)
|
||||
# - RELEASE_TAG=$RELEASE_VERSION_NEW
|
||||
# - echo "[DEBUG] RELEASE_VERSION_CURRENT[$RELEASE_VERSION_CURRENT]"
|
||||
# - echo "[DEBUG] RELEASE_CHANGELOG[$RELEASE_CHANGELOG]"
|
||||
# - echo "[DEBUG] RELEASE_VERSION_NEW[$RELEASE_VERSION_NEW]"
|
||||
# - echo "[DEBUG] RELEASE_TAG[$RELEASE_TAG]"
|
||||
# - RELEASE_TAG_SHA1=$(git log -n1 --format=format:"%H")
|
||||
# - echo "[DEBUG] RELEASE_TAG_SHA1[$RELEASE_TAG_SHA1]"
|
||||
|
||||
# - |
|
||||
# if [ "0$RELEASE_VERSION_CURRENT" == "0$RELEASE_VERSION_NEW" ]; then
|
||||
|
||||
# echo "[DEBUG] not running extra actions, no new version";
|
||||
|
||||
# else
|
||||
|
||||
# echo "[DEBUG] Creating new Version Label";
|
||||
|
||||
# echo "----------------------------";
|
||||
|
||||
# echo ${MY_COMMAND};
|
||||
|
||||
# echo "----------------------------";
|
||||
|
||||
# cat ${MY_COMMAND};
|
||||
|
||||
# echo "----------------------------";
|
||||
|
||||
# ${MY_COMMAND};
|
||||
|
||||
# echo "----------------------------";
|
||||
# fi
|
||||
|
||||
# - if [ "0$RELEASE_VERSION_CURRENT" == "0$RELEASE_VERSION_NEW" ]; then echo "[DEBUG] No tag to delete, version was not bumped"; else git tag -d $RELEASE_TAG; fi
|
||||
|
||||
# - if [ "0$RELEASE_VERSION_CURRENT" == "0$RELEASE_VERSION_NEW" ]; then echo "[DEBUG] No push will be conducted, version was not bumped"; else git push; fi
|
||||
# - if [ "0$RELEASE_VERSION_CURRENT" == "0$RELEASE_VERSION_NEW" ]; then echo "[DEBUG] No release will be created, version was not bumped"; else release-cli create --name "Release $RELEASE_TAG" --tag-name "$RELEASE_TAG" --tag-message "$RELEASE_CHANGELOG" --ref "$RELEASE_TAG_SHA1" --description "$RELEASE_CHANGELOG"; fi
|
||||
# - if [ "$CI_COMMIT_BRANCH" == "master" ] ; then git checkout master; fi
|
||||
# - if [ "$CI_COMMIT_BRANCH" == "master" ] ; then git push --set-upstream origin master; fi
|
||||
# - if [ "$CI_COMMIT_BRANCH" == "master" ] ; then git merge --no-ff development; fi
|
||||
# - if [ "$CI_COMMIT_BRANCH" == "master" ] ; then git push origin master; fi
|
||||
# after_script:
|
||||
# - rm -Rf repo
|
||||
# rules:
|
||||
# - if: '$JOB_STOP_GITLAB_RELEASE'
|
||||
# when: never
|
||||
|
||||
# - if: "$CI_COMMIT_AUTHOR =='nfc_bot <helpdesk@nofusscomputing.com>'"
|
||||
# when: never
|
||||
|
||||
# - if: # condition_master_branch_push
|
||||
# $CI_COMMIT_BRANCH == "master" &&
|
||||
# $CI_PIPELINE_SOURCE == "push"
|
||||
# allow_failure: false
|
||||
# when: on_success
|
||||
|
||||
# - if: # condition_dev_branch_push
|
||||
# $CI_COMMIT_BRANCH == "development" &&
|
||||
# $CI_PIPELINE_SOURCE == "push"
|
||||
# when: manual
|
||||
# allow_failure: true
|
||||
|
||||
# # for testing
|
||||
# # - if: '$CI_COMMIT_BRANCH != "master"'
|
||||
# # when: always
|
||||
# # allow_failure: true
|
||||
# - when: never
|
||||
|
||||
# #
|
||||
# # Release
|
||||
# #
|
||||
# Gitlab Release:
|
||||
# extends:
|
||||
# - .gitlab_release
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# Docker.Hub.Branch.Publish:
|
||||
# extends: .publish-docker-hub
|
||||
# needs: [ "Docker Container" ]
|
||||
# resource_group: build
|
||||
# rules: # rules manually synced from docker/publish.gitlab-ci.yaml removing git tag
|
||||
|
||||
# # - if: # condition_master_branch_push
|
||||
# # $CI_COMMIT_BRANCH == "master" &&
|
||||
# # $CI_PIPELINE_SOURCE == "push"
|
||||
# # exists:
|
||||
# # - '{dockerfile,dockerfile.j2}'
|
||||
# # when: always
|
||||
|
||||
# - if:
|
||||
# $CI_COMMIT_AUTHOR =='nfc_bot <helpdesk@nofusscomputing.com>'
|
||||
# &&
|
||||
# $CI_COMMIT_BRANCH == "development"
|
||||
# when: never
|
||||
|
||||
# - if: $CI_COMMIT_TAG
|
||||
# exists:
|
||||
# - '{dockerfile,dockerfile.j2}'
|
||||
# when: always
|
||||
|
||||
# - if: # condition_dev_branch_push
|
||||
# $CI_COMMIT_BRANCH == "development" &&
|
||||
# $CI_PIPELINE_SOURCE == "push"
|
||||
# exists:
|
||||
# - '{dockerfile,dockerfile.j2}'
|
||||
# allow_failure: true
|
||||
# when: on_success
|
||||
|
||||
# - when: never
|
||||
|
||||
|
||||
# Github (Push --mirror):
|
||||
# extends:
|
||||
# - .git_push_mirror
|
||||
# needs: []
|
||||
# rules:
|
||||
# - if: '$JOB_STOP_GIT_PUSH_MIRROR'
|
||||
# when: never
|
||||
|
||||
# - if: $GIT_SYNC_URL == null
|
||||
# when: never
|
||||
|
||||
# - if: # condition_master_or_dev_push
|
||||
# $CI_COMMIT_BRANCH
|
||||
# &&
|
||||
# $CI_PIPELINE_SOURCE == "push"
|
||||
# when: always
|
||||
|
||||
# - when: never
|
||||
|
||||
|
||||
Website.Submodule.Deploy:
|
||||
extends: .submodule_update_trigger
|
||||
variables:
|
||||
SUBMODULE_UPDATE_TRIGGER_PROJECT: nofusscomputing/infrastructure/website
|
||||
environment:
|
||||
url: https://nofusscomputing.com/$PAGES_ENVIRONMENT_PATH
|
||||
name: Documentation
|
||||
rules:
|
||||
- if: # condition_dev_branch_push
|
||||
$CI_COMMIT_BRANCH == "development" &&
|
||||
$CI_PIPELINE_SOURCE == "push"
|
||||
exists:
|
||||
- '{docs/**,pages/**}/*.md'
|
||||
changes:
|
||||
paths:
|
||||
- '{docs/**,pages/**}/*.md'
|
||||
compare_to: 'master'
|
||||
when: always
|
||||
|
||||
- when: never
|
7
.gitlab/additional_actions_bump.sh
Executable file
7
.gitlab/additional_actions_bump.sh
Executable file
@ -0,0 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Create Version label wtihn repo
|
||||
curl \
|
||||
--data "name=v${RELEASE_TAG}&color=#eee600&description=Version%20that%20is%20affected" \
|
||||
--header "PRIVATE-TOKEN: $GIT_COMMIT_TOKEN" \
|
||||
"https://gitlab.com/api/v4/projects/${CI_PROJECT_ID}/labels"
|
37
.gitlab/merge_request_templates/default.md
Normal file
37
.gitlab/merge_request_templates/default.md
Normal file
@ -0,0 +1,37 @@
|
||||
### :books: Summary
|
||||
<!-- your summary here emojis ref: https://github.com/yodamad/gitlab-emoji -->
|
||||
|
||||
|
||||
|
||||
### :link: Links / References
|
||||
<!--
|
||||
|
||||
using a list as any links to other references or links as required. if relevent, describe the link/reference
|
||||
|
||||
Include any issues or related merge requests. Note: dependent MR's also to be added to "Merge request dependencies"
|
||||
|
||||
-->
|
||||
|
||||
|
||||
|
||||
### :construction_worker: Tasks
|
||||
|
||||
- [ ] Add your tasks here if required (delete)
|
||||
|
||||
<!-- dont remove tasks below strike through including the checkbox by enclosing in double tidle '~~' -->
|
||||
|
||||
- [ ] Contains ~"breaking-change" Any Breaking change(s)?
|
||||
|
||||
_Breaking Change must also be notated in the commit that introduces it and in [Conventional Commit Format](https://www.conventionalcommits.org/en/v1.0.0/)._
|
||||
|
||||
- [ ] Release notes updated
|
||||
|
||||
- [ ] ~Documentation Documentation written
|
||||
|
||||
_All features to be documented within the correct section(s). Administration, Development and/or User_
|
||||
|
||||
- [ ] Milestone assigned
|
||||
|
||||
- [ ] [Unit Test(s) Written](https://nofusscomputing.com/projects/centurion_erp/development/testing/)
|
||||
|
||||
_ensure test coverage delta is not less than zero_
|
81
.gitlab/pytest.gitlab-ci.yml
Normal file
81
.gitlab/pytest.gitlab-ci.yml
Normal file
@ -0,0 +1,81 @@
|
||||
|
||||
|
||||
.pytest:
|
||||
stage: test
|
||||
image: python:3.11-alpine3.19
|
||||
needs: []
|
||||
before_script:
|
||||
- pip install -r requirements.txt
|
||||
- pip install -r requirements_test.txt
|
||||
- cd app
|
||||
artifacts:
|
||||
expire_in: "30 days"
|
||||
when: always
|
||||
reports:
|
||||
junit:
|
||||
- artifacts/*.JUnit.xml
|
||||
paths:
|
||||
- artifacts/
|
||||
rules:
|
||||
|
||||
- if: # Occur on merge
|
||||
$CI_COMMIT_BRANCH
|
||||
&&
|
||||
(
|
||||
$CI_PIPELINE_SOURCE == "push"
|
||||
||
|
||||
$CI_PIPELINE_SOURCE == "web"
|
||||
)
|
||||
when: always
|
||||
|
||||
- when: never
|
||||
|
||||
|
||||
Unit:
|
||||
extends: .pytest
|
||||
script:
|
||||
- pytest --cov --cov-report term --cov-report xml:../artifacts/coverage.xml --cov-report html:../artifacts/coverage/ --junit-xml=../artifacts/unit.JUnit.xml **/tests/unit
|
||||
coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/'
|
||||
artifacts:
|
||||
expire_in: "30 days"
|
||||
when: always
|
||||
reports:
|
||||
coverage_report:
|
||||
coverage_format: cobertura
|
||||
path: artifacts/coverage.xml
|
||||
junit:
|
||||
- artifacts/*.JUnit.xml
|
||||
paths:
|
||||
- artifacts/
|
||||
environment:
|
||||
name: Unit Test Coverage Report
|
||||
url: https://nofusscomputing.gitlab.io/-/projects/centurion_erp/-/jobs/${CI_JOB_ID}/artifacts/artifacts/coverage/index.html
|
||||
|
||||
|
||||
UI:
|
||||
extends: .pytest
|
||||
script:
|
||||
- apk update
|
||||
- apk add chromium-chromedriver
|
||||
- pytest --junit-xml=../artifacts/ui.JUnit.xml **/tests/ui
|
||||
artifacts:
|
||||
expire_in: "30 days"
|
||||
when: always
|
||||
reports:
|
||||
junit:
|
||||
- artifacts/*.JUnit.xml
|
||||
paths:
|
||||
- artifacts/
|
||||
rules:
|
||||
- if: # Occur on merge
|
||||
$CI_COMMIT_BRANCH
|
||||
&&
|
||||
(
|
||||
$CI_PIPELINE_SOURCE == "push"
|
||||
||
|
||||
$CI_PIPELINE_SOURCE == "web"
|
||||
)
|
||||
allow_failure: true
|
||||
when: always
|
||||
|
||||
- when: never
|
8
.gitmodules
vendored
Normal file
8
.gitmodules
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
[submodule "gitlab-ci"]
|
||||
path = gitlab-ci
|
||||
url = https://gitlab.com/nofusscomputing/projects/gitlab-ci.git
|
||||
branch = development
|
||||
[submodule "website-template"]
|
||||
path = website-template
|
||||
url = https://gitlab.com/nofusscomputing/infrastructure/website-template.git
|
||||
branch = development
|
10
.nfc_automation.yaml
Normal file
10
.nfc_automation.yaml
Normal file
@ -0,0 +1,10 @@
|
||||
---
|
||||
|
||||
role_git_conf:
|
||||
gitlab:
|
||||
submodule_branch: "development"
|
||||
default_branch: development
|
||||
mr_labels: ~"type::automation" ~"impact::0" ~"priority::0"
|
||||
auto_merge: true
|
||||
merge_request:
|
||||
patch_labels: '~"code review::not started"'
|
11
.vscode/extensions.json
vendored
Normal file
11
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"ms-python.python",
|
||||
"ms-python.debugpy",
|
||||
"njpwerner.autodocstring",
|
||||
"streetsidesoftware.code-spell-checker-australian-english",
|
||||
"streetsidesoftware.code-spell-checker",
|
||||
"qwtel.sqlite-viewer",
|
||||
"jebbs.markdown-extended",
|
||||
]
|
||||
}
|
37
.vscode/launch.json
vendored
Normal file
37
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,37 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Debug: Django",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"args": [
|
||||
"runserver",
|
||||
"0.0.0.0:8002"
|
||||
],
|
||||
"django": true,
|
||||
"autoStartBrowser": false,
|
||||
"program": "${workspaceFolder}/app/manage.py"
|
||||
},
|
||||
{
|
||||
"name": "Debug: Celery",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"module": "celery",
|
||||
"console": "integratedTerminal",
|
||||
"args": [
|
||||
"-A",
|
||||
"app",
|
||||
"worker",
|
||||
"-l",
|
||||
"INFO",
|
||||
"-n",
|
||||
"debug-itsm@%h"
|
||||
],
|
||||
"cwd": "${workspaceFolder}/app"
|
||||
}
|
||||
]
|
||||
}
|
20
.vscode/settings.json
vendored
Normal file
20
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"gitlab.aiAssistedCodeSuggestions.enabled": false,
|
||||
"gitlab.duoChat.enabled": false,
|
||||
"cSpell.enableFiletypes": [
|
||||
"!python"
|
||||
],
|
||||
"python.testing.pytestArgs": [
|
||||
// "-v",
|
||||
// "--cov",
|
||||
// "--cov-report xml",
|
||||
"app"
|
||||
],
|
||||
"python.testing.unittestEnabled": false,
|
||||
"python.testing.pytestEnabled": true,
|
||||
"testing.coverageToolbarEnabled": true,
|
||||
"cSpell.words": [
|
||||
"ITSM"
|
||||
],
|
||||
"cSpell.language": "en-AU",
|
||||
}
|
721
CHANGELOG.md
Normal file
721
CHANGELOG.md
Normal file
@ -0,0 +1,721 @@
|
||||
## 1.0.0-b14 (2024-08-12)
|
||||
|
||||
### Fixes
|
||||
|
||||
- **api**: ensure model_notes is an available field
|
||||
|
||||
### Tests
|
||||
|
||||
- **access**: test field model_notes
|
||||
|
||||
## 1.0.0-b13 (2024-08-11)
|
||||
|
||||
### Fixes
|
||||
|
||||
- Audit models for validations
|
||||
- **itam**: Ensure device name is formatted according to RFC1035 2.3.1
|
||||
- **itam**: Ensure device UUID is correctly formatted
|
||||
- **config_management**: Ensure that config group can't set self as parent
|
||||
- **settings**: ensure that the api token cant be saved to notes field
|
||||
|
||||
### Tests
|
||||
|
||||
- api field checks
|
||||
- **software**: api field checks
|
||||
|
||||
## 1.0.0-b12 (2024-08-10)
|
||||
|
||||
### Fixes
|
||||
|
||||
- **api**: ensure org mixin is inherited by software view
|
||||
- **base**: correct project links to github
|
||||
|
||||
### Tests
|
||||
|
||||
- api field checks
|
||||
|
||||
#128 #162
|
||||
- **teams**: api field checks
|
||||
- **organization**: api field checks
|
||||
|
||||
## 1.0.0-b11 (2024-08-10)
|
||||
|
||||
## 1.0.0-b10 (2024-08-09)
|
||||
|
||||
## 1.0.0-b9 (2024-08-09)
|
||||
|
||||
## 1.0.0-b8 (2024-08-09)
|
||||
|
||||
## 1.0.0-b7 (2024-08-09)
|
||||
|
||||
## 1.0.0-b6 (2024-08-09)
|
||||
|
||||
## 1.0.0-b5 (2024-07-31)
|
||||
|
||||
### feat
|
||||
|
||||
- add Config groups to API
|
||||
- **api**: Add device config groups to devices
|
||||
- **api**: Ability to fetch configgroups from api along with config
|
||||
|
||||
### Fixes
|
||||
|
||||
- **api**: Ensure device groups is read only
|
||||
|
||||
### Tests
|
||||
|
||||
- **api**: Field existence and type checks for device
|
||||
- **api**: test configgroups API fields
|
||||
|
||||
## 1.0.0-b4 (2024-07-29)
|
||||
|
||||
### feat
|
||||
|
||||
- **swagger**: remove `{format}` suffixed doc entries
|
||||
|
||||
### Fixes
|
||||
|
||||
- release-b3 fixes
|
||||
- **api**: cleanup team post/get
|
||||
- **api**: confirm HTTP method is allowed before permission check
|
||||
- **api**: Ensure that organizations can't be created via the API
|
||||
- **access**: Team model class inheritance order corrected
|
||||
|
||||
### Tests
|
||||
|
||||
- confirm that the tenancymanager is called
|
||||
|
||||
## 1.0.0-b3 (2024-07-21)
|
||||
|
||||
### Fixes
|
||||
|
||||
- **itam**: Limit os version count to devices user has access to
|
||||
|
||||
## 1.0.0-b2 (2024-07-19)
|
||||
|
||||
### Fixes
|
||||
|
||||
- **itam**: only show os version once
|
||||
|
||||
## 1.0.0-b1 (2024-07-19)
|
||||
|
||||
### Fixes
|
||||
|
||||
- **itam**: ensure installed operating system count is limited to users organizations
|
||||
- **itam**: ensure installed software count is limited to users organizations
|
||||
|
||||
## 1.0.0-a4 (2024-07-18)
|
||||
|
||||
### feat
|
||||
|
||||
- **api**: When processing uploaded inventory and name does not match, update name to one within inventory file
|
||||
- **config_management**: Group name to be entire breadcrumb
|
||||
|
||||
### Tests
|
||||
|
||||
- ensure inventory upload matches by both serial number and uuid if device name different
|
||||
- placeholder for moving organization
|
||||
|
||||
## 1.0.0-a3 (2024-07-18)
|
||||
|
||||
### feat
|
||||
|
||||
- **config_management**: Prevent a config group from being able to change organization
|
||||
- **itam**: On device organization change remove config groups
|
||||
|
||||
### Fixes
|
||||
|
||||
- **config_management**: dont attempt to do action during save if group being created
|
||||
- **itam**: remove org filter for device so that user can see installations
|
||||
- **itam**: remove org filter for operating systems so that user can see installations
|
||||
- **itam**: remove org filter for software so that user can see installations
|
||||
- **itam**: Device related items should not be global.
|
||||
- **itam**: When changing device organization move related items too.
|
||||
|
||||
## 1.0.0-a2 (2024-07-17)
|
||||
|
||||
### feat
|
||||
|
||||
- **api**: Inventory matching of device second by uuid
|
||||
- **api**: Inventory matching of device first by serial number
|
||||
- **base**: show warning bar if the user has not set a default organization
|
||||
|
||||
### Fixes
|
||||
|
||||
- **base**: dont show user warning bar for non-authenticated user
|
||||
- **api**: correct inventory operating system selection by name
|
||||
- **api**: correct inventory operating system and it's linking to device
|
||||
- **api**: correct inventory device search to be case insensitive
|
||||
|
||||
## 1.0.0-a1 (2024-07-16)
|
||||
|
||||
### BREAKING CHANGE
|
||||
|
||||
- squashed DB migrations in preparation for v1.0 release.
|
||||
|
||||
### feat
|
||||
|
||||
- Administratively set global items org/is_global field now read-only
|
||||
- **access**: Add multi-tennant manager
|
||||
|
||||
### Fixes
|
||||
|
||||
- **core**: migrate manufacturer to use new form/view logic
|
||||
- **settings**: correct the permission to view manufacturers
|
||||
- **access**: Correct team form fields
|
||||
- **config_management**: don't exclude parent from field, only self
|
||||
|
||||
### Refactoring
|
||||
|
||||
- repo preperation for v1.0.0-Alpha-1
|
||||
- Squash database migrations
|
||||
|
||||
### Tests
|
||||
|
||||
- tenancy objects
|
||||
- refactor to single abstract model for inclusion.
|
||||
|
||||
## 0.7.0 (2024-07-14)
|
||||
|
||||
### feat
|
||||
|
||||
- **core**: Filter every form field if associated with an organization to users organizations only
|
||||
- **core**: add var `template_name` to common view template for all views that require it
|
||||
- **core**: add Display view to common forms abstract class
|
||||
- **navigation**: always show every menu for super admin
|
||||
- **core**: only display navigation menu item if use can view model
|
||||
- **django**: update 5.0.6 -> 5.0.7
|
||||
- **core**: add common forms abstract class
|
||||
- **core**: add common views abstract class
|
||||
- add postgreSQL database support
|
||||
- **ui**: add config groups navigation icon
|
||||
- **ui**: add some navigation icons
|
||||
- **itam**: update inventory status icon
|
||||
- **itam**: ensure device software pagination links keep interface on software tab
|
||||
- "Migrate inventory processing to background worker"
|
||||
- **access**: enable non-organization django permission checks
|
||||
- **settings**: Add celery task results index and view page
|
||||
- **base**: Add background worker
|
||||
- **itam**: Update Serial Number from inventory if present and Serial Number not set
|
||||
- **itam**: Update UUID from inventory if present and UUID not set
|
||||
|
||||
### Fixes
|
||||
|
||||
- **config_management**: Don't allow a config group to assign itself as its parent
|
||||
- **config_management**: correct permission for deleting a host from config group
|
||||
- **config_management**: use parent group details to work out permissions when adding a host
|
||||
- **config_management**: use parent group details to work out permissions
|
||||
- **itam**: Add missing permissions to software categories index view
|
||||
- **itam**: Add missing permissions to device types index view
|
||||
- **itam**: Add missing permissions to device model index view
|
||||
- **settings**: Add missing permissions to app settings view
|
||||
- **itam**: Add missing permissions to software index view
|
||||
- **itam**: Add missing permissions to operating system index view
|
||||
- **itam**: Add missing permissions to device index view
|
||||
- **config_management**: Add missing permissions to group views
|
||||
- **navigation**: always show settings menu entry
|
||||
- **itam**: cater for fields that are prefixed
|
||||
- **itam**: Ability to view software category
|
||||
- **itam**: correct view permission
|
||||
- **access**: When adding a new team to org ensure parent model is fetched
|
||||
- **access**: enable org manager to view orgs
|
||||
- **settings**: restrict user visible organizations to ones they are part of
|
||||
- **access**: enable org manager to view orgs
|
||||
- **access**: fetch object if method exists
|
||||
- **docs**: update docs link to new path
|
||||
- **access**: correctly set team user parent model to team
|
||||
- **access**: fallback to django permissions if org permissions check is false
|
||||
- **access**: Correct logic so that org managers can see orgs they manage
|
||||
- **base**: add missing content_title to context
|
||||
- **access**: Enable Organization Manager to view organisations they are assigned to
|
||||
- **api**: correct logic for adding inventory UUID and serial number to device
|
||||
- **ui**: navigation alignment and software icon
|
||||
- **ui**: display organization manager name instead of ID
|
||||
- **access**: ensure name param exists before attempting to access
|
||||
- **itam**: dont show none/nil for device fields containing no value
|
||||
- **itam**: show device model name instead of ID
|
||||
- **api**: Ensure if serial number from inventory is `null` that it's not used
|
||||
- **api**: ensure checked uuid and serial number is used for updating
|
||||
- inventory
|
||||
- **itam**: only remove device software when not found during inventory upload
|
||||
- **itam**: only update software version if different
|
||||
- existing device without uuid not updated when uploading an inventory
|
||||
- Device Software tab pagination does not work
|
||||
- **itam**: correct device software pagination
|
||||
|
||||
### Refactoring
|
||||
|
||||
- adjust views missing add/change form to now use forms
|
||||
- add navigation menu expand arrows
|
||||
- migrate views to use new abstract model view classes
|
||||
- migrate forms to use new abstract model form class
|
||||
- **access**: Rename Team Button "new user" -> "Assign User"
|
||||
- **access**: model pk and name not required context for adding a device
|
||||
- rename field "model notes" -> "Notes"
|
||||
- remove settings model
|
||||
- **ui**: increase indentation to sub-menu items
|
||||
- **itam**: rename old inventory status icon for use with security
|
||||
- **api**: migrate inventory processing to background worker
|
||||
- **itam**: only perform actions on device inventory if DB matches inventory item
|
||||
|
||||
### Tests
|
||||
|
||||
- add test test_view_*_attribute_not_exists_fields for add and change views
|
||||
- fix test_view_change_attribute_type_form_class to test if type class
|
||||
- **views**: add test cases for model views
|
||||
- Add Test case abstract classes to models
|
||||
- **inventory**: add mocks?? for calling background worker
|
||||
- **view**: view permission checks
|
||||
- **inventory**: update tests for background worker changes
|
||||
|
||||
## 0.6.0 (2024-06-30)
|
||||
|
||||
### feat
|
||||
|
||||
- user api token
|
||||
- **api**: API token authentication
|
||||
- **api**: abilty for user to create/delete api token
|
||||
- **api**: create token model
|
||||
|
||||
### Fixes
|
||||
|
||||
- **user_token**: conduct user check on token view access
|
||||
- **itam**: use same form for edit and add
|
||||
- **itam**: dont add field inventorydate if adding new item
|
||||
- **api**: inventory upload requires sanitization
|
||||
|
||||
### Refactoring
|
||||
|
||||
- **settings**: use seperate change/view views
|
||||
- **settings**: use form for user settings
|
||||
- **tests**: move unit tests to unit test sub-directory
|
||||
|
||||
### Tests
|
||||
|
||||
- **token_auth**: test authentication method token
|
||||
- more tests
|
||||
- add .coveragerc to remove non-code files from coverage report
|
||||
- Unit Tests TenancyObjects
|
||||
- Test Cases for TenancyObjects
|
||||
- tests for checking links from rendered templetes
|
||||
- **core**: test cases for notes permissions
|
||||
- **config_management**: config groups history permissions
|
||||
- **api**: Majority of Inventory upload tests
|
||||
- **access**: TenancyObject field tests
|
||||
- **access**: remove skipped api tests for team users
|
||||
|
||||
## 0.5.0 (2024-06-17)
|
||||
|
||||
### feat
|
||||
|
||||
- Setup Organization Managers
|
||||
- **access**: add notes field to organization
|
||||
- **access**: add organization manger
|
||||
- **config_management**: Use breadcrumbs for child group name display
|
||||
- **config_management**: ability to add host to global group
|
||||
- **itam**: add a status of "bad" for devices
|
||||
- **itam**: paginate device software tab
|
||||
- **itam**: status of device visible on device index page
|
||||
- API Browser
|
||||
- **core**: add skeleton http browser
|
||||
- **core**: Add a notes field to manufacturer/ publisher
|
||||
- **itam**: Add a notes field to software category
|
||||
- **itam**: Add a notes field to device types
|
||||
- **itam**: Add a notes field to device models
|
||||
- **itam**: Add a notes field to software
|
||||
- **itam**: Add a notes field to operating system
|
||||
- **itam**: Add a notes field to devices
|
||||
- **access**: Add a notes field to teams
|
||||
- **base**: Add a notes field to `TenancyObjetcs` class
|
||||
- **settings**: add docs icon to application settings page
|
||||
- **itam**: add docs icon to software page
|
||||
- **itam**: add docs icon to operating system page
|
||||
- **itam**: add docs icon to devices page
|
||||
- **config_management**: add docs icon to config groups page
|
||||
- **base**: add dynamic docs icon
|
||||
- config group software
|
||||
- **models**: add property parent_object to models that have a parent
|
||||
- **config_management**: add config group software to group history
|
||||
- **itam**: render group software config within device rendered config
|
||||
- **config_management**: assign software action to config group
|
||||
- sso
|
||||
- add configuration value 'SESSION_COOKIE_AGE'
|
||||
- remove development SECRET_KEY and enforce checking for user configured one
|
||||
- **base**: build CSRF trusted origins from configuration
|
||||
- **base**: Enforceable SSO ONLY
|
||||
- **base**: configurable SSO
|
||||
|
||||
### Fixes
|
||||
|
||||
- **itam**: remove requirement that user needs change device to add notes
|
||||
- **core**: dont attempt to access parent_object if 'None' during history save
|
||||
- **config_management**: Add missing parent item getter to model
|
||||
- **core**: overridden save within SaveHistory to use default attributes
|
||||
- **access**: overridden save to use default attributes
|
||||
- History does not delete when item deleted
|
||||
- **core**: on object delete remove history entries
|
||||
- inventory upload cant determin object organization
|
||||
- **api**: ensure proper permission checking
|
||||
- dont throw an exception during settings load for an item django already checks
|
||||
- **core**: Add overrides for delete so delete history saved for items with parent model
|
||||
- **config_management**: correct delete success url
|
||||
- **base**: remove social auth from nav menu
|
||||
- **access**: add a team user permissions to use team organization
|
||||
|
||||
### Refactoring
|
||||
|
||||
- **access**: relocate permission check to own function
|
||||
- **itam**: move device os tab to details tab
|
||||
- **itam**: add device change form and adjust view to be non-form
|
||||
- **itam**: migrate device vie to use manual entered fields in two columns
|
||||
- **access**: migrate team users view to use forms
|
||||
- **access**: migrate teams view to use forms
|
||||
- **access**: migrate organization view to use form
|
||||
- **base**: cleanup form and prettyfy
|
||||
- **config_management**: relocate groups views to own directory
|
||||
- login to use base template
|
||||
- adjust template block names
|
||||
|
||||
### Tests
|
||||
|
||||
- **access**: team user model permission check for organization manager
|
||||
- **access**: team model permission check for organization manager
|
||||
- **access**: organization model permission check for organization manager
|
||||
- **access**: add test cases for model delete as organization manager
|
||||
- **access**: add test cases for model addd as organization manager
|
||||
- **access**: add test cases for model change as organization manager
|
||||
- **access**: add test cases for model view as organization manager
|
||||
- write some more
|
||||
- **core**: skip invalid tests
|
||||
- **itam**: tests for device type history entries
|
||||
- **core**: tests for manufacturer history entries
|
||||
- move manufacturer to it's parent
|
||||
- refactor api model permission tests to use an abstract class of test cases
|
||||
- move tests to the module they belong to
|
||||
- refactor history permission tests to use an abstract class of test cases
|
||||
- refactor model permission tests to use an abstract class of test cases
|
||||
- refactor history entry to have test cases in abstract classes
|
||||
- **itam**: history entry tests for software category
|
||||
- **itam**: history entry tests for device operating system version
|
||||
- **itam**: history entry tests for device operating system
|
||||
- **itam**: history entry tests for device software
|
||||
- **itam**: ensure child history is removed on config group software delete
|
||||
- add placeholder tests
|
||||
- **itam**: ensure history is removed on software delete
|
||||
- **itam**: ensure history is removed on operating system delete
|
||||
- **itam**: ensure history is removed on device model delete
|
||||
- **config_management**: test history on delete for config groups
|
||||
- **itam**: ensure history is removed on device delete
|
||||
- **access**: test team history
|
||||
- **access**: ensure team user history is created and removed as required
|
||||
- **access**: ensure history is removed on team delete
|
||||
- **access**: ensure history is removed on item delete
|
||||
- **api**: Inventory upload permission checks
|
||||
- **config_management**: testing of config_groups rendered config
|
||||
- **config_management**: history save tests for config groups software
|
||||
- **config_management**: config group software permission for add, change and delete
|
||||
- **base**: placeholder tests for config groups software
|
||||
- **base**: basic test for merge_software helper
|
||||
- during unit tests add SECRET_KEY
|
||||
|
||||
## 0.4.0 (2024-06-05)
|
||||
|
||||
### feat
|
||||
|
||||
- 2024 06 05
|
||||
- **database**: add mysql support
|
||||
- **api**: move invneotry api endpoint to '/api/device/inventory'
|
||||
- **core**: support more history types
|
||||
- **core**: function to fetch history entry item
|
||||
- 2024 06 02
|
||||
- **config_management**: Add button to groups ui for adding child group
|
||||
- **access**: throw error if no organization added
|
||||
- **itam**: add delete button to config group within ui
|
||||
- **itam**: Config groups rendered configuration now part of devices rendered configuration
|
||||
- **config_management**: Ability to delete a host from a config group
|
||||
- **config_management**: Ability to add a host to a config group
|
||||
- **config_management**: ensure config doesn't use reserved config keys
|
||||
- **config_management**: Config groups rendered config
|
||||
- **config_management**: add configuration groups
|
||||
- **api**: add swagger ui for documentation
|
||||
- **api**: filter software to users organizations
|
||||
- **api**: filter devices to users organizations
|
||||
- randomz
|
||||
- **api**: add org team view page
|
||||
- API configuration of permissions
|
||||
- **api**: configure team permissions
|
||||
|
||||
### Fixes
|
||||
|
||||
- **itam**: ensure device type saves history
|
||||
- **core**: correct history view permissions
|
||||
- **config_management**: set config dict keys to be valid ansible variables
|
||||
- **itam**: correct logic for device add dynamic success url
|
||||
- **itam**: correct config group link for device
|
||||
- **config_management**: correct model permissions
|
||||
- **config_management**: add config management to navigation
|
||||
- **ui**: remove api entries from navigation
|
||||
- **api**: check for org must by by type None
|
||||
- **api**: correct software permissions
|
||||
- **api**: corrct device permissions
|
||||
- **api**: permissions for teams
|
||||
- **api**: correct reverse url lookup to use NS API
|
||||
- **api**: permissions for organization
|
||||
|
||||
### Refactoring
|
||||
|
||||
- **access**: cache object so it doesnt have to be called multiple times
|
||||
- **config_management**: move groups to nav menu
|
||||
- **api**: migrate devices and software to viewsets
|
||||
- **api**: move permission check to mixin
|
||||
- **access**: add team option to org permission check
|
||||
|
||||
### Tests
|
||||
|
||||
- **api**: placeholder test for inventory
|
||||
- **settings**: access permission check for app settings
|
||||
- **settings**: history view permission check for software category
|
||||
- **settings**: history view permission check for manufacturer
|
||||
- **settings**: history view permission check for device type
|
||||
- **settings**: user settings
|
||||
- **settings**: view permission check for user settings
|
||||
- refactor core test layout
|
||||
- **itam**: view permission check for software
|
||||
- **itam**: view permission check for operating system
|
||||
- **itam**: view permission check for device model
|
||||
- **itam**: view permission check for device
|
||||
- **config_management**: view permission check for config_groups
|
||||
- **access**: view permission check for team
|
||||
- **access**: view permission check for organization
|
||||
- add history entry creation tests for most models
|
||||
- **config_management**: when adding a host to config group filter out host that are already members of the group
|
||||
- **config_management**: unit test for config groups model to ensure permissions are working
|
||||
- **api**: remove tests for os and manufacturer as they are not used in api
|
||||
- **api**: check model permissions for software
|
||||
- **api**: check model permissions for devices
|
||||
- **api**: check model permissions for teams
|
||||
- **api**: check model permissions for organizations
|
||||
|
||||
## 0.3.0 (2024-05-29)
|
||||
|
||||
### feat
|
||||
|
||||
- Randomz
|
||||
- **access**: during organization permission check, check to ensure user is logged on
|
||||
- **history**: always create an entry even if user=none
|
||||
- **itam**: device uuid must be unique
|
||||
- **itam**: device serial number must be unique
|
||||
- 2024 05 26
|
||||
- **setting**: Enable super admin to set ALL manufacturer/publishers as global
|
||||
- **setting**: Enable super admin to set ALL device types as global
|
||||
- **setting**: Enable super admin to set ALL device models as global
|
||||
- **setting**: Enable super admin to set ALL software categories as global
|
||||
- **UI**: show build details with page footer
|
||||
- **software**: Add output to stdout to show what is and has occurred
|
||||
- 2024 05 25
|
||||
- **base**: Add delete icon to content header
|
||||
- **itam**: Populate initial organization value from user default organization for software category creation
|
||||
- **itam**: Populate initial organization value from user default organization for device type creation
|
||||
- **itam**: Populate initial organization value from user default organization for device model creation
|
||||
- **api**: Populate initial organization value from user default organization inventory
|
||||
- **itam**: Populate initial organization value from user default organization for Software creation
|
||||
- **itam**: Populate initial organization value from user default organization for operating system creation
|
||||
- **device**: Populate initial organization value from user default organization
|
||||
- Add management command software
|
||||
- **setting**: Enable super admin to set ALL software as global
|
||||
- **user**: Add user settings panel
|
||||
- Manufacturer and Model Information
|
||||
- **itam**: Add publisher to software
|
||||
- **itam**: Add publisher to operating system
|
||||
- **itam**: Add device model
|
||||
- **core**: Add manufacturers
|
||||
- **settings**: add dummy model for permissions
|
||||
- **settings**: new module for whole of application settings/globals
|
||||
- 2024 05 21-23
|
||||
- **access**: Save changes to history for organization and teams
|
||||
- **software**: Save changes to history
|
||||
- **operating_system**: Save changes to history
|
||||
- **device**: Save changes to history
|
||||
- **core**: history model for saving model history
|
||||
- 2024 05 19/20
|
||||
- **itam**: Ability to add notes to software
|
||||
- **itam**: Ability to add notes to operating systems
|
||||
- **itam**: Ability to add notes on devices
|
||||
- **core**: notes model added to core
|
||||
- **device**: Record inventory date and show as part of details
|
||||
- **ui**: Show inventory details if they exist
|
||||
- **api**: API accept computer inventory
|
||||
|
||||
### Fixes
|
||||
|
||||
- **settings**: Add correct permissions for team user delete
|
||||
- **settings**: Add correct permissions for team user view/change
|
||||
- **settings**: Add correct permissions for team view/change
|
||||
- **settings**: Add correct permissions for team add
|
||||
- **settings**: Add correct permissions for team delete
|
||||
- **access**: correct back link within team view
|
||||
- **access**: correct url name to be within naming conventions
|
||||
- **settings**: Add correct permissions for manufacturer / publisher delete
|
||||
- **settings**: Add correct permissions for manufacturer / publisher add
|
||||
- **settings**: Add correct permissions for manufacturer / publisher view/update
|
||||
- **settings**: Add correct permissions for software category delete
|
||||
- **settings**: Add correct permissions for software category add
|
||||
- **settings**: Add correct permissions for software category view/update
|
||||
- **settings**: Add correct permissions for device type delete
|
||||
- **settings**: Add correct permissions for device type add
|
||||
- **settings**: Add correct permissions for device type view/update
|
||||
- **settings**: Add correct permissions for device model delete
|
||||
- **settings**: Add correct permissions for device model add
|
||||
- **settings**: Add correct permissions for device model view/update
|
||||
- **access**: Add correct permissions for organization view/update
|
||||
- **access**: use established view naming
|
||||
- **itam**: Add correct permissions for operating system delete
|
||||
- **itam**: Add correct permissions for operating system add
|
||||
- **itam**: Add correct permissions for operating system view/update
|
||||
- **itam**: Add correct permissions for software delete
|
||||
- **itam**: Add correct permissions for software add
|
||||
- **itam**: for non-admin user use correct order by fields for software view/update
|
||||
- **itam**: Add correct permissions for software view/update
|
||||
- **itam**: ensure permission_required parameter for view is a list
|
||||
- **core**: dont save history when no user information available
|
||||
- **access**: during organization permission check, check the entire list of permissions
|
||||
- **core**: dont save history for anonymous user
|
||||
- **access**: during permission check use post request params for an add action
|
||||
- **user**: on new-user signal create settings row if not exist
|
||||
- **itam**: ensure only user with change permission can change a device
|
||||
- **user**: if user settings row doesn't exist on access create
|
||||
- **access**: adding/deleting team group actions moved to model save/delete method override
|
||||
- **api**: add teams and permissions to org and teams respectively
|
||||
- **ui**: correct repo url used
|
||||
- **api**: device inventory date set to read only
|
||||
- **software**: ensure management command query correct for migration
|
||||
- **device**: OS form trying to add last inventory date when empty
|
||||
- add static files path to urls
|
||||
- **inventory**: Dont select device_type, use 'null'
|
||||
- **base**: show "content_title - SITE_TITLE" as site title
|
||||
- **device**: Read Only field set as required=false
|
||||
- correct typo in notes templates
|
||||
- **ui**: Ensure navigation menu entry highlighted for sub items
|
||||
|
||||
### Refactoring
|
||||
|
||||
- **access**: add to models a get_organization function
|
||||
- **access**: remove change view
|
||||
- **itam**: relocation item delete from list to inside device
|
||||
- **context_processor**: relocate as base
|
||||
- **itam**: software index does not require created and modified date
|
||||
- **organizations**: set org field to null if not set
|
||||
- **itam**: move software categories to settings app
|
||||
- **itam**: move device types to settings app
|
||||
- **template**: content_title can be rendered in base
|
||||
|
||||
### Tests
|
||||
|
||||
- cleanup duplicate tests and minor reshuffle
|
||||
- **access**: unit testing team user permissions
|
||||
- **access**: unit testing team permissions
|
||||
- **settings**: unit testing manufacturer permissions
|
||||
- **settings**: unit testing software category permissions
|
||||
- **device_model**: unit testing device type permissions
|
||||
- **device_model**: unit testing device model permissions
|
||||
- **organization**: unit testing organization permissions
|
||||
- **operating_system**: unit testing operating system permissions
|
||||
- **software**: unit testing software permissions
|
||||
- **device**: unit testing device permissions
|
||||
- adjust test layout and update contributing
|
||||
- **core**: placeholder tests for history component
|
||||
- **core**: place holder tests for notes model
|
||||
- **api**: add placeholder tests for inventory
|
||||
|
||||
## 0.2.0 (2024-05-18)
|
||||
|
||||
### feat
|
||||
|
||||
- 2024 05 18
|
||||
- **itam**: Add Operating System to ITAM models
|
||||
- **api**: force content type to be JSON for req/resp
|
||||
- **software**: view software
|
||||
- 2024 05 17
|
||||
- **device**: Prevent devices from being set global
|
||||
- **software**: if no installations found, denote
|
||||
- **device**: configurable software version
|
||||
- **software_version**: name does not need to be unique
|
||||
- **software_version**: set is_global to match software
|
||||
- **software**: prettify device software action
|
||||
- **software**: ability to add software versions
|
||||
- **base**: add stylised action button/text
|
||||
- **software**: add pagination for index
|
||||
- **device**: add pagination for index
|
||||
|
||||
### Fixes
|
||||
|
||||
- **device**: correct software link
|
||||
|
||||
## 0.1.0 (2024-05-17)
|
||||
|
||||
### feat
|
||||
|
||||
- API token auth
|
||||
- **api**: initial token authentication implementation
|
||||
- itam and API setup
|
||||
- **docker**: add settings to store data in separate volume
|
||||
- **django**: add split settings for specifying additional settings paths
|
||||
- **api**: Add device config to device
|
||||
- **itam**: add organization to device installs
|
||||
- **itam**: migrate app from own repo
|
||||
- Enable API by default
|
||||
- Genesis
|
||||
- **admin**: remove team management
|
||||
- **admin**: remove group management
|
||||
- **access**: adjustable team permissions
|
||||
- **api**: initial work on API
|
||||
- **template**: add header content icon block
|
||||
- **tenancy**: Add is_ global field
|
||||
- **access**: when modifying a team ad/remove user from linked group
|
||||
- **auth**: include python social auth django application
|
||||
- Build docker container for release
|
||||
- **access**: add permissions to team and user
|
||||
- **style**: format check boxes
|
||||
- **access**: delete team user form
|
||||
- **view**: new user
|
||||
- user who is 'is_superuser' to view everything and not be denied access
|
||||
- **access**: add org mixin to current views
|
||||
- **access**: add views for each action for teams
|
||||
- **access**: add mixin to check organization permissions against user and object
|
||||
- **account**: show admin site link if user is staff
|
||||
- **development**: added the debug django app
|
||||
- **access**: rename structure to access and remove organization app in favour of own implementation
|
||||
- **account**: Add user password change form
|
||||
- **urls**: provide option to exclude navigation items
|
||||
- **structure**: unregister admin pages from organization app not required
|
||||
- **auth**: Custom Login Page
|
||||
- **auth**: Add User Account Menu
|
||||
- **auth**: Setup Login required
|
||||
- Dyno-magic build navigation from application urls.py
|
||||
- **structure**: Select and View an individual Organization
|
||||
- **structure**: View Organizations
|
||||
- **app**: Add new app structure for organizations and teams
|
||||
- **template**: add base template
|
||||
- **django**: add organizations app
|
||||
|
||||
### Fixes
|
||||
|
||||
- **itam**: device software to come from device org or global not users orgs
|
||||
- **access**: correct team required permissions
|
||||
- **fields**: correct autoslug field so it works
|
||||
- **docker**: build wheels then install
|
||||
|
||||
### Refactoring
|
||||
|
||||
- button to use same selection colour
|
||||
- **access**: remove inline form for org teams
|
||||
- rename app from itsm -> app
|
||||
- **access**: dont use inline formset
|
||||
- **views**: move views to own directory
|
||||
- **access**: addjust org and teams to use different view per action
|
||||
|
||||
### Tests
|
||||
|
||||
- interim unit tests
|
||||
|
||||
## 0.0.1 (2024-05-06)
|
61
CONTRIBUTING.md
Normal file
61
CONTRIBUTING.md
Normal file
@ -0,0 +1,61 @@
|
||||
# Contribution Guide
|
||||
|
||||
|
||||
## Dev Environment
|
||||
|
||||
It's advised to setup a python virtual env for development. this can be done with the following commands.
|
||||
|
||||
``` bash
|
||||
|
||||
python3 -m venv venv
|
||||
|
||||
source venv/bin/activate
|
||||
|
||||
pip install -r requirements.txt
|
||||
|
||||
```
|
||||
|
||||
To setup the centurion erp test server run the following
|
||||
|
||||
``` bash
|
||||
|
||||
cd app
|
||||
|
||||
python manage.py runserver 8002
|
||||
|
||||
python3 manage.py migrate
|
||||
|
||||
python3 manage.py createsuperuser
|
||||
|
||||
# If model changes
|
||||
python3 manage.py makemigrations --noinput
|
||||
|
||||
# To update code highlight run
|
||||
pygmentize -S default -f html -a .codehilite > project-static/code.css
|
||||
|
||||
```
|
||||
|
||||
Updates to python modules will need to be captured with SCM. This can be done by running `pip freeze > requirements.txt` from the running virtual environment.
|
||||
|
||||
|
||||
|
||||
## Tests
|
||||
|
||||
!!! danger "Requirement"
|
||||
All models **are** to have tests written for them, Including testing between dependent models.
|
||||
|
||||
See [Documentation](https://nofusscomputing.com/projects/django-template/development/testing/) for further information
|
||||
|
||||
|
||||
## Docker Container
|
||||
|
||||
``` bash
|
||||
|
||||
cd app
|
||||
|
||||
docker build . --tag centurion-erp:dev
|
||||
|
||||
docker run -d --rm -v ${PWD}/db.sqlite3:/app/db.sqlite3 -p 8002:8000 --name app centurion-erp:dev
|
||||
|
||||
```
|
||||
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 No Fuss Computing
|
||||
|
||||
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
README.md
Normal file
74
README.md
Normal file
@ -0,0 +1,74 @@
|
||||
<span style="text-align: center;">
|
||||
|
||||
# No Fuss Computing - Centurion ERP
|
||||
|
||||
<br>
|
||||
|
||||

|
||||
|
||||
|
||||
[](https://hub.docker.com/r/nofusscomputing/centurion-erp) [](https://artifacthub.io/packages/container/centurion-erp/centurion-erp)
|
||||
|
||||
|
||||
|
||||
----
|
||||
|
||||
<br>
|
||||
|
||||
  
|
||||
|
||||
|
||||
|
||||
 
|
||||
|
||||
<br>
|
||||
|
||||
 
|
||||
|
||||
|
||||
This project is hosted on [Github](https://github.com/NofussComputing/centurion_erp) and has a read-only copy hosted on [gitlab](https://gitlab.com/nofusscomputing/projects/centurion_erp).
|
||||
|
||||
----
|
||||
|
||||
**Stable Branch**
|
||||
|
||||
  
|
||||

|
||||
|
||||
|
||||
|
||||
----
|
||||
|
||||
**Development Branch**
|
||||
|
||||
|
||||
|
||||
  
|
||||

|
||||
|
||||
|
||||
----
|
||||
<br>
|
||||
|
||||
</div>
|
||||
|
||||
links:
|
||||
|
||||
- [Issues](https://github.com/nofusscomputing/centurion_erp/issues)
|
||||
|
||||
- [Merge Requests (Pull Requests)](https://github.com/nofusscomputing/centurion_erp/pulls)
|
||||
|
||||
|
||||
An ERP with a large emphasis on the IT Service Management (ITSM) and Automation.
|
||||
|
||||
|
||||
## Contributing
|
||||
|
||||
All contributions for this project must conducted from [GitHub](https://github.com/nofusscomputing/centurion_erp).
|
||||
|
||||
For further details on contributing please refer to the [contribution guide](CONTRIBUTING.md).
|
||||
|
||||
|
||||
## Other
|
||||
|
||||
This repo is release under this [license](LICENSE)
|
7
Release-Notes.md
Normal file
7
Release-Notes.md
Normal file
@ -0,0 +1,7 @@
|
||||
# Version 1.0.0
|
||||
|
||||
Initial Release of Centurion ERP.
|
||||
|
||||
## Breaking changes
|
||||
|
||||
- Nil
|
17
app/.coveragerc
Normal file
17
app/.coveragerc
Normal file
@ -0,0 +1,17 @@
|
||||
[run]
|
||||
source = .
|
||||
omit =
|
||||
*migrations/*
|
||||
*tests/*/*
|
||||
|
||||
[report]
|
||||
omit =
|
||||
*/tests/*/*
|
||||
*/migrations/*
|
||||
*apps.py
|
||||
*manage.py
|
||||
*__init__.py
|
||||
*asgi*
|
||||
*wsgi*
|
||||
*admin.py
|
||||
*urls.py
|
0
app/access/__init__.py
Normal file
0
app/access/__init__.py
Normal file
30
app/access/admin.py
Normal file
30
app/access/admin.py
Normal file
@ -0,0 +1,30 @@
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth.models import Group
|
||||
|
||||
from .models import *
|
||||
|
||||
admin.site.unregister(Group)
|
||||
|
||||
class TeamInline(admin.TabularInline):
|
||||
model = Team
|
||||
extra = 0
|
||||
|
||||
readonly_fields = ['name', 'created', 'modified']
|
||||
fields = ['team_name']
|
||||
|
||||
fk_name = 'organization'
|
||||
|
||||
|
||||
class OrganizationAdmin(admin.ModelAdmin):
|
||||
fieldsets = [
|
||||
(None, {"fields": ["name", 'manager', "slug"]}),
|
||||
#("Date information", {"fields": ["slug"], "classes": ["collapse"]}),
|
||||
]
|
||||
inlines = [TeamInline]
|
||||
list_display = ["name", "created", "modified"]
|
||||
list_filter = ["created"]
|
||||
search_fields = ["team_name"]
|
||||
|
||||
|
||||
admin.site.register(Organization,OrganizationAdmin)
|
||||
|
6
app/access/apps.py
Normal file
6
app/access/apps.py
Normal file
@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AccessConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'access'
|
59
app/access/fields.py
Normal file
59
app/access/fields.py
Normal file
@ -0,0 +1,59 @@
|
||||
from django.db import models
|
||||
from django.utils.timezone import now
|
||||
from django.template.defaultfilters import slugify
|
||||
|
||||
class AutoCreatedField(models.DateTimeField):
|
||||
"""
|
||||
A DateTimeField that automatically populates itself at
|
||||
object creation.
|
||||
|
||||
By default, sets editable=False, default=datetime.now.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
kwargs.setdefault("editable", False)
|
||||
|
||||
kwargs.setdefault("default", now)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class AutoLastModifiedField(AutoCreatedField):
|
||||
"""
|
||||
A DateTimeField that updates itself on each save() of the model.
|
||||
|
||||
By default, sets editable=False and default=datetime.now.
|
||||
|
||||
"""
|
||||
|
||||
def pre_save(self, model_instance, add):
|
||||
|
||||
value = now()
|
||||
|
||||
setattr(model_instance, self.attname, value)
|
||||
|
||||
return value
|
||||
|
||||
|
||||
class AutoSlugField(models.SlugField):
|
||||
"""
|
||||
A DateTimeField that updates itself on each save() of the model.
|
||||
|
||||
By default, sets editable=False and default=datetime.now.
|
||||
|
||||
"""
|
||||
|
||||
def pre_save(self, model_instance, add):
|
||||
|
||||
if not model_instance.slug or model_instance.slug == '_':
|
||||
value = model_instance.name.lower().replace(' ', '_')
|
||||
|
||||
setattr(model_instance, self.attname, value)
|
||||
|
||||
return value
|
||||
|
||||
return model_instance.slug
|
||||
|
||||
|
38
app/access/forms/organization.py
Normal file
38
app/access/forms/organization.py
Normal file
@ -0,0 +1,38 @@
|
||||
from django import forms
|
||||
from django.db.models import Q
|
||||
|
||||
from app import settings
|
||||
|
||||
from access.models import Organization
|
||||
|
||||
from core.forms.common import CommonModelForm
|
||||
|
||||
class OrganizationForm(CommonModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Organization
|
||||
fields = [
|
||||
'name',
|
||||
'manager',
|
||||
'model_notes',
|
||||
]
|
||||
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.fields['created'] = forms.DateTimeField(
|
||||
label="Created",
|
||||
input_formats=settings.DATETIME_FORMAT,
|
||||
initial=kwargs['instance'].created,
|
||||
disabled=True,
|
||||
required=False,
|
||||
)
|
||||
|
||||
self.fields['modified'] = forms.DateTimeField(
|
||||
label="Modified",
|
||||
input_formats=settings.DATETIME_FORMAT,
|
||||
initial=kwargs['instance'].modified,
|
||||
disabled=True,
|
||||
required=False,
|
||||
)
|
103
app/access/forms/team.py
Normal file
103
app/access/forms/team.py
Normal file
@ -0,0 +1,103 @@
|
||||
from django import forms
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.db.models import Q
|
||||
from django.forms import inlineformset_factory
|
||||
|
||||
from app import settings
|
||||
|
||||
from .team_users import TeamUsersForm, TeamUsers
|
||||
|
||||
from access.models import Team
|
||||
|
||||
from core.forms.common import CommonModelForm
|
||||
|
||||
TeamUserFormSet = inlineformset_factory(
|
||||
model=TeamUsers,
|
||||
parent_model= Team,
|
||||
extra = 1,
|
||||
fields=[
|
||||
'user',
|
||||
'manager'
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
|
||||
class TeamFormAdd(CommonModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Team
|
||||
fields = [
|
||||
'team_name',
|
||||
'model_notes',
|
||||
]
|
||||
|
||||
|
||||
|
||||
class TeamForm(CommonModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Team
|
||||
fields = [
|
||||
'team_name',
|
||||
'permissions',
|
||||
'model_notes',
|
||||
]
|
||||
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.fields['created'] = forms.DateTimeField(
|
||||
label="Created",
|
||||
input_formats=settings.DATETIME_FORMAT,
|
||||
initial=kwargs['instance'].created,
|
||||
disabled=True,
|
||||
required=False,
|
||||
)
|
||||
|
||||
self.fields['modified'] = forms.DateTimeField(
|
||||
label="Modified",
|
||||
input_formats=settings.DATETIME_FORMAT,
|
||||
initial=kwargs['instance'].modified,
|
||||
disabled=True,
|
||||
required=False,
|
||||
)
|
||||
|
||||
self.fields['permissions'].widget.attrs = {'style': "height: 200px;"}
|
||||
|
||||
apps = [
|
||||
'access',
|
||||
'assistance',
|
||||
'config_management',
|
||||
'core',
|
||||
'django_celery_results',
|
||||
'itam',
|
||||
'settings',
|
||||
]
|
||||
|
||||
exclude_models = [
|
||||
'appsettings',
|
||||
'chordcounter',
|
||||
'groupresult',
|
||||
'organization'
|
||||
'settings',
|
||||
'usersettings',
|
||||
]
|
||||
|
||||
exclude_permissions = [
|
||||
'add_organization',
|
||||
'add_taskresult',
|
||||
'change_organization',
|
||||
'change_taskresult',
|
||||
'delete_organization',
|
||||
'delete_taskresult',
|
||||
]
|
||||
|
||||
self.fields['permissions'].queryset = Permission.objects.filter(
|
||||
content_type__app_label__in=apps,
|
||||
).exclude(
|
||||
content_type__model__in=exclude_models
|
||||
).exclude(
|
||||
codename__in = exclude_permissions
|
||||
)
|
16
app/access/forms/team_users.py
Normal file
16
app/access/forms/team_users.py
Normal file
@ -0,0 +1,16 @@
|
||||
from django.db.models import Q
|
||||
|
||||
from app import settings
|
||||
|
||||
from access.models import TeamUsers
|
||||
|
||||
from core.forms.common import CommonModelForm
|
||||
|
||||
class TeamUsersForm(CommonModelForm):
|
||||
|
||||
class Meta:
|
||||
model = TeamUsers
|
||||
fields = [
|
||||
'user',
|
||||
'manager',
|
||||
]
|
73
app/access/migrations/0001_initial.py
Normal file
73
app/access/migrations/0001_initial.py
Normal file
@ -0,0 +1,73 @@
|
||||
# Generated by Django 5.0.7 on 2024-07-12 03:54
|
||||
|
||||
import access.fields
|
||||
import access.models
|
||||
import django.contrib.auth.models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('auth', '0012_alter_user_first_name_max_length'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Organization',
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, serialize=False, unique=True)),
|
||||
('name', models.CharField(max_length=50, unique=True)),
|
||||
('model_notes', models.TextField(blank=True, default=None, null=True, verbose_name='Notes')),
|
||||
('slug', access.fields.AutoSlugField()),
|
||||
('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)),
|
||||
('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)),
|
||||
('manager', models.ForeignKey(help_text='Organization Manager', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'Organizations',
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Team',
|
||||
fields=[
|
||||
('group_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='auth.group')),
|
||||
('is_global', models.BooleanField(default=False)),
|
||||
('model_notes', models.TextField(blank=True, default=None, null=True, verbose_name='Notes')),
|
||||
('team_name', models.CharField(default='', max_length=50, verbose_name='Name')),
|
||||
('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)),
|
||||
('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)),
|
||||
('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists])),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'Teams',
|
||||
'ordering': ['team_name'],
|
||||
},
|
||||
bases=('auth.group', models.Model),
|
||||
managers=[
|
||||
('objects', django.contrib.auth.models.GroupManager()),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TeamUsers',
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, serialize=False, unique=True)),
|
||||
('manager', models.BooleanField(blank=True, default=False, verbose_name='manager')),
|
||||
('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)),
|
||||
('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)),
|
||||
('team', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='team', to='access.team')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'Team Users',
|
||||
'ordering': ['user'],
|
||||
},
|
||||
),
|
||||
]
|
0
app/access/migrations/__init__.py
Normal file
0
app/access/migrations/__init__.py
Normal file
362
app/access/mixin.py
Normal file
362
app/access/mixin.py
Normal file
@ -0,0 +1,362 @@
|
||||
|
||||
from django.contrib.auth.mixins import AccessMixin, PermissionRequiredMixin
|
||||
from django.contrib.auth.models import Group
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
from .models import Organization, Team
|
||||
|
||||
|
||||
class OrganizationMixin():
|
||||
"""Base Organization class"""
|
||||
|
||||
request = None
|
||||
|
||||
user_groups = []
|
||||
|
||||
|
||||
def get_parent_obj(self):
|
||||
""" Get the Parent Model Object
|
||||
|
||||
Use in views where the the model has no organization and the organization should be fetched from the parent model.
|
||||
|
||||
Requires attribute `parent_model` within the view with the value of the parent's model class
|
||||
|
||||
Returns:
|
||||
parent_model (Model): with PK from kwargs['pk']
|
||||
"""
|
||||
|
||||
return self.parent_model.objects.get(pk=self.kwargs['pk'])
|
||||
|
||||
|
||||
def object_organization(self) -> int:
|
||||
|
||||
id = None
|
||||
|
||||
try:
|
||||
|
||||
if hasattr(self, 'get_queryset'):
|
||||
self.get_queryset()
|
||||
|
||||
|
||||
if hasattr(self, 'parent_model'):
|
||||
obj = self.get_parent_obj()
|
||||
|
||||
id = obj.get_organization().id
|
||||
|
||||
if obj.is_global:
|
||||
|
||||
id = 0
|
||||
|
||||
|
||||
if hasattr(self, 'get_object') and id is None:
|
||||
|
||||
obj = self.get_object()
|
||||
|
||||
id = obj.get_organization().id
|
||||
|
||||
if hasattr(obj, 'is_global'):
|
||||
|
||||
if obj.is_global:
|
||||
|
||||
id = 0
|
||||
|
||||
|
||||
except AttributeError:
|
||||
|
||||
if self.request.method == 'POST':
|
||||
|
||||
if self.request.POST.get("organization", ""):
|
||||
|
||||
id = int(self.request.POST.get("organization", ""))
|
||||
|
||||
for field in self.request.POST.dict(): # cater for fields prefixed '<prefix>-<field name>'
|
||||
|
||||
a_field = str(field).split('-')
|
||||
|
||||
if len(a_field) == 2:
|
||||
|
||||
if a_field[1] == 'organization':
|
||||
|
||||
id = int(self.request.POST.get(field))
|
||||
|
||||
except:
|
||||
|
||||
pass
|
||||
|
||||
|
||||
return id
|
||||
|
||||
|
||||
def is_member(self, organization: int) -> bool:
|
||||
"""Returns true if the current user is a member of the organization
|
||||
|
||||
iterates over the user_organizations list and returns true if the user is a member
|
||||
|
||||
Returns:
|
||||
bool: _description_
|
||||
"""
|
||||
|
||||
is_member = False
|
||||
|
||||
if organization in self.user_organizations():
|
||||
|
||||
return True
|
||||
|
||||
return is_member
|
||||
|
||||
|
||||
def get_permission_required(self):
|
||||
"""
|
||||
Override of 'PermissionRequiredMixin' method so that this mixin can obtain the required permission.
|
||||
"""
|
||||
|
||||
if self.permission_required is None:
|
||||
raise ImproperlyConfigured(
|
||||
f"{self.__class__.__name__} is missing the "
|
||||
f"permission_required attribute. Define "
|
||||
f"{self.__class__.__name__}.permission_required, or override "
|
||||
f"{self.__class__.__name__}.get_permission_required()."
|
||||
)
|
||||
if isinstance(self.permission_required, str):
|
||||
perms = (self.permission_required,)
|
||||
else:
|
||||
perms = self.permission_required
|
||||
return perms
|
||||
|
||||
|
||||
@cached_property
|
||||
def is_manager(self) -> bool:
|
||||
""" Returns true if the current user is a member of the organization"""
|
||||
is_manager = False
|
||||
|
||||
return is_manager
|
||||
|
||||
|
||||
def user_organizations(self) -> list():
|
||||
"""Current Users organizations
|
||||
|
||||
Fetches the Organizations the user is apart of.
|
||||
|
||||
Get All groups the user is part of, fetch the associated team,
|
||||
iterate over the results adding the organization ID to a list to be returned.
|
||||
|
||||
Returns:
|
||||
_type_: User Organizations.
|
||||
"""
|
||||
|
||||
user_organizations = []
|
||||
|
||||
teams = Team.objects
|
||||
|
||||
for group in self.request.user.groups.all():
|
||||
|
||||
team = teams.get(pk=group.id)
|
||||
|
||||
self.user_groups = self.user_groups + [group.id]
|
||||
|
||||
user_organizations = user_organizations + [team.organization.id]
|
||||
|
||||
return user_organizations
|
||||
|
||||
|
||||
# ToDo: Ensure that the group has access to item
|
||||
def has_organization_permission(self, organization: int=None) -> bool:
|
||||
|
||||
has_permission = False
|
||||
|
||||
if not organization:
|
||||
|
||||
organization = self.object_organization()
|
||||
|
||||
if self.is_member(organization) or organization == 0:
|
||||
|
||||
groups = Group.objects.filter(pk__in=self.user_groups)
|
||||
|
||||
for group in groups:
|
||||
|
||||
team = Team.objects.filter(pk=group.id)
|
||||
team = team.values('organization_id').get()
|
||||
|
||||
for permission in group.permissions.values('content_type__app_label', 'codename').all():
|
||||
|
||||
assembled_permission = str(permission["content_type__app_label"]) + '.' + str(permission["codename"])
|
||||
|
||||
if assembled_permission in self.get_permission_required() and (team['organization_id'] == organization or organization == 0):
|
||||
|
||||
return True
|
||||
|
||||
return has_permission
|
||||
|
||||
|
||||
def permission_check(self, request, permissions_required: list = None) -> bool:
|
||||
|
||||
self.request = request
|
||||
|
||||
if permissions_required:
|
||||
|
||||
self.permission_required = permissions_required
|
||||
|
||||
organization_manager_models = [
|
||||
'access.organization',
|
||||
'access.team',
|
||||
'access.teamusers',
|
||||
]
|
||||
|
||||
is_organization_manager = False
|
||||
|
||||
queryset = None
|
||||
|
||||
if hasattr(self, 'get_queryset'):
|
||||
|
||||
queryset = self.get_queryset()
|
||||
|
||||
obj = None
|
||||
|
||||
if hasattr(self, 'get_object'):
|
||||
|
||||
|
||||
try:
|
||||
|
||||
obj = self.get_object()
|
||||
|
||||
except:
|
||||
|
||||
pass
|
||||
|
||||
|
||||
if hasattr(self, 'model'):
|
||||
|
||||
if self.model._meta.label_lower in organization_manager_models:
|
||||
|
||||
organization = Organization.objects.get(pk=self.object_organization())
|
||||
|
||||
if organization.manager == request.user:
|
||||
|
||||
is_organization_manager = True
|
||||
|
||||
return True
|
||||
|
||||
|
||||
if request.user.is_superuser:
|
||||
|
||||
return True
|
||||
|
||||
perms = self.get_permission_required()
|
||||
|
||||
if self.has_organization_permission():
|
||||
|
||||
return True
|
||||
|
||||
if self.request.user.has_perms(perms) and len(self.kwargs) == 0 and str(self.request.method).lower() == 'get':
|
||||
|
||||
return True
|
||||
|
||||
for required_permission in self.permission_required:
|
||||
|
||||
if required_permission.replace(
|
||||
'view_', ''
|
||||
) == 'access.organization' and len(self.kwargs) == 0:
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
|
||||
class OrganizationPermission(AccessMixin, OrganizationMixin):
|
||||
"""## Permission Checking
|
||||
|
||||
The base django permissions have not been modified with this app providing Multi-Tenancy. This is done by a mixin, that checks if the item is apart of an organization, if it is; confirmation is made that the user is part of the same organization and as long as they have the correct permission within the organization, access is granted.
|
||||
|
||||
|
||||
### How it works
|
||||
|
||||
The overall permissions system of django has not been modified with it remaining fully functional. The multi-tenancy has been setup based off of an organization with teams. A team to the underlying django system is an extension of the django auth group and for every team created a django auth group is created. THe group name is set using the following format: `<organization>_<team name>` and contains underscores `_` instead of spaces.
|
||||
|
||||
A User who is added to an team as a "Manager" can modify the team members or if they have permission `access.change_team` which also allows the changing of team permissions. Modification of an organization can be done by the django administrator (super user) or any user with permission `access._change_organization`.
|
||||
|
||||
Items can be set as `Global`, meaning that all users who have the correct permission regardless of organization will be able to take action against the object.
|
||||
|
||||
Permissions that can be modified for a team have been limited to application permissions only unless adjust the permissions from the django admin site.
|
||||
|
||||
|
||||
### Multi-Tenancy workflow
|
||||
|
||||
The workflow is conducted as part of the view and has the following flow:
|
||||
|
||||
1. Checks if user is member of organization the object the action is being performed on. Will also return true if the object has field `is_global` set to `true`.
|
||||
|
||||
1. Fetches all teams the user is part of.
|
||||
|
||||
1. obtains all permissions that are linked to the team.
|
||||
|
||||
1. checks if user has the required permission for the action.
|
||||
|
||||
1. confirms that the team the permission came from is part of the same organization as the object the action is being conducted on.
|
||||
|
||||
1. ONLY on success of the above items, grants access.
|
||||
"""
|
||||
|
||||
permission_required: list = []
|
||||
""" Permission required for the view
|
||||
|
||||
Not specifying this property adjusts the permission check logic so that you can
|
||||
use the `permission_check()` function directly.
|
||||
|
||||
An example of a get request....
|
||||
|
||||
``` py
|
||||
def get(self, request, *args, **kwargs):
|
||||
|
||||
if not request.user.is_authenticated:
|
||||
|
||||
return self.handle_no_permission()
|
||||
|
||||
if not self.permission_check(request, [ 'access.view_organization' ]):
|
||||
|
||||
raise PermissionDenied('You are not part of this organization')
|
||||
|
||||
return super().get(request, *args, **kwargs)
|
||||
```
|
||||
this example details manual usage of the `permission_check()` function for a get request.
|
||||
"""
|
||||
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
|
||||
if not request.user.is_authenticated:
|
||||
return self.handle_no_permission()
|
||||
|
||||
if len(self.permission_required) > 0:
|
||||
|
||||
non_organization_models = [
|
||||
'TaskResult'
|
||||
]
|
||||
|
||||
if hasattr(self, 'model'):
|
||||
|
||||
|
||||
if hasattr(self.model, '__name__'):
|
||||
|
||||
if self.model.__name__ in non_organization_models:
|
||||
|
||||
if hasattr(self, 'get_object'):
|
||||
|
||||
self.get_object()
|
||||
|
||||
perms = self.get_permission_required()
|
||||
|
||||
|
||||
if not self.request.user.has_perms(perms):
|
||||
|
||||
return self.handle_no_permission()
|
||||
|
||||
return super().dispatch(self.request, *args, **kwargs)
|
||||
|
||||
|
||||
if not self.permission_check(request):
|
||||
|
||||
raise PermissionDenied('You are not part of this organization')
|
||||
|
||||
return super().dispatch(self.request, *args, **kwargs)
|
324
app/access/models.py
Normal file
324
app/access/models.py
Normal file
@ -0,0 +1,324 @@
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User, Group, Permission
|
||||
from django.forms import ValidationError
|
||||
|
||||
from .fields import *
|
||||
|
||||
from core.middleware.get_request import get_request
|
||||
from core.mixin.history_save import SaveHistory
|
||||
|
||||
|
||||
class Organization(SaveHistory):
|
||||
|
||||
class Meta:
|
||||
verbose_name_plural = "Organizations"
|
||||
ordering = ['name']
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
if self.slug == '_':
|
||||
self.slug = self.name.lower().replace(' ', '_')
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
id = models.AutoField(
|
||||
primary_key=True,
|
||||
unique=True,
|
||||
blank=False
|
||||
)
|
||||
|
||||
name = models.CharField(
|
||||
blank = False,
|
||||
max_length = 50,
|
||||
unique = True,
|
||||
)
|
||||
|
||||
manager = models.ForeignKey(
|
||||
User,
|
||||
on_delete=models.SET_NULL,
|
||||
blank = False,
|
||||
null = True,
|
||||
help_text = 'Organization Manager'
|
||||
)
|
||||
|
||||
model_notes = models.TextField(
|
||||
blank = True,
|
||||
default = None,
|
||||
null= True,
|
||||
verbose_name = 'Notes',
|
||||
)
|
||||
|
||||
slug = AutoSlugField()
|
||||
|
||||
created = AutoCreatedField()
|
||||
|
||||
modified = AutoLastModifiedField()
|
||||
|
||||
|
||||
def get_organization(self):
|
||||
return self
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
|
||||
class TenancyManager(models.Manager):
|
||||
"""Multi-Tennant Object Manager
|
||||
|
||||
This manager specifically caters for the multi-tenancy features of Centurion ERP.
|
||||
"""
|
||||
|
||||
|
||||
def get_queryset(self):
|
||||
""" Fetch the data
|
||||
|
||||
This function filters the data fetched from the database to that which is from the organizations
|
||||
the user is a part of.
|
||||
|
||||
!!! danger "Requirement"
|
||||
This method may be overridden however must still be called from the overriding function. i.e. `super().get_queryset()`
|
||||
|
||||
## Workflow
|
||||
|
||||
This functions workflow is as follows:
|
||||
|
||||
- Fetch the user from the request
|
||||
|
||||
- Check if the user is authenticated
|
||||
|
||||
- Iterate over the users teams
|
||||
|
||||
- Store unique organizations from users teams
|
||||
|
||||
- return results
|
||||
|
||||
Returns:
|
||||
(queryset): **super user**: return unfiltered data.
|
||||
(queryset): **not super user**: return data from the stored unique organizations.
|
||||
"""
|
||||
|
||||
request = get_request()
|
||||
|
||||
user_organizations: list(str()) = []
|
||||
|
||||
|
||||
if request:
|
||||
|
||||
user = request.user._wrapped if hasattr(request.user,'_wrapped') else request.user
|
||||
|
||||
|
||||
if user.is_authenticated:
|
||||
|
||||
for team_user in TeamUsers.objects.filter(user=user):
|
||||
|
||||
|
||||
if team_user.team.organization.name not in user_organizations:
|
||||
|
||||
|
||||
if not user_organizations:
|
||||
|
||||
self.user_organizations = []
|
||||
|
||||
user_organizations += [ team_user.team.organization.id ]
|
||||
|
||||
|
||||
if len(user_organizations) > 0 and not user.is_superuser:
|
||||
|
||||
return super().get_queryset().filter(
|
||||
models.Q(organization__in=user_organizations)
|
||||
|
|
||||
models.Q(is_global = True)
|
||||
)
|
||||
|
||||
return super().get_queryset()
|
||||
|
||||
|
||||
|
||||
class TenancyObject(SaveHistory):
|
||||
""" Tenancy Model Abstrct class.
|
||||
|
||||
This class is for inclusion wihtin **every** model within Centurion ERP.
|
||||
Provides the required fields, functions and methods for multi tennant objects.
|
||||
Unless otherwise stated, **no** object within this class may be overridden.
|
||||
|
||||
Raises:
|
||||
ValidationError: User failed to supply organization
|
||||
"""
|
||||
|
||||
objects = TenancyManager()
|
||||
""" Multi-Tenanant Objects """
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
def validatate_organization_exists(self):
|
||||
"""Ensure that the user did provide an organization
|
||||
|
||||
Raises:
|
||||
ValidationError: User failed to supply organization.
|
||||
"""
|
||||
|
||||
if not self:
|
||||
raise ValidationError('You must provide an organization')
|
||||
|
||||
|
||||
organization = models.ForeignKey(
|
||||
Organization,
|
||||
on_delete=models.CASCADE,
|
||||
blank = False,
|
||||
null = True,
|
||||
validators = [validatate_organization_exists],
|
||||
)
|
||||
|
||||
is_global = models.BooleanField(
|
||||
default = False,
|
||||
blank = False
|
||||
)
|
||||
|
||||
model_notes = models.TextField(
|
||||
blank = True,
|
||||
default = None,
|
||||
null= True,
|
||||
verbose_name = 'Notes',
|
||||
)
|
||||
|
||||
def get_organization(self) -> Organization:
|
||||
return self.organization
|
||||
|
||||
|
||||
|
||||
class Team(Group, TenancyObject):
|
||||
class Meta:
|
||||
# proxy = True
|
||||
verbose_name_plural = "Teams"
|
||||
ordering = ['team_name']
|
||||
|
||||
|
||||
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
|
||||
|
||||
self.name = self.organization.name.lower().replace(' ', '_') + '_' + self.team_name.lower().replace(' ', '_')
|
||||
|
||||
super().save(force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields)
|
||||
|
||||
|
||||
team_name = models.CharField(
|
||||
verbose_name = 'Name',
|
||||
blank = False,
|
||||
max_length = 50,
|
||||
unique = False,
|
||||
default = ''
|
||||
)
|
||||
|
||||
created = AutoCreatedField()
|
||||
|
||||
modified = AutoLastModifiedField()
|
||||
|
||||
|
||||
@property
|
||||
def parent_object(self):
|
||||
""" Fetch the parent object """
|
||||
|
||||
return self.organization
|
||||
|
||||
|
||||
def permission_list(self) -> list:
|
||||
|
||||
permission_list = []
|
||||
|
||||
for permission in self.permissions.all():
|
||||
|
||||
if str(permission.content_type.app_label + '.' + permission.codename) in permission_list:
|
||||
continue
|
||||
|
||||
permission_list += [ str(permission.content_type.app_label + '.' + permission.codename) ]
|
||||
|
||||
return [permission_list, self.permissions.all()]
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return self.team_name
|
||||
|
||||
|
||||
|
||||
class TeamUsers(SaveHistory):
|
||||
|
||||
class Meta:
|
||||
# proxy = True
|
||||
verbose_name_plural = "Team Users"
|
||||
ordering = ['user']
|
||||
|
||||
id = models.AutoField(
|
||||
primary_key=True,
|
||||
unique=True,
|
||||
blank=False
|
||||
)
|
||||
|
||||
team = models.ForeignKey(
|
||||
Team,
|
||||
related_name="team",
|
||||
on_delete=models.CASCADE)
|
||||
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE
|
||||
)
|
||||
|
||||
manager = models.BooleanField(
|
||||
verbose_name='manager',
|
||||
default=False,
|
||||
blank=True
|
||||
)
|
||||
|
||||
created = AutoCreatedField()
|
||||
|
||||
modified = AutoLastModifiedField()
|
||||
|
||||
|
||||
def delete(self, using=None, keep_parents=False):
|
||||
""" Delete Team
|
||||
|
||||
Overrides, post-action
|
||||
As teams are an extension of Groups, remove the user to the team.
|
||||
"""
|
||||
|
||||
super().delete(using=using, keep_parents=keep_parents)
|
||||
|
||||
group = Group.objects.get(pk=self.team.id)
|
||||
|
||||
user = User.objects.get(pk=self.user_id)
|
||||
|
||||
user.groups.remove(group)
|
||||
|
||||
|
||||
def get_organization(self) -> Organization:
|
||||
return self.team.organization
|
||||
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
""" Save Team
|
||||
|
||||
Overrides, post-action
|
||||
As teams are an extension of groups, add the user to the matching group.
|
||||
"""
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
group = Group.objects.get(pk=self.team.id)
|
||||
|
||||
user = User.objects.get(pk=self.user_id)
|
||||
|
||||
user.groups.add(group)
|
||||
|
||||
|
||||
@property
|
||||
def parent_object(self):
|
||||
""" Fetch the parent object """
|
||||
|
||||
return self.team
|
||||
|
||||
def __str__(self):
|
||||
return self.user.username
|
||||
|
22
app/access/templates/access/index.html.j2
Normal file
22
app/access/templates/access/index.html.j2
Normal file
@ -0,0 +1,22 @@
|
||||
{% extends 'base.html.j2' %}
|
||||
|
||||
{% block content_header_icon %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<table class="data">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Created</th>
|
||||
<th>Modified</th>
|
||||
</tr>
|
||||
{% for org in organization_list %}
|
||||
<tr>
|
||||
<td><a href="/organization/{{ org.id }}/">{{ org.name }}</a></td>
|
||||
<td>{{ org.created }}</td>
|
||||
<td>{{ org.modified }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</table>
|
||||
{% endblock %}
|
106
app/access/templates/access/organization.html.j2
Normal file
106
app/access/templates/access/organization.html.j2
Normal file
@ -0,0 +1,106 @@
|
||||
{% extends 'base.html.j2' %}
|
||||
|
||||
{% load markdown %}
|
||||
|
||||
{% block title %}Organization - {{ organization.name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<style>
|
||||
form div .helptext {
|
||||
background-color: rgb(0, 140, 255);
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.detail-view-field {
|
||||
display:unset;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
padding: 0px 20px 40px 20px;
|
||||
|
||||
}
|
||||
|
||||
.detail-view-field label {
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
width: 200px;
|
||||
margin: 10px;
|
||||
/*padding: 10px;*/
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
|
||||
}
|
||||
|
||||
.detail-view-field span {
|
||||
display: inline-block;
|
||||
width: 340px;
|
||||
margin: 10px;
|
||||
/*padding: 10px;*/
|
||||
border-bottom: 1px solid #ccc;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
|
||||
<div style="align-items:flex-start; align-content: center; display: flexbox; width: 100%">
|
||||
<div style="display: inline; width: 40%; margin: 30px;">
|
||||
|
||||
<div class="detail-view-field">
|
||||
<label>{{ form.name.label }}</label>
|
||||
<span>{{ form.name.value }}</span>
|
||||
</div>
|
||||
|
||||
<div class="detail-view-field">
|
||||
<label>{{ form.manager.label }}</label>
|
||||
<span>{{ organization.manager }}</span>
|
||||
</div>
|
||||
|
||||
<div class="detail-view-field">
|
||||
<label>{{ form.created.label }}</label>
|
||||
<span>{{ form.created.value }}</span>
|
||||
</div>
|
||||
|
||||
<div class="detail-view-field">
|
||||
<label>{{ form.modified.label }}</label>
|
||||
<span>{{ form.modified.value }}</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div style="display: inline; width: 40%; margin: 30px; text-align: left;">
|
||||
<div>
|
||||
<label style="font-weight: bold; width: 100%; border-bottom: 1px solid #ccc; display: block; text-align: inherit;">{{ form.model_notes.label }}</label>
|
||||
|
||||
<div style="display: inline-block; text-align: left;">{{ form.model_notes.value | markdown | safe }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: block;">
|
||||
<input type="button" value="<< Back" onclick="window.location='{% url 'Access:Organizations' %}';">
|
||||
<input type="button" value="New Team" onclick="window.location='{% url 'Access:_team_add' organization.id %}';">
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Team Name</th>
|
||||
<th>Created</th>
|
||||
<th>Modified</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% for field in teams %}
|
||||
<tr>
|
||||
<td><a href="{% url 'Access:_team_view' organization_id=organization.id pk=field.id %}">{{ field.team_name }}</a></td>
|
||||
<td>{{ field.created }}</td>
|
||||
<td>{{ field.modified }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
{% endblock %}
|
48
app/access/templates/access/team.html.j2
Normal file
48
app/access/templates/access/team.html.j2
Normal file
@ -0,0 +1,48 @@
|
||||
{% extends 'base.html.j2' %}
|
||||
|
||||
{% block title %}Team - {{ team.team_name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
{{ form.as_div }}
|
||||
|
||||
<input style="display:unset;" type="submit" value="Submit">
|
||||
</form>
|
||||
|
||||
|
||||
<hr />
|
||||
|
||||
<input type="button" value="<< Back" onclick="window.location='{% url 'Access:_organization_view' pk=organization.id %}';">
|
||||
<input type="button" value="Delete Team"
|
||||
onclick="window.location='{% url 'Access:_team_delete' organization_id=organization.id pk=team.id %}';">
|
||||
<input type="button" value="Assign User"
|
||||
onclick="window.location='{% url 'Access:_team_user_add' organization_id=organization.id pk=team.id %}';">
|
||||
{{ formset.management_form }}
|
||||
|
||||
<table id="formset" class="form">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>User</th>
|
||||
<th>Manager</th>
|
||||
<th>Created</th>
|
||||
<th>Modified</th>
|
||||
<th> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
{% for field in teamusers %}
|
||||
<tr>
|
||||
<td>{{ field.user }}</td>
|
||||
<td><input type="checkbox" {% if field.manager %}checked{% endif %} disabled></td>
|
||||
<td>{{ field.created }}</td>
|
||||
<td>{{ field.modified }}</td>
|
||||
<td><a
|
||||
href="{% url 'Access:_team_user_delete' organization_id=organization.id team_id=field.team_id pk=field.id %}">Delete</a></a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
{% endblock %}
|
0
app/access/tests/__init__.py
Normal file
0
app/access/tests/__init__.py
Normal file
0
app/access/tests/abstract/__init__.py
Normal file
0
app/access/tests/abstract/__init__.py
Normal file
@ -0,0 +1,251 @@
|
||||
import pytest
|
||||
import unittest
|
||||
|
||||
from django.test import Client
|
||||
from django.shortcuts import reverse
|
||||
|
||||
|
||||
|
||||
class OrganizationManagerModelPermissionView:
|
||||
""" Tests for checking Organization Manager model permissions """
|
||||
|
||||
|
||||
app_namespace: str = None
|
||||
""" Application namespace of the model being tested """
|
||||
|
||||
different_organization_is_manager: object
|
||||
""" User whom is organization Manager of different organization than object """
|
||||
|
||||
url_name_view: str
|
||||
""" url name of the model view to be tested """
|
||||
|
||||
url_view_kwargs: dict = None
|
||||
""" View URL kwargs for model being tested """
|
||||
|
||||
user_is_organization_manager: object
|
||||
""" User whom is organization Manager of the object"""
|
||||
|
||||
|
||||
|
||||
def test_model_view_different_organizaiton_is_organization_manager_denied(self):
|
||||
""" Check correct permission for view
|
||||
|
||||
Attempt to view with user from different organization whom is an organization Manager.
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
if self.app_namespace:
|
||||
|
||||
url = reverse(self.app_namespace + ':' + self.url_name_view, kwargs=self.url_view_kwargs)
|
||||
|
||||
else:
|
||||
|
||||
url = reverse(self.url_name_view, kwargs=self.url_view_kwargs)
|
||||
|
||||
|
||||
client.force_login(self.different_organization_is_manager)
|
||||
response = client.get(url)
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
def test_model_view_has_no_permission_is_organization_manager(self):
|
||||
""" Confirm that an organization manager can view the model
|
||||
|
||||
Attempt to view as user who is an organization manager and has no permissions assigned.
|
||||
Object to be within same organization the user is a manager of.
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
if self.app_namespace:
|
||||
|
||||
url = reverse(self.app_namespace + ':' + self.url_name_view, kwargs=self.url_view_kwargs)
|
||||
|
||||
else:
|
||||
|
||||
url = reverse(self.url_name_view, kwargs=self.url_view_kwargs)
|
||||
|
||||
|
||||
client.force_login(self.user_is_organization_manager)
|
||||
response = client.get(url)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
|
||||
class OrganizationManagerModelPermissionAdd:
|
||||
""" Tests for checking model Add permissions """
|
||||
|
||||
|
||||
app_namespace: str = None
|
||||
""" Application namespace of the model being tested """
|
||||
|
||||
different_organization_is_manager: object
|
||||
""" User whom is organization Manager of different organization than object """
|
||||
|
||||
url_name_view: str
|
||||
""" url name of the model view to be tested """
|
||||
|
||||
url_view_kwargs: dict = None
|
||||
""" View URL kwargs for model being tested """
|
||||
|
||||
user_is_organization_manager: object
|
||||
""" User whom is organization Manager of the object"""
|
||||
|
||||
|
||||
|
||||
def test_model_add_different_organization_is_organization_manager_denied(self):
|
||||
""" Check correct permission for add
|
||||
|
||||
attempt to add as user from different organization whom is an organization Manager.
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.app_namespace + ':' + self.url_name_add, kwargs=self.url_add_kwargs)
|
||||
|
||||
|
||||
client.force_login(self.different_organization_is_manager)
|
||||
response = client.post(url, data=self.add_data)
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
def test_model_add_has_no_permission_is_organization_manager(self):
|
||||
""" Check correct permission for add
|
||||
|
||||
Attempt to add as user who is an organization manager and has no permissions assigned.
|
||||
Object to be within same organization the user is a manager of.
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.app_namespace + ':' + self.url_name_add, kwargs=self.url_add_kwargs)
|
||||
|
||||
|
||||
client.force_login(self.user_is_organization_manager)
|
||||
response = client.post(url, data=self.add_data)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
|
||||
class OrganizationManagerModelPermissionChange:
|
||||
""" Tests for checking model change permissions """
|
||||
|
||||
|
||||
app_namespace: str = None
|
||||
""" Application namespace of the model being tested """
|
||||
|
||||
different_organization_is_manager: object
|
||||
""" User whom is organization Manager of different organization than object """
|
||||
|
||||
url_name_change: str
|
||||
""" url name of the model view to be tested """
|
||||
|
||||
url_change_kwargs: dict = None
|
||||
""" View URL kwargs for model being tested """
|
||||
|
||||
user_is_organization_manager: object
|
||||
""" User whom is organization Manager of the object"""
|
||||
|
||||
|
||||
|
||||
def test_model_change_different_organization_is_organization_manager_denied(self):
|
||||
""" Ensure permission view cant make change
|
||||
|
||||
Attempt to make change as user from different organization whom is an organization Manager.
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.app_namespace + ':' + self.url_name_change, kwargs=self.url_change_kwargs)
|
||||
|
||||
|
||||
client.force_login(self.different_organization_is_manager)
|
||||
response = client.post(url, data=self.change_data)
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
def test_model_change_has_no_permission_is_organization_manager(self):
|
||||
""" Check correct permission for change
|
||||
|
||||
Make change as user who is an organization manager and has no permissions assigned.
|
||||
Object to be within same organization the user is a manager of.
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.app_namespace + ':' + self.url_name_change, kwargs=self.url_change_kwargs)
|
||||
|
||||
|
||||
client.force_login(self.user_is_organization_manager)
|
||||
response = client.post(url, data=self.change_data)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
|
||||
class OrganizationManagerModelPermissionDelete:
|
||||
""" Tests for checking model delete permissions """
|
||||
|
||||
|
||||
app_namespace: str = None
|
||||
""" Application namespace of the model being tested """
|
||||
|
||||
different_organization_is_manager: object
|
||||
""" User whom is organization Manager of different organization than object """
|
||||
|
||||
url_name_view: str
|
||||
""" url name of the model view to be tested """
|
||||
|
||||
url_view_kwargs: dict = None
|
||||
""" View URL kwargs for model being tested """
|
||||
|
||||
user_is_organization_manager: object
|
||||
""" User whom is organization Manager of the object"""
|
||||
|
||||
|
||||
|
||||
def test_model_delete_different_organization_is_organization_manager_denied(self):
|
||||
""" Check correct permission for delete
|
||||
|
||||
Attempt to delete as user from different organization whom is an organization Manager.
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.app_namespace + ':' + self.url_name_delete, kwargs=self.url_delete_kwargs)
|
||||
|
||||
|
||||
client.force_login(self.different_organization_is_manager)
|
||||
response = client.delete(url, data=self.delete_data)
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
def test_model_delete_has_no_permission_is_organization_manager(self):
|
||||
""" Check correct permission for delete
|
||||
|
||||
Delete item as user who is an organization manager and has no permissions assigned.
|
||||
Object to be within same organization the user is a manager of.
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.app_namespace + ':' + self.url_name_delete, kwargs=self.url_delete_kwargs)
|
||||
|
||||
|
||||
client.force_login(self.user_is_organization_manager)
|
||||
response = client.delete(url, data=self.delete_data)
|
||||
|
||||
assert response.status_code == 302 and response.url == self.url_delete_response
|
||||
|
||||
|
||||
class OrganizationManagerModelPermissions(
|
||||
OrganizationManagerModelPermissionView,
|
||||
OrganizationManagerModelPermissionAdd,
|
||||
OrganizationManagerModelPermissionChange,
|
||||
OrganizationManagerModelPermissionDelete
|
||||
):
|
||||
""" Tests for checking Organization Manager model permissions
|
||||
|
||||
This class includes all test cases for: Add, Change, Delete and View.
|
||||
"""
|
||||
|
||||
app_namespace: str = None
|
88
app/access/tests/abstract/tenancy_object.py
Normal file
88
app/access/tests/abstract/tenancy_object.py
Normal file
@ -0,0 +1,88 @@
|
||||
import pytest
|
||||
import unittest
|
||||
|
||||
from access.models import TenancyManager
|
||||
|
||||
|
||||
|
||||
class TenancyObject:
|
||||
""" Tests for checking TenancyObject """
|
||||
|
||||
model = None
|
||||
""" Model to be tested """
|
||||
|
||||
|
||||
def test_has_attr_get_organization(self):
|
||||
""" TenancyObject attribute check
|
||||
|
||||
TenancyObject has function get_organization
|
||||
"""
|
||||
|
||||
assert hasattr(self.model, 'get_organization')
|
||||
|
||||
|
||||
def test_has_attr_is_global(self):
|
||||
""" TenancyObject attribute check
|
||||
|
||||
TenancyObject has field is_global
|
||||
"""
|
||||
|
||||
assert hasattr(self.model, 'is_global')
|
||||
|
||||
|
||||
|
||||
def test_has_attr_model_notes(self):
|
||||
""" TenancyObject attribute check
|
||||
|
||||
TenancyObject has field model_notes
|
||||
"""
|
||||
|
||||
assert hasattr(self.model, 'model_notes')
|
||||
|
||||
|
||||
|
||||
def test_has_attr_organization(self):
|
||||
""" TenancyObject attribute check
|
||||
|
||||
TenancyObject has field organization
|
||||
"""
|
||||
|
||||
assert hasattr(self.model, 'organization')
|
||||
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_create_no_organization_fails(self):
|
||||
""" Devices must be assigned an organization
|
||||
|
||||
Must not be able to create an item without an organization
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_edit_no_organization_fails(self):
|
||||
""" Devices must be assigned an organization
|
||||
|
||||
Must not be able to edit an item without an organization
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def test_has_attr_organization(self):
|
||||
""" TenancyObject attribute check
|
||||
|
||||
TenancyObject has function objects
|
||||
"""
|
||||
|
||||
assert hasattr(self.model, 'objects')
|
||||
|
||||
|
||||
def test_attribute_is_type_objects(self):
|
||||
""" Attribute Check
|
||||
|
||||
attribute `objects` must be set to `access.models.TenancyManager()`
|
||||
"""
|
||||
|
||||
assert type(self.model.objects) is TenancyManager
|
0
app/access/tests/unit/__init__.py
Normal file
0
app/access/tests/unit/__init__.py
Normal file
371
app/access/tests/unit/organization/test_organizaiton_api.py
Normal file
371
app/access/tests/unit/organization/test_organizaiton_api.py
Normal file
@ -0,0 +1,371 @@
|
||||
import pytest
|
||||
import unittest
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import AnonymousUser, User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.shortcuts import reverse
|
||||
from django.test import Client, TestCase
|
||||
|
||||
from rest_framework.relations import Hyperlink
|
||||
|
||||
from access.models import Organization, Team, TeamUsers, Permission
|
||||
|
||||
|
||||
|
||||
class OrganizationAPI(TestCase):
|
||||
|
||||
model = Organization
|
||||
|
||||
app_namespace = 'API'
|
||||
|
||||
url_name = '_api_organization'
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
"""Setup Test
|
||||
|
||||
1. Create an organization for user and item
|
||||
. create an organization that is different to item
|
||||
2. Create a device
|
||||
3. create teams with each permission: view, add, change, delete
|
||||
4. create a user per team
|
||||
"""
|
||||
|
||||
organization = Organization.objects.create(name='test_org')
|
||||
|
||||
self.organization = organization
|
||||
|
||||
different_organization = Organization.objects.create(name='test_different_organization')
|
||||
|
||||
|
||||
self.item = organization
|
||||
|
||||
self.url_view_kwargs = {'pk': self.item.id}
|
||||
|
||||
self.url_kwargs = {'pk': self.item.id}
|
||||
|
||||
view_permissions = Permission.objects.get(
|
||||
codename = 'view_' + self.model._meta.model_name,
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = self.model._meta.app_label,
|
||||
model = self.model._meta.model_name,
|
||||
)
|
||||
)
|
||||
|
||||
view_team = Team.objects.create(
|
||||
team_name = 'view_team',
|
||||
organization = organization,
|
||||
)
|
||||
|
||||
view_team.permissions.set([view_permissions])
|
||||
|
||||
|
||||
self.view_user = User.objects.create_user(username="test_user_view", password="password")
|
||||
teamuser = TeamUsers.objects.create(
|
||||
team = view_team,
|
||||
user = self.view_user
|
||||
)
|
||||
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs)
|
||||
|
||||
|
||||
client.force_login(self.view_user)
|
||||
response = client.get(url)
|
||||
|
||||
self.api_data = response.data
|
||||
|
||||
|
||||
|
||||
def test_api_field_exists_id(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
id field must exist
|
||||
"""
|
||||
|
||||
assert 'id' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_id(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
id field must be int
|
||||
"""
|
||||
|
||||
assert type(self.api_data['id']) is int
|
||||
|
||||
|
||||
def test_api_field_exists_name(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
name field must exist
|
||||
"""
|
||||
|
||||
assert 'name' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_name(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
name field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['name']) is str
|
||||
|
||||
|
||||
def test_api_field_exists_teams(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
teams field must exist
|
||||
"""
|
||||
|
||||
assert 'teams' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_teams(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
teams field must be list
|
||||
"""
|
||||
|
||||
assert type(self.api_data['teams']) is list
|
||||
|
||||
|
||||
def test_api_field_exists_url(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
url field must exist
|
||||
"""
|
||||
|
||||
assert 'url' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_url(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
url field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['url']) is Hyperlink
|
||||
|
||||
|
||||
|
||||
|
||||
def test_api_field_exists_teams_id(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
teams.id field must exist
|
||||
"""
|
||||
|
||||
assert 'id' in self.api_data['teams'][0]
|
||||
|
||||
|
||||
def test_api_field_type_teams_id(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
teams.id field must be int
|
||||
"""
|
||||
|
||||
assert type(self.api_data['teams'][0]['id']) is int
|
||||
|
||||
|
||||
def test_api_field_exists_teams_team_name(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
teams.team_name field must exist
|
||||
"""
|
||||
|
||||
assert 'team_name' in self.api_data['teams'][0]
|
||||
|
||||
|
||||
def test_api_field_type_teams_team_name(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
teams.team_name field must be string
|
||||
"""
|
||||
|
||||
assert type(self.api_data['teams'][0]['team_name']) is str
|
||||
|
||||
|
||||
def test_api_field_exists_teams_permissions(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
teams.permissions field must exist
|
||||
"""
|
||||
|
||||
assert 'permissions' in self.api_data['teams'][0]
|
||||
|
||||
|
||||
def test_api_field_type_teams_permissions(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
teams.permissions field must be list
|
||||
"""
|
||||
|
||||
assert type(self.api_data['teams'][0]['permissions']) is list
|
||||
|
||||
|
||||
def test_api_field_exists_teams_permissions_url(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
teams.permissions_url field must exist
|
||||
"""
|
||||
|
||||
assert 'permissions_url' in self.api_data['teams'][0]
|
||||
|
||||
|
||||
def test_api_field_type_teams_permissions_url(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
teams.permissions_url field must be url
|
||||
"""
|
||||
|
||||
assert type(self.api_data['teams'][0]['permissions_url']) is str
|
||||
|
||||
|
||||
def test_api_field_exists_teams_url(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
teams.url field must exist
|
||||
"""
|
||||
|
||||
assert 'url' in self.api_data['teams'][0]
|
||||
|
||||
|
||||
def test_api_field_type_teams_url(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
teams.url field must be url
|
||||
"""
|
||||
|
||||
assert type(self.api_data['teams'][0]['url']) is str
|
||||
|
||||
|
||||
|
||||
def test_api_field_exists_teams_permissions_id(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
teams.permissions.id field must exist
|
||||
"""
|
||||
|
||||
assert 'id' in self.api_data['teams'][0]['permissions'][0]
|
||||
|
||||
|
||||
def test_api_field_type_teams_permissions_id(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
teams.permissions.id field must be int
|
||||
"""
|
||||
|
||||
assert type(self.api_data['teams'][0]['permissions'][0]['id']) is int
|
||||
|
||||
|
||||
def test_api_field_exists_teams_permissions_name(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
teams.permissions.name field must exist
|
||||
"""
|
||||
|
||||
assert 'name' in self.api_data['teams'][0]['permissions'][0]
|
||||
|
||||
|
||||
def test_api_field_type_teams_permissions_name(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
teams.permissions.name field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['teams'][0]['permissions'][0]['name']) is str
|
||||
|
||||
|
||||
def test_api_field_exists_teams_permissions_codename(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
teams.permissions.codename field must exist
|
||||
"""
|
||||
|
||||
assert 'codename' in self.api_data['teams'][0]['permissions'][0]
|
||||
|
||||
|
||||
def test_api_field_type_teams_permissions_codename(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
teams.permissions.codename field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['teams'][0]['permissions'][0]['codename']) is str
|
||||
|
||||
|
||||
def test_api_field_exists_teams_permissions_content_type(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
teams.permissions.content_type field must exist
|
||||
"""
|
||||
|
||||
assert 'content_type' in self.api_data['teams'][0]['permissions'][0]
|
||||
|
||||
|
||||
def test_api_field_type_teams_permissions_content_type(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
teams.permissions.content_type field must be dict
|
||||
"""
|
||||
|
||||
assert type(self.api_data['teams'][0]['permissions'][0]['content_type']) is dict
|
||||
|
||||
|
||||
|
||||
def test_api_field_exists_teams_permissions_content_type_id(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
teams.permissions.content_type.id field must exist
|
||||
"""
|
||||
|
||||
assert 'id' in self.api_data['teams'][0]['permissions'][0]['content_type']
|
||||
|
||||
|
||||
def test_api_field_type_teams_permissions_content_type_id(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
teams.permissions.content_type.id field must be int
|
||||
"""
|
||||
|
||||
assert type(self.api_data['teams'][0]['permissions'][0]['content_type']['id']) is int
|
||||
|
||||
|
||||
def test_api_field_exists_teams_permissions_content_type_app_label(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
teams.permissions.content_type.app_label field must exist
|
||||
"""
|
||||
|
||||
assert 'app_label' in self.api_data['teams'][0]['permissions'][0]['content_type']
|
||||
|
||||
|
||||
def test_api_field_type_teams_permissions_content_type_app_label(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
teams.permissions.content_type.app_label field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['teams'][0]['permissions'][0]['content_type']['app_label']) is str
|
||||
|
||||
|
||||
def test_api_field_exists_teams_permissions_content_type_model(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
teams.permissions.content_type.model field must exist
|
||||
"""
|
||||
|
||||
assert 'model' in self.api_data['teams'][0]['permissions'][0]['content_type']
|
||||
|
||||
|
||||
def test_api_field_type_teams_permissions_content_type_model(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
teams.permissions.content_type.model field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['teams'][0]['permissions'][0]['content_type']['model']) is str
|
@ -0,0 +1,214 @@
|
||||
# from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import AnonymousUser, User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.shortcuts import reverse
|
||||
from django.test import TestCase, Client
|
||||
|
||||
import pytest
|
||||
import unittest
|
||||
import requests
|
||||
|
||||
from access.models import Organization, Team, TeamUsers, Permission
|
||||
from access.tests.abstract.model_permissions_organization_manager import OrganizationManagerModelPermissionChange, OrganizationManagerModelPermissionView
|
||||
|
||||
from app.tests.abstract.model_permissions import ModelPermissionsView, ModelPermissionsChange
|
||||
|
||||
|
||||
class OrganizationPermissions(
|
||||
TestCase,
|
||||
ModelPermissionsView,
|
||||
ModelPermissionsChange,
|
||||
OrganizationManagerModelPermissionChange,
|
||||
OrganizationManagerModelPermissionView,
|
||||
):
|
||||
|
||||
model = Organization
|
||||
|
||||
app_namespace = 'Access'
|
||||
|
||||
url_name_view = '_organization_view'
|
||||
|
||||
# url_name_add = '_organization_add'
|
||||
|
||||
url_name_change = '_organization_view'
|
||||
|
||||
# url_name_delete = '_organization_delete'
|
||||
|
||||
# url_delete_response = reverse('ITAM:Operating Systems')
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
"""Setup Test
|
||||
|
||||
1. Create an organization for user and item
|
||||
. create an organization that is different to item
|
||||
2. Create a device
|
||||
3. create teams with each permission: view, add, change, delete
|
||||
4. create a user per team
|
||||
"""
|
||||
|
||||
organization = Organization.objects.create(name='test_org')
|
||||
|
||||
self.organization = organization
|
||||
|
||||
different_organization = Organization.objects.create(
|
||||
name='test_different_organization'
|
||||
)
|
||||
|
||||
self.different_organization = different_organization
|
||||
|
||||
|
||||
# self.item = self.model.objects.create(
|
||||
# organization=organization,
|
||||
# name = 'deviceone'
|
||||
# )
|
||||
|
||||
self.item = organization
|
||||
|
||||
|
||||
self.url_view_kwargs = {'pk': self.item.id}
|
||||
|
||||
# self.url_add_kwargs = {'pk': self.item.id}
|
||||
|
||||
# self.add_data = {'operating_system': 'operating_system', 'organization': self.organization.id}
|
||||
|
||||
self.url_change_kwargs = {'pk': self.item.id}
|
||||
|
||||
self.change_data = {'operating_system': 'operating_system', 'organization': self.organization.id}
|
||||
|
||||
# self.url_delete_kwargs = {'pk': self.item.id}
|
||||
|
||||
# self.delete_data = {'operating_system': 'operating_system', 'organization': self.organization.id}
|
||||
|
||||
|
||||
view_permissions = Permission.objects.get(
|
||||
codename = 'view_' + self.model._meta.model_name,
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = self.model._meta.app_label,
|
||||
model = self.model._meta.model_name,
|
||||
)
|
||||
)
|
||||
|
||||
view_team = Team.objects.create(
|
||||
team_name = 'view_team',
|
||||
organization = organization,
|
||||
)
|
||||
|
||||
view_team.permissions.set([view_permissions])
|
||||
|
||||
|
||||
|
||||
add_permissions = Permission.objects.get(
|
||||
codename = 'add_' + self.model._meta.model_name,
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = self.model._meta.app_label,
|
||||
model = self.model._meta.model_name,
|
||||
)
|
||||
)
|
||||
|
||||
add_team = Team.objects.create(
|
||||
team_name = 'add_team',
|
||||
organization = organization,
|
||||
)
|
||||
|
||||
add_team.permissions.set([add_permissions])
|
||||
|
||||
|
||||
|
||||
change_permissions = Permission.objects.get(
|
||||
codename = 'change_' + self.model._meta.model_name,
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = self.model._meta.app_label,
|
||||
model = self.model._meta.model_name,
|
||||
)
|
||||
)
|
||||
|
||||
change_team = Team.objects.create(
|
||||
team_name = 'change_team',
|
||||
organization = organization,
|
||||
)
|
||||
|
||||
change_team.permissions.set([change_permissions])
|
||||
|
||||
|
||||
|
||||
delete_permissions = Permission.objects.get(
|
||||
codename = 'delete_' + self.model._meta.model_name,
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = self.model._meta.app_label,
|
||||
model = self.model._meta.model_name,
|
||||
)
|
||||
)
|
||||
|
||||
delete_team = Team.objects.create(
|
||||
team_name = 'delete_team',
|
||||
organization = organization,
|
||||
)
|
||||
|
||||
delete_team.permissions.set([delete_permissions])
|
||||
|
||||
|
||||
self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password")
|
||||
|
||||
|
||||
self.view_user = User.objects.create_user(username="test_user_view", password="password")
|
||||
teamuser = TeamUsers.objects.create(
|
||||
team = view_team,
|
||||
user = self.view_user
|
||||
)
|
||||
|
||||
self.add_user = User.objects.create_user(username="test_user_add", password="password")
|
||||
teamuser = TeamUsers.objects.create(
|
||||
team = add_team,
|
||||
user = self.add_user
|
||||
)
|
||||
|
||||
self.change_user = User.objects.create_user(username="test_user_change", password="password")
|
||||
teamuser = TeamUsers.objects.create(
|
||||
team = change_team,
|
||||
user = self.change_user
|
||||
)
|
||||
|
||||
self.delete_user = User.objects.create_user(username="test_user_delete", password="password")
|
||||
teamuser = TeamUsers.objects.create(
|
||||
team = delete_team,
|
||||
user = self.delete_user
|
||||
)
|
||||
|
||||
|
||||
self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password")
|
||||
|
||||
|
||||
different_organization_team = Team.objects.create(
|
||||
team_name = 'different_organization_team',
|
||||
organization = different_organization,
|
||||
)
|
||||
|
||||
different_organization_team.permissions.set([
|
||||
view_permissions,
|
||||
add_permissions,
|
||||
change_permissions,
|
||||
delete_permissions,
|
||||
])
|
||||
|
||||
TeamUsers.objects.create(
|
||||
team = different_organization_team,
|
||||
user = self.different_organization_user
|
||||
)
|
||||
|
||||
self.user_is_organization_manager = User.objects.create_user(
|
||||
username="test_org_manager",
|
||||
password="password"
|
||||
)
|
||||
|
||||
self.organization.manager = self.user_is_organization_manager
|
||||
self.organization.save()
|
||||
|
||||
self.different_organization_is_manager = User.objects.create_user(
|
||||
username="test_org_manager_different_org",
|
||||
password="password"
|
||||
)
|
||||
|
||||
self.different_organization.manager = self.different_organization_is_manager
|
||||
self.different_organization.save()
|
@ -0,0 +1,239 @@
|
||||
import pytest
|
||||
import unittest
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import AnonymousUser, User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.shortcuts import reverse
|
||||
from django.test import Client, TestCase
|
||||
|
||||
from access.models import Organization, Team, TeamUsers, Permission
|
||||
|
||||
from api.tests.abstract.api_permissions import APIPermissionChange, APIPermissionView
|
||||
|
||||
|
||||
|
||||
class OrganizationPermissionsAPI(TestCase, APIPermissionChange, APIPermissionView):
|
||||
|
||||
model = Organization
|
||||
|
||||
model_name = 'organization'
|
||||
app_label = 'access'
|
||||
|
||||
app_namespace = 'API'
|
||||
|
||||
url_name = '_api_organization'
|
||||
|
||||
url_list = '_api_orgs'
|
||||
|
||||
change_data = {'name': 'device'}
|
||||
|
||||
# delete_data = {'device': 'device'}
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
"""Setup Test
|
||||
|
||||
1. Create an organization for user and item
|
||||
. create an organization that is different to item
|
||||
2. Create a device
|
||||
3. create teams with each permission: view, add, change, delete
|
||||
4. create a user per team
|
||||
"""
|
||||
|
||||
organization = Organization.objects.create(name='test_org')
|
||||
|
||||
self.organization = organization
|
||||
|
||||
different_organization = Organization.objects.create(name='test_different_organization')
|
||||
|
||||
|
||||
self.item = organization
|
||||
|
||||
self.url_view_kwargs = {'pk': self.item.id}
|
||||
|
||||
self.url_kwargs = {'pk': self.item.id}
|
||||
|
||||
# self.add_data = {'name': 'device', 'organization': self.organization.id}
|
||||
|
||||
|
||||
view_permissions = Permission.objects.get(
|
||||
codename = 'view_' + self.model._meta.model_name,
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = self.model._meta.app_label,
|
||||
model = self.model._meta.model_name,
|
||||
)
|
||||
)
|
||||
|
||||
view_team = Team.objects.create(
|
||||
team_name = 'view_team',
|
||||
organization = organization,
|
||||
)
|
||||
|
||||
view_team.permissions.set([view_permissions])
|
||||
|
||||
|
||||
|
||||
add_permissions = Permission.objects.get(
|
||||
codename = 'add_' + self.model._meta.model_name,
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = self.model._meta.app_label,
|
||||
model = self.model._meta.model_name,
|
||||
)
|
||||
)
|
||||
|
||||
add_team = Team.objects.create(
|
||||
team_name = 'add_team',
|
||||
organization = organization,
|
||||
)
|
||||
|
||||
add_team.permissions.set([add_permissions])
|
||||
|
||||
|
||||
|
||||
change_permissions = Permission.objects.get(
|
||||
codename = 'change_' + self.model._meta.model_name,
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = self.model._meta.app_label,
|
||||
model = self.model._meta.model_name,
|
||||
)
|
||||
)
|
||||
|
||||
change_team = Team.objects.create(
|
||||
team_name = 'change_team',
|
||||
organization = organization,
|
||||
)
|
||||
|
||||
change_team.permissions.set([change_permissions])
|
||||
|
||||
|
||||
|
||||
delete_permissions = Permission.objects.get(
|
||||
codename = 'delete_' + self.model._meta.model_name,
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = self.model._meta.app_label,
|
||||
model = self.model._meta.model_name,
|
||||
)
|
||||
)
|
||||
|
||||
delete_team = Team.objects.create(
|
||||
team_name = 'delete_team',
|
||||
organization = organization,
|
||||
)
|
||||
|
||||
delete_team.permissions.set([delete_permissions])
|
||||
|
||||
|
||||
self.super_user = User.objects.create_user(username="super_user", password="password", is_superuser=True)
|
||||
|
||||
self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password")
|
||||
|
||||
|
||||
self.view_user = User.objects.create_user(username="test_user_view", password="password")
|
||||
teamuser = TeamUsers.objects.create(
|
||||
team = view_team,
|
||||
user = self.view_user
|
||||
)
|
||||
|
||||
self.add_user = User.objects.create_user(username="test_user_add", password="password")
|
||||
teamuser = TeamUsers.objects.create(
|
||||
team = add_team,
|
||||
user = self.add_user
|
||||
)
|
||||
|
||||
self.change_user = User.objects.create_user(username="test_user_change", password="password")
|
||||
teamuser = TeamUsers.objects.create(
|
||||
team = change_team,
|
||||
user = self.change_user
|
||||
)
|
||||
|
||||
self.delete_user = User.objects.create_user(username="test_user_delete", password="password")
|
||||
teamuser = TeamUsers.objects.create(
|
||||
team = delete_team,
|
||||
user = self.delete_user
|
||||
)
|
||||
|
||||
|
||||
self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password")
|
||||
|
||||
|
||||
different_organization_team = Team.objects.create(
|
||||
team_name = 'different_organization_team',
|
||||
organization = different_organization,
|
||||
)
|
||||
|
||||
different_organization_team.permissions.set([
|
||||
view_permissions,
|
||||
add_permissions,
|
||||
change_permissions,
|
||||
delete_permissions,
|
||||
])
|
||||
|
||||
TeamUsers.objects.create(
|
||||
team = different_organization_team,
|
||||
user = self.different_organization_user
|
||||
)
|
||||
|
||||
|
||||
def test_add_is_prohibited_anon_user(self):
|
||||
""" Ensure Organization cant be created
|
||||
|
||||
Attempt to create organization as anon user
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.app_namespace + ':' + self.url_list)
|
||||
|
||||
|
||||
# client.force_login(self.add_user)
|
||||
response = client.post(url, data={'name': 'should not create'}, content_type='application/json')
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
def test_add_is_prohibited_diff_org_user(self):
|
||||
""" Ensure Organization cant be created
|
||||
|
||||
Attempt to create organization as user with different org permissions.
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.app_namespace + ':' + self.url_list)
|
||||
|
||||
|
||||
client.force_login(self.different_organization_user)
|
||||
response = client.post(url, data={'name': 'should not create'}, content_type='application/json')
|
||||
|
||||
assert response.status_code == 405
|
||||
|
||||
|
||||
def test_add_is_prohibited_super_user(self):
|
||||
""" Ensure Organization cant be created
|
||||
|
||||
Attempt to create organization as user who is super user
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.app_namespace + ':' + self.url_list)
|
||||
|
||||
|
||||
client.force_login(self.super_user)
|
||||
response = client.post(url, data={'name': 'should not create'}, content_type='application/json')
|
||||
|
||||
assert response.status_code == 405
|
||||
|
||||
|
||||
def test_add_is_prohibited_user_same_org(self):
|
||||
""" Ensure Organization cant be created
|
||||
|
||||
Attempt to create organization as user with permission
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.app_namespace + ':' + self.url_list)
|
||||
|
||||
|
||||
client.force_login(self.add_user)
|
||||
response = client.post(url, data={'name': 'should not create'}, content_type='application/json')
|
||||
|
||||
assert response.status_code == 405
|
10
app/access/tests/unit/organization/test_organization.py
Normal file
10
app/access/tests/unit/organization/test_organization.py
Normal file
@ -0,0 +1,10 @@
|
||||
from django.test import TestCase
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
import unittest
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.exceptions import ValidationError
|
||||
from access.models import Organization, Team
|
||||
|
@ -0,0 +1,187 @@
|
||||
|
||||
import pytest
|
||||
import unittest
|
||||
import requests
|
||||
|
||||
from django.test import TestCase, Client
|
||||
|
||||
from access.models import Organization
|
||||
|
||||
from core.models.history import History
|
||||
|
||||
from access.models import Organization
|
||||
|
||||
|
||||
|
||||
class OrganizationHistory(TestCase):
|
||||
|
||||
model = Organization
|
||||
|
||||
model_name = 'organization'
|
||||
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
""" Setup Test """
|
||||
|
||||
organization = Organization.objects.create(name='test_org')
|
||||
|
||||
self.organization = organization
|
||||
|
||||
self.item_create = self.model.objects.create(
|
||||
name = 'test_item_' + self.model_name,
|
||||
)
|
||||
|
||||
|
||||
self.history_create = History.objects.get(
|
||||
action = History.Actions.ADD[0],
|
||||
item_pk = self.item_create.pk,
|
||||
item_class = self.model._meta.model_name,
|
||||
)
|
||||
|
||||
self.item_change = self.item_create
|
||||
self.item_change.name = 'test_item_' + self.model_name + '_changed'
|
||||
self.item_change.save()
|
||||
|
||||
self.history_change = History.objects.get(
|
||||
action = History.Actions.UPDATE[0],
|
||||
item_pk = self.item_change.pk,
|
||||
item_class = self.model._meta.model_name,
|
||||
)
|
||||
|
||||
self.item_delete = self.model.objects.create(
|
||||
name = 'test_item_delete_' + self.model_name,
|
||||
)
|
||||
|
||||
self.item_delete.delete()
|
||||
|
||||
self.history_delete = History.objects.filter(
|
||||
item_pk = self.item_delete.pk,
|
||||
item_class = self.model._meta.model_name,
|
||||
)
|
||||
|
||||
self.history_delete_children = History.objects.filter(
|
||||
item_parent_pk = self.item_delete.pk,
|
||||
item_parent_class = self.model._meta.model_name,
|
||||
)
|
||||
|
||||
|
||||
|
||||
def test_history_entry_item_add_field_action(self):
|
||||
""" Ensure action is "add" for item creation """
|
||||
|
||||
history = self.history_create.__dict__
|
||||
|
||||
assert history['action'] == int(History.Actions.ADD[0])
|
||||
# assert type(history['action']) is int
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_history_entry_item_add_field_after(self):
|
||||
""" Ensure after field contains correct value """
|
||||
|
||||
history = self.history_create.__dict__
|
||||
|
||||
assert history['after'] == str('{}')
|
||||
# assert type(history['after']) is str
|
||||
|
||||
|
||||
def test_history_entry_item_add_field_before(self):
|
||||
""" Ensure before field is an empty JSON string for create """
|
||||
|
||||
history = self.history_create.__dict__
|
||||
|
||||
assert history['before'] == str('{}')
|
||||
# assert type(history['before']) is str
|
||||
|
||||
|
||||
def test_history_entry_item_add_field_item_pk(self):
|
||||
""" Ensure history entry field item_pk is the created items pk """
|
||||
|
||||
history = self.history_create.__dict__
|
||||
|
||||
assert history['item_pk'] == self.item_create.pk
|
||||
# assert type(history['item_pk']) is int
|
||||
|
||||
|
||||
def test_history_entry_item_add_field_item_class(self):
|
||||
""" Ensure history entry field item_class is the model name """
|
||||
|
||||
history = self.history_create.__dict__
|
||||
|
||||
assert history['item_class'] == self.model._meta.model_name
|
||||
# assert type(history['item_class']) is str
|
||||
|
||||
|
||||
|
||||
|
||||
################################## Change ##################################
|
||||
|
||||
|
||||
|
||||
|
||||
def test_history_entry_item_change_field_action(self):
|
||||
""" Ensure action is "add" for item creation """
|
||||
|
||||
history = self.history_change.__dict__
|
||||
|
||||
assert history['action'] == int(History.Actions.UPDATE[0])
|
||||
# assert type(history['action']) is int
|
||||
|
||||
|
||||
def test_history_entry_item_change_field_after(self):
|
||||
""" Ensure after field contains correct value """
|
||||
|
||||
history = self.history_change.__dict__
|
||||
|
||||
assert history['after'] == str('{"name": "test_item_' + self.model_name + '_changed"}')
|
||||
# assert type(history['after']) is str
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_history_entry_item_change_field_before(self):
|
||||
""" Ensure before field is an empty JSON string for create """
|
||||
|
||||
history = self.history_change.__dict__
|
||||
|
||||
assert history['before'] == str('{}')
|
||||
# assert type(history['before']) is str
|
||||
|
||||
|
||||
def test_history_entry_item_change_field_item_pk(self):
|
||||
""" Ensure history entry field item_pk is the created items pk """
|
||||
|
||||
history = self.history_change.__dict__
|
||||
|
||||
assert history['item_pk'] == self.item_create.pk
|
||||
# assert type(history['item_pk']) is int
|
||||
|
||||
|
||||
def test_history_entry_item_change_field_item_class(self):
|
||||
""" Ensure history entry field item_class is the model name """
|
||||
|
||||
history = self.history_change.__dict__
|
||||
|
||||
assert history['item_class'] == self.model._meta.model_name
|
||||
# assert type(history['item_class']) is str
|
||||
|
||||
|
||||
|
||||
|
||||
################################## Delete ##################################
|
||||
|
||||
|
||||
|
||||
|
||||
def test_device_history_entry_delete(self):
|
||||
""" When an item is deleted, it's history entries must be removed """
|
||||
|
||||
assert self.history_delete.exists() is False
|
||||
|
||||
|
||||
def test_device_history_entry_children_delete(self):
|
||||
""" When an item is deleted, it's history entries must be removed """
|
||||
|
||||
assert self.history_delete_children.exists() is False
|
||||
|
||||
|
@ -0,0 +1,165 @@
|
||||
# from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import AnonymousUser, User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.shortcuts import reverse
|
||||
from django.test import TestCase, Client
|
||||
|
||||
import pytest
|
||||
import unittest
|
||||
import requests
|
||||
|
||||
from access.models import Organization, Team, TeamUsers, Permission
|
||||
|
||||
from core.models.history import History
|
||||
|
||||
|
||||
class OrganizationHistoryPermissions(TestCase):
|
||||
|
||||
|
||||
item_model = Organization
|
||||
|
||||
|
||||
model = History
|
||||
|
||||
model_name = 'history'
|
||||
|
||||
app_label = 'core'
|
||||
|
||||
namespace = ''
|
||||
|
||||
name_view = '_history'
|
||||
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
"""Setup Test
|
||||
|
||||
1. Create an organization for user and item
|
||||
2. create an organization that is different to item
|
||||
3. Create a device
|
||||
4. Add history device history entry as item
|
||||
5. create a user
|
||||
6. create user in different organization (with the required permission)
|
||||
"""
|
||||
|
||||
organization = Organization.objects.create(name='test_org')
|
||||
|
||||
self.organization = organization
|
||||
|
||||
different_organization = Organization.objects.create(name='test_different_organization')
|
||||
|
||||
self.item = self.organization
|
||||
|
||||
self.history_model_name = self.item._meta.model_name
|
||||
|
||||
self.history = self.model.objects.get(
|
||||
item_pk = self.item.id,
|
||||
item_class = self.item._meta.model_name,
|
||||
action = self.model.Actions.ADD,
|
||||
)
|
||||
|
||||
view_permissions = Permission.objects.get(
|
||||
codename = 'view_' + self.model_name,
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = self.app_label,
|
||||
model = self.model_name,
|
||||
)
|
||||
)
|
||||
|
||||
view_team = Team.objects.create(
|
||||
team_name = 'view_team',
|
||||
organization = organization,
|
||||
)
|
||||
|
||||
view_team.permissions.set([view_permissions])
|
||||
|
||||
|
||||
self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password")
|
||||
|
||||
|
||||
self.view_user = User.objects.create_user(username="test_user_view", password="password")
|
||||
teamuser = TeamUsers.objects.create(
|
||||
team = view_team,
|
||||
user = self.view_user
|
||||
)
|
||||
|
||||
self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password")
|
||||
|
||||
|
||||
different_organization_team = Team.objects.create(
|
||||
team_name = 'different_organization_team',
|
||||
organization = different_organization,
|
||||
)
|
||||
|
||||
different_organization_team.permissions.set([
|
||||
view_permissions,
|
||||
])
|
||||
|
||||
TeamUsers.objects.create(
|
||||
team = different_organization_team,
|
||||
user = self.different_organization_user
|
||||
)
|
||||
|
||||
|
||||
|
||||
def test_auth_view_history_user_anon_denied(self):
|
||||
""" Check correct permission for view
|
||||
|
||||
Attempt to view as anon user
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.namespace + self.name_view, kwargs={'model_name': self.history_model_name, 'model_pk': self.item.id})
|
||||
|
||||
response = client.get(url)
|
||||
|
||||
assert response.status_code == 302 and response.url.startswith('/account/login')
|
||||
|
||||
|
||||
def test_auth_view_history_no_permission_denied(self):
|
||||
""" Check correct permission for view
|
||||
|
||||
Attempt to view with user missing permission
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.namespace + self.name_view, kwargs={'model_name': self.history_model_name, 'model_pk': self.item.id})
|
||||
|
||||
|
||||
client.force_login(self.no_permissions_user)
|
||||
response = client.get(url)
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
def test_auth_view_history_different_organizaiton_denied(self):
|
||||
""" Check correct permission for view
|
||||
|
||||
Attempt to view with user from different organization
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.namespace + self.name_view, kwargs={'model_name': self.history_model_name, 'model_pk': self.item.id})
|
||||
|
||||
|
||||
client.force_login(self.different_organization_user)
|
||||
response = client.get(url)
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
def test_auth_view_history_has_permission(self):
|
||||
""" Check correct permission for view
|
||||
|
||||
Attempt to view as user with view permission
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.namespace + self.name_view, kwargs={'model_name': self.history_model_name, 'model_pk': self.item.id})
|
||||
|
||||
|
||||
client.force_login(self.view_user)
|
||||
response = client.get(url)
|
||||
|
||||
assert response.status_code == 200
|
@ -0,0 +1,21 @@
|
||||
import pytest
|
||||
import unittest
|
||||
import requests
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from app.tests.abstract.models import ModelDisplay, ModelIndex
|
||||
|
||||
|
||||
|
||||
class OrganizationViews(
|
||||
TestCase,
|
||||
ModelDisplay,
|
||||
ModelIndex
|
||||
):
|
||||
|
||||
display_module = 'access.views.organization'
|
||||
display_view = 'View'
|
||||
|
||||
index_module = display_module
|
||||
index_view = 'IndexView'
|
70
app/access/tests/unit/team/test_team.py
Normal file
70
app/access/tests/unit/team/test_team.py
Normal file
@ -0,0 +1,70 @@
|
||||
import pytest
|
||||
import unittest
|
||||
|
||||
from django.test import TestCase, Client
|
||||
|
||||
from access.models import Organization, Team, TeamUsers, Permission
|
||||
|
||||
from app.tests.abstract.models import TenancyModel
|
||||
|
||||
|
||||
|
||||
class TeamModel(
|
||||
TestCase,
|
||||
TenancyModel
|
||||
):
|
||||
|
||||
model = Team
|
||||
|
||||
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
""" Setup Test
|
||||
|
||||
"""
|
||||
|
||||
self.parent_item = Organization.objects.create(name='test_org')
|
||||
|
||||
different_organization = Organization.objects.create(name='test_different_organization')
|
||||
|
||||
self.item = self.model.objects.create(
|
||||
organization=self.parent_item,
|
||||
name = 'teamone'
|
||||
)
|
||||
|
||||
|
||||
def test_model_has_property_parent_object(self):
|
||||
""" Check if model contains 'parent_object'
|
||||
|
||||
This is a required property for all models that have a parent
|
||||
"""
|
||||
|
||||
assert hasattr(self.model, 'parent_object')
|
||||
|
||||
|
||||
def test_model_property_parent_object_returns_object(self):
|
||||
""" Check if model contains 'parent_object'
|
||||
|
||||
This is a required property for all models that have a parent
|
||||
"""
|
||||
|
||||
assert self.item.parent_object is self.parent_item
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_function_save_attributes():
|
||||
""" Ensure save Attributes function match django default
|
||||
|
||||
the save method is overridden. the function attributes must match default django method
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="uses Django group manager")
|
||||
def test_attribute_is_type_objects(self):
|
||||
pass
|
||||
|
||||
@pytest.mark.skip(reason="uses Django group manager")
|
||||
def test_model_class_tenancy_manager_function_get_queryset_called(self):
|
||||
pass
|
313
app/access/tests/unit/team/test_team_api.py
Normal file
313
app/access/tests/unit/team/test_team_api.py
Normal file
@ -0,0 +1,313 @@
|
||||
import pytest
|
||||
import unittest
|
||||
import requests
|
||||
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import AnonymousUser, User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.shortcuts import reverse
|
||||
from django.test import Client, TestCase
|
||||
|
||||
from rest_framework.relations import Hyperlink
|
||||
|
||||
from access.models import Organization, Team, TeamUsers, Permission
|
||||
|
||||
# from api.tests.abstract.api_permissions import APIPermissions
|
||||
|
||||
|
||||
|
||||
class TeamAPI(TestCase):
|
||||
|
||||
model = Team
|
||||
|
||||
app_namespace = 'API'
|
||||
|
||||
url_name = '_api_team'
|
||||
|
||||
# url_list = '_api_organization_teams'
|
||||
|
||||
# change_data = {'name': 'device'}
|
||||
|
||||
# delete_data = {'device': 'device'}
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
"""Setup Test
|
||||
|
||||
1. Create an organization for user and item
|
||||
. create an organization that is different to item
|
||||
2. Create a team
|
||||
3. create teams with each permission: view, add, change, delete
|
||||
4. create a user per team
|
||||
"""
|
||||
|
||||
organization = Organization.objects.create(name='test_org')
|
||||
|
||||
self.organization = organization
|
||||
|
||||
different_organization = Organization.objects.create(name='test_different_organization')
|
||||
|
||||
|
||||
self.item = self.model.objects.create(
|
||||
organization=organization,
|
||||
team_name = 'teamone',
|
||||
model_notes = 'random note'
|
||||
)
|
||||
|
||||
|
||||
self.url_kwargs = {'organization_id': self.organization.id}
|
||||
|
||||
self.url_view_kwargs = {'organization_id': self.organization.id, 'group_ptr_id': self.item.id}
|
||||
|
||||
self.add_data = {'team_name': 'team_post'}
|
||||
|
||||
|
||||
view_permissions = Permission.objects.get(
|
||||
codename = 'view_' + self.model._meta.model_name,
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = self.model._meta.app_label,
|
||||
model = self.model._meta.model_name,
|
||||
)
|
||||
)
|
||||
|
||||
# view_team = Team.objects.create(
|
||||
# team_name = 'view_team',
|
||||
# organization = organization,
|
||||
# )
|
||||
|
||||
self.item.permissions.set([view_permissions])
|
||||
|
||||
self.view_user = User.objects.create_user(username="test_user_view", password="password")
|
||||
teamuser = TeamUsers.objects.create(
|
||||
team = self.item,
|
||||
user = self.view_user
|
||||
)
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs)
|
||||
|
||||
|
||||
client.force_login(self.view_user)
|
||||
response = client.get(url)
|
||||
|
||||
self.api_data = response.data
|
||||
|
||||
|
||||
|
||||
def test_api_field_exists_id(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
id field must exist
|
||||
"""
|
||||
|
||||
assert 'id' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_id(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
id field must be int
|
||||
"""
|
||||
|
||||
assert type(self.api_data['id']) is int
|
||||
|
||||
|
||||
def test_api_field_exists_team_name(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
team_name field must exist
|
||||
"""
|
||||
|
||||
assert 'team_name' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_name(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
team_name field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['team_name']) is str
|
||||
|
||||
|
||||
def test_api_field_exists_model_notes(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
model_notes field must exist
|
||||
"""
|
||||
|
||||
assert 'model_notes' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_model_notes(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
model_notes field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['model_notes']) is str
|
||||
|
||||
|
||||
def test_api_field_exists_url(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
url field must exist
|
||||
"""
|
||||
|
||||
assert 'url' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_url(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
url field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['url']) is str
|
||||
|
||||
|
||||
def test_api_field_exists_permissions(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
permissions field must exist
|
||||
"""
|
||||
|
||||
assert 'permissions' in self.api_data
|
||||
|
||||
|
||||
def test_api_field_type_permissions(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
url field must be list
|
||||
"""
|
||||
|
||||
assert type(self.api_data['permissions']) is list
|
||||
|
||||
|
||||
|
||||
def test_api_field_exists_permissions_id(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
permissions.id field must exist
|
||||
"""
|
||||
|
||||
assert 'id' in self.api_data['permissions'][0]
|
||||
|
||||
|
||||
def test_api_field_type_permissions_id(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
permissions.id field must be int
|
||||
"""
|
||||
|
||||
assert type(self.api_data['permissions'][0]['id']) is int
|
||||
|
||||
|
||||
def test_api_field_exists_permissions_name(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
permissions.name field must exist
|
||||
"""
|
||||
|
||||
assert 'name' in self.api_data['permissions'][0]
|
||||
|
||||
|
||||
def test_api_field_type_permissions_name(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
permissions.name field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['permissions'][0]['name']) is str
|
||||
|
||||
|
||||
def test_api_field_exists_permissions_codename(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
permissions.codename field must exist
|
||||
"""
|
||||
|
||||
assert 'codename' in self.api_data['permissions'][0]
|
||||
|
||||
|
||||
def test_api_field_type_permissions_codename(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
permissions.codename field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['permissions'][0]['codename']) is str
|
||||
|
||||
|
||||
def test_api_field_exists_permissions_content_type(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
permissions.content_type field must exist
|
||||
"""
|
||||
|
||||
assert 'content_type' in self.api_data['permissions'][0]
|
||||
|
||||
|
||||
def test_api_field_type_permissions_content_type(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
permissions.content_type field must be dict
|
||||
"""
|
||||
|
||||
assert type(self.api_data['permissions'][0]['content_type']) is dict
|
||||
|
||||
|
||||
|
||||
def test_api_field_exists_permissions_content_type_id(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
permissions.content_type.id field must exist
|
||||
"""
|
||||
|
||||
assert 'id' in self.api_data['permissions'][0]['content_type']
|
||||
|
||||
|
||||
def test_api_field_type_permissions_content_type_id(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
permissions.content_type.id field must be int
|
||||
"""
|
||||
|
||||
assert type(self.api_data['permissions'][0]['content_type']['id']) is int
|
||||
|
||||
|
||||
def test_api_field_exists_permissions_content_type_app_label(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
permissions.content_type.app_label field must exist
|
||||
"""
|
||||
|
||||
assert 'app_label' in self.api_data['permissions'][0]['content_type']
|
||||
|
||||
|
||||
def test_api_field_type_permissions_content_type_app_label(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
permissions.content_type.app_label field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['permissions'][0]['content_type']['app_label']) is str
|
||||
|
||||
|
||||
def test_api_field_exists_permissions_content_type_model(self):
|
||||
""" Test for existance of API Field
|
||||
|
||||
permissions.content_type.model field must exist
|
||||
"""
|
||||
|
||||
assert 'model' in self.api_data['permissions'][0]['content_type']
|
||||
|
||||
|
||||
def test_api_field_type_permissions_content_type_model(self):
|
||||
""" Test for type for API Field
|
||||
|
||||
permissions.content_type.model field must be str
|
||||
"""
|
||||
|
||||
assert type(self.api_data['permissions'][0]['content_type']['model']) is str
|
79
app/access/tests/unit/team/test_team_core_history.py
Normal file
79
app/access/tests/unit/team/test_team_core_history.py
Normal file
@ -0,0 +1,79 @@
|
||||
|
||||
import pytest
|
||||
import unittest
|
||||
import requests
|
||||
|
||||
from django.test import TestCase, Client
|
||||
|
||||
from access.models import Organization
|
||||
|
||||
from core.models.history import History
|
||||
from core.tests.abstract.history_entry import HistoryEntry
|
||||
from core.tests.abstract.history_entry_child_model import HistoryEntryChildItem
|
||||
|
||||
from access.models import Team
|
||||
|
||||
from django.contrib.auth.models import Group
|
||||
|
||||
|
||||
class TeamHistory(TestCase, HistoryEntry, HistoryEntryChildItem):
|
||||
|
||||
|
||||
model = Team
|
||||
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
""" Setup Test """
|
||||
|
||||
organization = Organization.objects.create(name='test_org')
|
||||
|
||||
self.organization = organization
|
||||
|
||||
self.item_parent = organization
|
||||
|
||||
self.item_create = self.model.objects.create(
|
||||
team_name = 'test_item_' + self.model._meta.model_name,
|
||||
organization = self.organization
|
||||
)
|
||||
|
||||
|
||||
self.history_create = History.objects.get(
|
||||
action = History.Actions.ADD[0],
|
||||
item_pk = self.item_create.pk,
|
||||
item_class = self.model._meta.model_name,
|
||||
)
|
||||
|
||||
self.item_change = self.item_create
|
||||
self.item_change.team_name = 'test_item_' + self.model._meta.model_name + '_changed'
|
||||
self.item_change.save()
|
||||
|
||||
self.field_after_expected_value = '{"name": "test_org_' + self.item_change.team_name + '", "team_name": "' + self.item_change.team_name + '"}'
|
||||
|
||||
self.history_change = History.objects.get(
|
||||
action = History.Actions.UPDATE[0],
|
||||
item_pk = self.item_change.pk,
|
||||
item_class = self.model._meta.model_name,
|
||||
)
|
||||
|
||||
debug = Group.objects.all()
|
||||
|
||||
self.item_delete = self.model.objects.create(
|
||||
team_name = 'test_item_delete_' + self.model._meta.model_name,
|
||||
organization = self.organization
|
||||
)
|
||||
|
||||
self.deleted_pk = self.item_delete.pk
|
||||
|
||||
self.item_delete.delete()
|
||||
|
||||
self.history_delete = History.objects.get(
|
||||
action = History.Actions.DELETE[0],
|
||||
item_pk = self.deleted_pk,
|
||||
item_class = self.model._meta.model_name,
|
||||
)
|
||||
|
||||
self.history_delete_children = History.objects.filter(
|
||||
item_parent_pk = self.deleted_pk,
|
||||
item_parent_class = self.item_parent._meta.model_name,
|
||||
)
|
168
app/access/tests/unit/team/test_team_history_permission.py
Normal file
168
app/access/tests/unit/team/test_team_history_permission.py
Normal file
@ -0,0 +1,168 @@
|
||||
# from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import AnonymousUser, User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.shortcuts import reverse
|
||||
from django.test import TestCase, Client
|
||||
|
||||
import pytest
|
||||
import unittest
|
||||
import requests
|
||||
|
||||
from access.models import Organization, Team, TeamUsers, Permission
|
||||
|
||||
from core.models.history import History
|
||||
|
||||
|
||||
class TeamHistoryPermissions(TestCase):
|
||||
|
||||
|
||||
item_model = Team
|
||||
|
||||
|
||||
model = History
|
||||
|
||||
model_name = 'history'
|
||||
|
||||
app_label = 'core'
|
||||
|
||||
namespace = ''
|
||||
|
||||
name_view = '_history'
|
||||
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
"""Setup Test
|
||||
|
||||
1. Create an organization for user and item
|
||||
2. create an organization that is different to item
|
||||
3. Create a device
|
||||
4. Add history device history entry as item
|
||||
5. create a user
|
||||
6. create user in different organization (with the required permission)
|
||||
"""
|
||||
|
||||
organization = Organization.objects.create(name='test_org')
|
||||
|
||||
self.organization = organization
|
||||
|
||||
different_organization = Organization.objects.create(name='test_different_organization')
|
||||
|
||||
self.item = self.item_model.objects.create(
|
||||
organization=organization,
|
||||
name = 'deviceone'
|
||||
)
|
||||
|
||||
self.history_model_name = self.item._meta.model_name
|
||||
|
||||
self.history = self.model.objects.get(
|
||||
item_pk = self.item.id,
|
||||
item_class = self.item._meta.model_name,
|
||||
action = self.model.Actions.ADD,
|
||||
)
|
||||
|
||||
view_permissions = Permission.objects.get(
|
||||
codename = 'view_' + self.model_name,
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = self.app_label,
|
||||
model = self.model_name,
|
||||
)
|
||||
)
|
||||
|
||||
view_team = Team.objects.create(
|
||||
team_name = 'view_team',
|
||||
organization = organization,
|
||||
)
|
||||
|
||||
view_team.permissions.set([view_permissions])
|
||||
|
||||
|
||||
self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password")
|
||||
|
||||
|
||||
self.view_user = User.objects.create_user(username="test_user_view", password="password")
|
||||
teamuser = TeamUsers.objects.create(
|
||||
team = view_team,
|
||||
user = self.view_user
|
||||
)
|
||||
|
||||
self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password")
|
||||
|
||||
|
||||
different_organization_team = Team.objects.create(
|
||||
team_name = 'different_organization_team',
|
||||
organization = different_organization,
|
||||
)
|
||||
|
||||
different_organization_team.permissions.set([
|
||||
view_permissions,
|
||||
])
|
||||
|
||||
TeamUsers.objects.create(
|
||||
team = different_organization_team,
|
||||
user = self.different_organization_user
|
||||
)
|
||||
|
||||
|
||||
|
||||
def test_auth_view_history_user_anon_denied(self):
|
||||
""" Check correct permission for view
|
||||
|
||||
Attempt to view as anon user
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.namespace + self.name_view, kwargs={'model_name': self.history_model_name, 'model_pk': self.item.id})
|
||||
|
||||
response = client.get(url)
|
||||
|
||||
assert response.status_code == 302 and response.url.startswith('/account/login')
|
||||
|
||||
|
||||
def test_auth_view_history_no_permission_denied(self):
|
||||
""" Check correct permission for view
|
||||
|
||||
Attempt to view with user missing permission
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.namespace + self.name_view, kwargs={'model_name': self.history_model_name, 'model_pk': self.item.id})
|
||||
|
||||
|
||||
client.force_login(self.no_permissions_user)
|
||||
response = client.get(url)
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
def test_auth_view_history_different_organizaiton_denied(self):
|
||||
""" Check correct permission for view
|
||||
|
||||
Attempt to view with user from different organization
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.namespace + self.name_view, kwargs={'model_name': self.history_model_name, 'model_pk': self.item.id})
|
||||
|
||||
|
||||
client.force_login(self.different_organization_user)
|
||||
response = client.get(url)
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
def test_auth_view_history_has_permission(self):
|
||||
""" Check correct permission for view
|
||||
|
||||
Attempt to view as user with view permission
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.namespace + self.name_view, kwargs={'model_name': self.history_model_name, 'model_pk': self.item.id})
|
||||
|
||||
|
||||
client.force_login(self.view_user)
|
||||
response = client.get(url)
|
||||
|
||||
assert response.status_code == 200
|
210
app/access/tests/unit/team/test_team_permission.py
Normal file
210
app/access/tests/unit/team/test_team_permission.py
Normal file
@ -0,0 +1,210 @@
|
||||
# from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import AnonymousUser, User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.shortcuts import reverse
|
||||
from django.test import TestCase, Client
|
||||
|
||||
import pytest
|
||||
import unittest
|
||||
import requests
|
||||
|
||||
from access.models import Organization, Team, TeamUsers, Permission
|
||||
from access.tests.abstract.model_permissions_organization_manager import OrganizationManagerModelPermissions
|
||||
|
||||
from app.tests.abstract.model_permissions import ModelPermissions
|
||||
|
||||
|
||||
|
||||
class TeamPermissions(
|
||||
TestCase,
|
||||
ModelPermissions,
|
||||
OrganizationManagerModelPermissions,
|
||||
):
|
||||
|
||||
model = Team
|
||||
|
||||
app_namespace = 'Access'
|
||||
|
||||
url_name_view = '_team_view'
|
||||
|
||||
url_name_add = '_team_add'
|
||||
|
||||
url_name_change = '_team_view'
|
||||
|
||||
url_name_delete = '_team_delete'
|
||||
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
"""Setup Test
|
||||
|
||||
1. Create an organization for user and item
|
||||
. create an organization that is different to item
|
||||
2. Create a team
|
||||
3. create teams with each permission: view, add, change, delete
|
||||
4. create a user per team
|
||||
"""
|
||||
|
||||
organization = Organization.objects.create(name='test_org')
|
||||
|
||||
self.organization = organization
|
||||
|
||||
different_organization = Organization.objects.create(name='test_different_organization')
|
||||
|
||||
self.different_organization = different_organization
|
||||
|
||||
|
||||
self.item = self.model.objects.create(
|
||||
organization=organization,
|
||||
name = 'teamone'
|
||||
)
|
||||
|
||||
|
||||
self.url_view_kwargs = {'organization_id': self.organization.id, 'pk': self.item.id}
|
||||
|
||||
self.url_add_kwargs = {'pk': self.organization.id}
|
||||
|
||||
self.add_data = {'team': 'team'}
|
||||
|
||||
self.url_change_kwargs = {'organization_id': self.organization.id, 'pk': self.item.id}
|
||||
|
||||
self.change_data = {'team': 'team'}
|
||||
|
||||
self.url_delete_kwargs = {'organization_id': self.organization.id, 'pk': self.item.id}
|
||||
|
||||
self.delete_data = {'team': 'team'}
|
||||
|
||||
self.url_delete_response = reverse('Access:_organization_view', kwargs={'pk': self.organization.id})
|
||||
|
||||
|
||||
view_permissions = Permission.objects.get(
|
||||
codename = 'view_' + self.model._meta.model_name,
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = self.model._meta.app_label,
|
||||
model = self.model._meta.model_name,
|
||||
)
|
||||
)
|
||||
|
||||
view_team = Team.objects.create(
|
||||
team_name = 'view_team',
|
||||
organization = organization,
|
||||
)
|
||||
|
||||
view_team.permissions.set([view_permissions])
|
||||
|
||||
|
||||
|
||||
add_permissions = Permission.objects.get(
|
||||
codename = 'add_' + self.model._meta.model_name,
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = self.model._meta.app_label,
|
||||
model = self.model._meta.model_name,
|
||||
)
|
||||
)
|
||||
|
||||
add_team = Team.objects.create(
|
||||
team_name = 'add_team',
|
||||
organization = organization,
|
||||
)
|
||||
|
||||
add_team.permissions.set([add_permissions])
|
||||
|
||||
|
||||
|
||||
change_permissions = Permission.objects.get(
|
||||
codename = 'change_' + self.model._meta.model_name,
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = self.model._meta.app_label,
|
||||
model = self.model._meta.model_name,
|
||||
)
|
||||
)
|
||||
|
||||
change_team = Team.objects.create(
|
||||
team_name = 'change_team',
|
||||
organization = organization,
|
||||
)
|
||||
|
||||
change_team.permissions.set([change_permissions])
|
||||
|
||||
|
||||
|
||||
delete_permissions = Permission.objects.get(
|
||||
codename = 'delete_' + self.model._meta.model_name,
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = self.model._meta.app_label,
|
||||
model = self.model._meta.model_name,
|
||||
)
|
||||
)
|
||||
|
||||
delete_team = Team.objects.create(
|
||||
team_name = 'delete_team',
|
||||
organization = organization,
|
||||
)
|
||||
|
||||
delete_team.permissions.set([delete_permissions])
|
||||
|
||||
|
||||
self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password")
|
||||
|
||||
|
||||
self.view_user = User.objects.create_user(username="test_user_view", password="password")
|
||||
teamuser = TeamUsers.objects.create(
|
||||
team = view_team,
|
||||
user = self.view_user
|
||||
)
|
||||
|
||||
self.add_user = User.objects.create_user(username="test_user_add", password="password")
|
||||
teamuser = TeamUsers.objects.create(
|
||||
team = add_team,
|
||||
user = self.add_user
|
||||
)
|
||||
|
||||
self.change_user = User.objects.create_user(username="test_user_change", password="password")
|
||||
teamuser = TeamUsers.objects.create(
|
||||
team = change_team,
|
||||
user = self.change_user
|
||||
)
|
||||
|
||||
self.delete_user = User.objects.create_user(username="test_user_delete", password="password")
|
||||
teamuser = TeamUsers.objects.create(
|
||||
team = delete_team,
|
||||
user = self.delete_user
|
||||
)
|
||||
|
||||
|
||||
self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password")
|
||||
|
||||
|
||||
different_organization_team = Team.objects.create(
|
||||
team_name = 'different_organization_team',
|
||||
organization = different_organization,
|
||||
)
|
||||
|
||||
different_organization_team.permissions.set([
|
||||
view_permissions,
|
||||
add_permissions,
|
||||
change_permissions,
|
||||
delete_permissions,
|
||||
])
|
||||
|
||||
TeamUsers.objects.create(
|
||||
team = different_organization_team,
|
||||
user = self.different_organization_user
|
||||
)
|
||||
|
||||
self.user_is_organization_manager = User.objects.create_user(
|
||||
username="test_org_manager",
|
||||
password="password"
|
||||
)
|
||||
|
||||
self.organization.manager = self.user_is_organization_manager
|
||||
self.organization.save()
|
||||
|
||||
self.different_organization_is_manager = User.objects.create_user(
|
||||
username="test_org_manager_different_org",
|
||||
password="password"
|
||||
)
|
||||
|
||||
self.different_organization.manager = self.different_organization_is_manager
|
||||
self.different_organization.save()
|
175
app/access/tests/unit/team/test_team_permission_api.py
Normal file
175
app/access/tests/unit/team/test_team_permission_api.py
Normal file
@ -0,0 +1,175 @@
|
||||
import pytest
|
||||
import unittest
|
||||
import requests
|
||||
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import AnonymousUser, User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test import TestCase
|
||||
|
||||
from access.models import Organization, Team, TeamUsers, Permission
|
||||
|
||||
from api.tests.abstract.api_permissions import APIPermissions
|
||||
|
||||
|
||||
|
||||
class TeamPermissionsAPI(TestCase, APIPermissions):
|
||||
|
||||
model = Team
|
||||
|
||||
app_namespace = 'API'
|
||||
|
||||
url_name = '_api_team'
|
||||
|
||||
url_list = '_api_organization_teams'
|
||||
|
||||
change_data = {'name': 'device'}
|
||||
|
||||
delete_data = {'device': 'device'}
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
"""Setup Test
|
||||
|
||||
1. Create an organization for user and item
|
||||
. create an organization that is different to item
|
||||
2. Create a team
|
||||
3. create teams with each permission: view, add, change, delete
|
||||
4. create a user per team
|
||||
"""
|
||||
|
||||
organization = Organization.objects.create(name='test_org')
|
||||
|
||||
self.organization = organization
|
||||
|
||||
different_organization = Organization.objects.create(name='test_different_organization')
|
||||
|
||||
|
||||
self.item = self.model.objects.create(
|
||||
organization=organization,
|
||||
name = 'teamone'
|
||||
)
|
||||
|
||||
|
||||
self.url_kwargs = {'organization_id': self.organization.id}
|
||||
|
||||
self.url_view_kwargs = {'organization_id': self.organization.id, 'group_ptr_id': self.item.id}
|
||||
|
||||
self.add_data = {'team_name': 'team_post'}
|
||||
|
||||
|
||||
view_permissions = Permission.objects.get(
|
||||
codename = 'view_' + self.model._meta.model_name,
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = self.model._meta.app_label,
|
||||
model = self.model._meta.model_name,
|
||||
)
|
||||
)
|
||||
|
||||
view_team = Team.objects.create(
|
||||
team_name = 'view_team',
|
||||
organization = organization,
|
||||
)
|
||||
|
||||
view_team.permissions.set([view_permissions])
|
||||
|
||||
|
||||
|
||||
add_permissions = Permission.objects.get(
|
||||
codename = 'add_' + self.model._meta.model_name,
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = self.model._meta.app_label,
|
||||
model = self.model._meta.model_name,
|
||||
)
|
||||
)
|
||||
|
||||
add_team = Team.objects.create(
|
||||
team_name = 'add_team',
|
||||
organization = organization,
|
||||
)
|
||||
|
||||
add_team.permissions.set([add_permissions])
|
||||
|
||||
|
||||
|
||||
change_permissions = Permission.objects.get(
|
||||
codename = 'change_' + self.model._meta.model_name,
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = self.model._meta.app_label,
|
||||
model = self.model._meta.model_name,
|
||||
)
|
||||
)
|
||||
|
||||
change_team = Team.objects.create(
|
||||
team_name = 'change_team',
|
||||
organization = organization,
|
||||
)
|
||||
|
||||
change_team.permissions.set([change_permissions])
|
||||
|
||||
|
||||
|
||||
delete_permissions = Permission.objects.get(
|
||||
codename = 'delete_' + self.model._meta.model_name,
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = self.model._meta.app_label,
|
||||
model = self.model._meta.model_name,
|
||||
)
|
||||
)
|
||||
|
||||
delete_team = Team.objects.create(
|
||||
team_name = 'delete_team',
|
||||
organization = organization,
|
||||
)
|
||||
|
||||
delete_team.permissions.set([delete_permissions])
|
||||
|
||||
|
||||
self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password")
|
||||
|
||||
|
||||
self.view_user = User.objects.create_user(username="test_user_view", password="password")
|
||||
teamuser = TeamUsers.objects.create(
|
||||
team = view_team,
|
||||
user = self.view_user
|
||||
)
|
||||
|
||||
self.add_user = User.objects.create_user(username="test_user_add", password="password")
|
||||
teamuser = TeamUsers.objects.create(
|
||||
team = add_team,
|
||||
user = self.add_user
|
||||
)
|
||||
|
||||
self.change_user = User.objects.create_user(username="test_user_change", password="password")
|
||||
teamuser = TeamUsers.objects.create(
|
||||
team = change_team,
|
||||
user = self.change_user
|
||||
)
|
||||
|
||||
self.delete_user = User.objects.create_user(username="test_user_delete", password="password")
|
||||
teamuser = TeamUsers.objects.create(
|
||||
team = delete_team,
|
||||
user = self.delete_user
|
||||
)
|
||||
|
||||
|
||||
self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password")
|
||||
|
||||
|
||||
different_organization_team = Team.objects.create(
|
||||
team_name = 'different_organization_team',
|
||||
organization = different_organization,
|
||||
)
|
||||
|
||||
different_organization_team.permissions.set([
|
||||
view_permissions,
|
||||
add_permissions,
|
||||
change_permissions,
|
||||
delete_permissions,
|
||||
])
|
||||
|
||||
TeamUsers.objects.create(
|
||||
team = different_organization_team,
|
||||
user = self.different_organization_user
|
||||
)
|
29
app/access/tests/unit/team/test_team_views.py
Normal file
29
app/access/tests/unit/team/test_team_views.py
Normal file
@ -0,0 +1,29 @@
|
||||
import pytest
|
||||
import unittest
|
||||
import requests
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from app.tests.abstract.models import ModelAdd, ModelDelete, ModelDisplay
|
||||
|
||||
|
||||
|
||||
class TeamViews(
|
||||
TestCase,
|
||||
ModelAdd,
|
||||
ModelDelete,
|
||||
ModelDisplay,
|
||||
):
|
||||
|
||||
add_module = 'access.views.team'
|
||||
add_view = 'Add'
|
||||
|
||||
# change_module = add_module
|
||||
# change_view = 'Change'
|
||||
|
||||
delete_module = add_module
|
||||
delete_view = 'Delete'
|
||||
|
||||
display_module = add_module
|
||||
display_view = 'View'
|
||||
|
56
app/access/tests/unit/team_user/test_team_user.py
Normal file
56
app/access/tests/unit/team_user/test_team_user.py
Normal file
@ -0,0 +1,56 @@
|
||||
import pytest
|
||||
import unittest
|
||||
|
||||
from django.test import TestCase, Client
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from access.models import Organization, Team, TeamUsers, Permission
|
||||
|
||||
|
||||
|
||||
class TeamUsersModel(TestCase):
|
||||
|
||||
model = TeamUsers
|
||||
|
||||
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
""" Setup Test
|
||||
|
||||
"""
|
||||
|
||||
organization = Organization.objects.create(name='test_org')
|
||||
|
||||
different_organization = Organization.objects.create(name='test_different_organization')
|
||||
|
||||
self.parent_item = Team.objects.create(
|
||||
team_name = 'test_team',
|
||||
organization = organization,
|
||||
)
|
||||
|
||||
team_user = User.objects.create_user(username="test_self.team_user", password="password")
|
||||
|
||||
self.item = self.model.objects.create(
|
||||
team = self.parent_item,
|
||||
user = team_user
|
||||
)
|
||||
|
||||
|
||||
|
||||
def test_model_has_property_parent_object(self):
|
||||
""" Check if model contains 'parent_object'
|
||||
|
||||
This is a required property for all models that have a parent
|
||||
"""
|
||||
|
||||
assert hasattr(self.model, 'parent_object')
|
||||
|
||||
|
||||
def test_model_property_parent_object_returns_object(self):
|
||||
""" Check if model contains 'parent_object'
|
||||
|
||||
This is a required property for all models that have a parent
|
||||
"""
|
||||
|
||||
assert self.item.parent_object == self.parent_item
|
@ -0,0 +1,92 @@
|
||||
|
||||
import pytest
|
||||
import unittest
|
||||
import requests
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import TestCase, Client
|
||||
|
||||
from access.models import Organization
|
||||
|
||||
from core.models.history import History
|
||||
from core.tests.abstract.history_entry import HistoryEntry
|
||||
from core.tests.abstract.history_entry_child_model import HistoryEntryChildItem
|
||||
|
||||
from access.models import Team, TeamUsers
|
||||
|
||||
|
||||
|
||||
class TeamUsersHistory(TestCase, HistoryEntry, HistoryEntryChildItem):
|
||||
|
||||
model = TeamUsers
|
||||
|
||||
model_name = 'teamusers'
|
||||
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
""" Setup Test """
|
||||
|
||||
organization = Organization.objects.create(name='test_org')
|
||||
|
||||
self.organization = organization
|
||||
|
||||
self.item_parent = Team.objects.create(
|
||||
team_name = 'test_item_' + self.model._meta.model_name,
|
||||
organization = self.organization
|
||||
)
|
||||
|
||||
self.user = User.objects.create(
|
||||
username = 'test_item_' + self.model._meta.model_name,
|
||||
password = 'a random password'
|
||||
)
|
||||
|
||||
self.item_create = self.model.objects.create(
|
||||
user = self.user,
|
||||
team = self.item_parent
|
||||
)
|
||||
|
||||
|
||||
self.history_create = History.objects.get(
|
||||
action = History.Actions.ADD[0],
|
||||
item_pk = self.item_create.pk,
|
||||
item_class = self.model._meta.model_name,
|
||||
)
|
||||
|
||||
self.item_change = self.item_create
|
||||
self.item_change.manager = True
|
||||
self.item_change.save()
|
||||
|
||||
self.field_after_expected_value = '{"manager": true}'
|
||||
|
||||
self.history_change = History.objects.get(
|
||||
action = History.Actions.UPDATE[0],
|
||||
item_pk = self.item_change.pk,
|
||||
item_class = self.model._meta.model_name,
|
||||
)
|
||||
|
||||
|
||||
self.user_delete = User.objects.create(
|
||||
username = 'test_item_delete' + self.model._meta.model_name,
|
||||
password = 'a random password'
|
||||
)
|
||||
|
||||
self.item_delete = self.model.objects.create(
|
||||
user = self.user_delete,
|
||||
team = self.item_parent
|
||||
)
|
||||
|
||||
self.deleted_pk = self.item_delete.pk
|
||||
|
||||
self.item_delete.delete()
|
||||
|
||||
self.history_delete = History.objects.get(
|
||||
action = History.Actions.DELETE[0],
|
||||
item_pk = self.deleted_pk,
|
||||
item_class = self.model._meta.model_name,
|
||||
)
|
||||
|
||||
self.history_delete_children = History.objects.filter(
|
||||
item_parent_pk = self.deleted_pk,
|
||||
item_parent_class = self.item_parent._meta.model_name,
|
||||
)
|
48
app/access/tests/unit/team_user/test_team_user_functions.py
Normal file
48
app/access/tests/unit/team_user/test_team_user_functions.py
Normal file
@ -0,0 +1,48 @@
|
||||
from django.test import TestCase
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
import unittest
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.exceptions import ValidationError
|
||||
from access.models import Organization, Team
|
||||
|
||||
|
||||
# @pytest.fixture
|
||||
# def organization() -> Organization:
|
||||
# return Organization.objects.create(
|
||||
# name='Test org',
|
||||
# )
|
||||
|
||||
|
||||
# @pytest.fixture
|
||||
# def team() -> Team:
|
||||
# return Team.objects.create(
|
||||
# name='Team one',
|
||||
# organization = Organization.objects.create(
|
||||
# name='Test org',
|
||||
# ),
|
||||
# )
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_authorization_user_permission_add_team_manager(user):
|
||||
"""Ensure user can be added when user is team manager
|
||||
|
||||
user requires permissions team view and user add
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_authorization_user_permission_delete_team_manager(user):
|
||||
"""Ensure user can be deleted when user is team manager
|
||||
|
||||
user requires permissions team view and user delete
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
# is_superuser to be able to view, add, change, delete for all objects
|
||||
|
325
app/access/tests/unit/team_user/test_team_user_permission.py
Normal file
325
app/access/tests/unit/team_user/test_team_user_permission.py
Normal file
@ -0,0 +1,325 @@
|
||||
# from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import AnonymousUser, User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.shortcuts import reverse
|
||||
from django.test import TestCase, Client
|
||||
|
||||
import pytest
|
||||
import unittest
|
||||
import requests
|
||||
|
||||
from access.models import Organization, Team, TeamUsers, Permission
|
||||
from access.tests.abstract.model_permissions_organization_manager import OrganizationManagerModelPermissionAdd, OrganizationManagerModelPermissionDelete
|
||||
|
||||
from app.tests.abstract.model_permissions import ModelPermissionsAdd, ModelPermissionsChange, ModelPermissionsDelete
|
||||
|
||||
|
||||
|
||||
class TeamUserPermissions(
|
||||
TestCase,
|
||||
ModelPermissionsAdd,
|
||||
ModelPermissionsDelete,
|
||||
OrganizationManagerModelPermissionAdd,
|
||||
OrganizationManagerModelPermissionDelete
|
||||
):
|
||||
|
||||
model = TeamUsers
|
||||
|
||||
app_namespace = 'Access'
|
||||
|
||||
url_name_view = '_team_user_view'
|
||||
|
||||
url_name_add = '_team_user_add'
|
||||
|
||||
url_name_change = '_team_user_view'
|
||||
|
||||
url_name_delete = '_team_user_delete'
|
||||
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
"""Setup Test
|
||||
|
||||
1. Create an organization for user and item
|
||||
. create an organization that is different to item
|
||||
2. Create a device
|
||||
3. create teams with each permission: view, add, change, delete
|
||||
4. create a user per team
|
||||
"""
|
||||
|
||||
organization = Organization.objects.create(name='test_org')
|
||||
|
||||
self.organization = organization
|
||||
|
||||
different_organization = Organization.objects.create(name='test_different_organization')
|
||||
|
||||
self.different_organization = different_organization
|
||||
|
||||
self.test_team = Team.objects.create(
|
||||
team_name = 'test_team',
|
||||
organization = organization,
|
||||
)
|
||||
|
||||
self.team_user = User.objects.create_user(username="test_self.team_user", password="password")
|
||||
|
||||
self.item = self.model.objects.create(
|
||||
team = self.test_team,
|
||||
user = self.team_user
|
||||
)
|
||||
|
||||
|
||||
self.url_view_kwargs = {'pk': self.item.id}
|
||||
|
||||
self.url_add_kwargs = {'organization_id': self.organization.id, 'pk': self.item.id}
|
||||
|
||||
self.add_data = {'operating_system': 'operating_system', 'organization': self.organization.id}
|
||||
|
||||
self.url_change_kwargs = {'organization_id': self.organization.id, 'team_id': self.item.team.id, 'pk': self.item.id}
|
||||
|
||||
self.change_data = {'operating_system': 'operating_system', 'organization': self.organization.id}
|
||||
|
||||
self.url_delete_kwargs = {'organization_id': self.organization.id, 'team_id': self.item.team.id, 'pk': self.item.id}
|
||||
|
||||
self.delete_data = {'operating_system': 'operating_system', 'organization': self.organization.id}
|
||||
|
||||
self.url_delete_response = reverse('Access:_team_view',
|
||||
kwargs={
|
||||
'organization_id': self.organization.id,
|
||||
'pk': self.test_team.id
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
view_permissions = Permission.objects.get(
|
||||
codename = 'view_' + self.model._meta.model_name,
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = self.model._meta.app_label,
|
||||
model = self.model._meta.model_name,
|
||||
)
|
||||
)
|
||||
|
||||
view_team = Team.objects.create(
|
||||
team_name = 'view_team',
|
||||
organization = organization,
|
||||
)
|
||||
|
||||
view_team.permissions.set([view_permissions])
|
||||
|
||||
|
||||
|
||||
add_permissions = Permission.objects.get(
|
||||
codename = 'add_' + self.model._meta.model_name,
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = self.model._meta.app_label,
|
||||
model = self.model._meta.model_name,
|
||||
)
|
||||
)
|
||||
|
||||
add_team = Team.objects.create(
|
||||
team_name = 'add_team',
|
||||
organization = organization,
|
||||
)
|
||||
|
||||
add_team.permissions.set([add_permissions])
|
||||
|
||||
|
||||
|
||||
change_permissions = Permission.objects.get(
|
||||
codename = 'change_' + self.model._meta.model_name,
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = self.model._meta.app_label,
|
||||
model = self.model._meta.model_name,
|
||||
)
|
||||
)
|
||||
|
||||
change_team = Team.objects.create(
|
||||
team_name = 'change_team',
|
||||
organization = organization,
|
||||
)
|
||||
|
||||
change_team.permissions.set([change_permissions])
|
||||
|
||||
|
||||
|
||||
delete_permissions = Permission.objects.get(
|
||||
codename = 'delete_' + self.model._meta.model_name,
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = self.model._meta.app_label,
|
||||
model = self.model._meta.model_name,
|
||||
)
|
||||
)
|
||||
|
||||
delete_team = Team.objects.create(
|
||||
team_name = 'delete_team',
|
||||
organization = organization,
|
||||
)
|
||||
|
||||
delete_team.permissions.set([delete_permissions])
|
||||
|
||||
|
||||
self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password")
|
||||
|
||||
|
||||
self.view_user = User.objects.create_user(username="test_user_view", password="password")
|
||||
teamuser = TeamUsers.objects.create(
|
||||
team = view_team,
|
||||
user = self.view_user
|
||||
)
|
||||
|
||||
self.add_user = User.objects.create_user(username="test_user_add", password="password")
|
||||
teamuser = TeamUsers.objects.create(
|
||||
team = add_team,
|
||||
user = self.add_user
|
||||
)
|
||||
|
||||
self.change_user = User.objects.create_user(username="test_user_change", password="password")
|
||||
teamuser = TeamUsers.objects.create(
|
||||
team = change_team,
|
||||
user = self.change_user
|
||||
)
|
||||
|
||||
self.delete_user = User.objects.create_user(username="test_user_delete", password="password")
|
||||
teamuser = TeamUsers.objects.create(
|
||||
team = delete_team,
|
||||
user = self.delete_user
|
||||
)
|
||||
|
||||
|
||||
self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password")
|
||||
|
||||
|
||||
different_organization_team = Team.objects.create(
|
||||
team_name = 'different_organization_team',
|
||||
organization = different_organization,
|
||||
)
|
||||
|
||||
different_organization_team.permissions.set([
|
||||
view_permissions,
|
||||
add_permissions,
|
||||
change_permissions,
|
||||
delete_permissions,
|
||||
])
|
||||
|
||||
TeamUsers.objects.create(
|
||||
team = different_organization_team,
|
||||
user = self.different_organization_user
|
||||
)
|
||||
|
||||
self.user_is_organization_manager = User.objects.create_user(
|
||||
username="test_org_manager",
|
||||
password="password"
|
||||
)
|
||||
|
||||
self.organization.manager = self.user_is_organization_manager
|
||||
self.organization.save()
|
||||
|
||||
self.different_organization_is_manager = User.objects.create_user(
|
||||
username="test_org_manager_different_org",
|
||||
password="password"
|
||||
)
|
||||
|
||||
self.different_organization.manager = self.different_organization_is_manager
|
||||
self.different_organization.save()
|
||||
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="feature does not exist")
|
||||
def test_team_user_auth_change_user_anon_denied(self):
|
||||
""" Check correct permission for change
|
||||
|
||||
Attempt to change as anon
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse('Access:_team_user_view', kwargs={'pk': self.item.id})
|
||||
|
||||
|
||||
response = client.patch(url, data={'device': 'device'})
|
||||
|
||||
assert response.status_code == 302 and response.url.startswith('/account/login')
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="feature does not exist")
|
||||
def test_team_user_auth_change_no_permission_denied(self):
|
||||
""" Ensure permission view cant make change
|
||||
|
||||
Attempt to make change as user without permissions
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse('Access:_team_user_view', kwargs={'pk': self.item.id})
|
||||
|
||||
|
||||
client.force_login(self.no_permissions_user)
|
||||
response = client.post(url, data={'device': 'device'})
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="feature does not exist")
|
||||
def test_team_user_auth_change_different_organization_denied(self):
|
||||
""" Ensure permission view cant make change
|
||||
|
||||
Attempt to make change as user from different organization
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse('Access:_team_user_view', kwargs={'pk': self.item.id})
|
||||
|
||||
|
||||
client.force_login(self.different_organization_user)
|
||||
response = client.post(url, data={'device': 'device'})
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="feature does not exist")
|
||||
def test_team_user_auth_change_permission_view_denied(self):
|
||||
""" Ensure permission view cant make change
|
||||
|
||||
Attempt to make change as user with view permission
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse('Access:_team_user_view', kwargs={'pk': self.item.id})
|
||||
|
||||
|
||||
client.force_login(self.view_user)
|
||||
response = client.post(url, data={'device': 'device'})
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="feature does not exist")
|
||||
def test_team_user_auth_change_permission_add_denied(self):
|
||||
""" Ensure permission view cant make change
|
||||
|
||||
Attempt to make change as user with add permission
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse('Access:_team_user_view', kwargs={'pk': self.item.id})
|
||||
|
||||
|
||||
client.force_login(self.add_user)
|
||||
response = client.post(url, data={'device': 'device'})
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="feature does not exist")
|
||||
def test_team_user_auth_change_has_permission(self):
|
||||
""" Check correct permission for change
|
||||
|
||||
Make change with user who has change permission
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse('Access:_team_user_view', kwargs={'pk': self.item.id})
|
||||
|
||||
|
||||
client.force_login(self.change_user)
|
||||
response = client.post(url, data={'device': 'device'})
|
||||
|
||||
assert response.status_code == 200
|
30
app/access/tests/unit/team_user/test_team_user_views.py
Normal file
30
app/access/tests/unit/team_user/test_team_user_views.py
Normal file
@ -0,0 +1,30 @@
|
||||
import pytest
|
||||
import unittest
|
||||
import requests
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from app.tests.abstract.models import AddView, DeleteView
|
||||
|
||||
|
||||
|
||||
class TeamUserViews(
|
||||
TestCase,
|
||||
AddView,
|
||||
DeleteView
|
||||
):
|
||||
|
||||
add_module = 'access.views.user'
|
||||
add_view = 'Add'
|
||||
|
||||
# change_module = add_module
|
||||
# change_view = 'GroupView'
|
||||
|
||||
delete_module = add_module
|
||||
delete_view = 'Delete'
|
||||
|
||||
# display_module = add_module
|
||||
# display_view = 'GroupView'
|
||||
|
||||
# index_module = add_module
|
||||
# index_view = 'GroupIndexView'
|
93
app/access/tests/unit/tenancy_object/test_tenancy_object.py
Normal file
93
app/access/tests/unit/tenancy_object/test_tenancy_object.py
Normal file
@ -0,0 +1,93 @@
|
||||
import pytest
|
||||
import unittest
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from access.models import TenancyObject, TenancyManager
|
||||
|
||||
from core.mixin.history_save import SaveHistory
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
|
||||
class TenancyManagerTests(TestCase):
|
||||
|
||||
item = TenancyManager
|
||||
|
||||
|
||||
def test_has_attribute_get_queryset(self):
|
||||
""" Field organization exists """
|
||||
|
||||
assert hasattr(self.item, 'get_queryset')
|
||||
|
||||
|
||||
def test_is_function_get_queryset(self):
|
||||
""" Attribute 'get_organization' is a function """
|
||||
|
||||
assert callable(self.item.get_queryset)
|
||||
|
||||
|
||||
|
||||
class TenancyObjectTests(TestCase):
|
||||
|
||||
item = TenancyObject
|
||||
|
||||
|
||||
def test_class_inherits_save_history(self):
|
||||
""" Confirm class inheritence
|
||||
|
||||
TenancyObject must inherit SaveHistory
|
||||
"""
|
||||
|
||||
assert issubclass(TenancyObject, SaveHistory)
|
||||
|
||||
|
||||
def test_has_attribute_organization(self):
|
||||
""" Field organization exists """
|
||||
|
||||
assert hasattr(self.item, 'organization')
|
||||
|
||||
|
||||
def test_has_attribute_is_global(self):
|
||||
""" Field organization exists """
|
||||
|
||||
assert hasattr(self.item, 'is_global')
|
||||
|
||||
|
||||
def test_has_attribute_model_notes(self):
|
||||
""" Field organization exists """
|
||||
|
||||
assert hasattr(self.item, 'model_notes')
|
||||
|
||||
|
||||
def test_has_attribute_get_organization(self):
|
||||
""" Function 'get_organization' Exists """
|
||||
|
||||
assert hasattr(self.item, 'get_organization')
|
||||
|
||||
|
||||
def test_is_function_get_organization(self):
|
||||
""" Attribute 'get_organization' is a function """
|
||||
|
||||
assert callable(self.item.get_organization)
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="figure out how to test abstract class")
|
||||
def test_has_attribute_objects(self):
|
||||
""" Attribute Check
|
||||
|
||||
attribute `objects` must be set to `access.models.TenancyManager()`
|
||||
"""
|
||||
|
||||
assert 'objects' in self.item
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="figure out how to test abstract class")
|
||||
def test_attribute_not_none_objects(self):
|
||||
""" Attribute Check
|
||||
|
||||
attribute `objects` must be set to `access.models.TenancyManager()`
|
||||
"""
|
||||
|
||||
assert self.item.objects is not None
|
16
app/access/urls.py
Normal file
16
app/access/urls.py
Normal file
@ -0,0 +1,16 @@
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
from .views import team, organization, user
|
||||
|
||||
app_name = "Access"
|
||||
urlpatterns = [
|
||||
path("", organization.IndexView.as_view(), name="Organizations"),
|
||||
path("<int:pk>/", organization.View.as_view(), name="_organization_view"),
|
||||
# path("<int:pk>/edit", organization.Change.as_view(), name="_organization_change"),
|
||||
path("<int:organization_id>/team/<int:pk>/", team.View.as_view(), name="_team_view"),
|
||||
path("<int:pk>/team/add", team.Add.as_view(), name="_team_add"),
|
||||
path("<int:organization_id>/team/<int:pk>/delete", team.Delete.as_view(), name="_team_delete"),
|
||||
path("<int:organization_id>/team/<int:pk>/user/add", user.Add.as_view(), name="_team_user_add"),
|
||||
path("<int:organization_id>/team/<int:team_id>/user/<int:pk>/delete", user.Delete.as_view(), name="_team_user_delete"),
|
||||
]
|
123
app/access/views/organization.py
Normal file
123
app/access/views/organization.py
Normal file
@ -0,0 +1,123 @@
|
||||
from django.contrib.auth import decorators as auth_decorator
|
||||
from django.db.models import Q
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views import generic
|
||||
|
||||
from access.mixin import *
|
||||
from access.models import *
|
||||
|
||||
from access.forms.organization import OrganizationForm
|
||||
|
||||
from core.views.common import ChangeView, IndexView
|
||||
|
||||
|
||||
class IndexView(IndexView):
|
||||
|
||||
model = Organization
|
||||
permission_required = [
|
||||
'access.view_organization'
|
||||
]
|
||||
template_name = 'access/index.html.j2'
|
||||
context_object_name = "organization_list"
|
||||
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
context['content_title'] = 'Organizations'
|
||||
|
||||
return context
|
||||
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
if self.request.user.is_superuser:
|
||||
|
||||
return Organization.objects.filter()
|
||||
|
||||
else:
|
||||
|
||||
return Organization.objects.filter(
|
||||
Q(pk__in=self.user_organizations())
|
||||
|
|
||||
Q(manager=self.request.user.id)
|
||||
)
|
||||
|
||||
|
||||
|
||||
class View(ChangeView):
|
||||
|
||||
context_object_name = "organization"
|
||||
|
||||
form_class = OrganizationForm
|
||||
|
||||
model = Organization
|
||||
|
||||
template_name = "access/organization.html.j2"
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
|
||||
if not request.user.is_authenticated:
|
||||
|
||||
return self.handle_no_permission()
|
||||
|
||||
if not self.permission_check(request, [ 'access.view_organization' ]):
|
||||
|
||||
raise PermissionDenied('You are not part of this organization')
|
||||
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
|
||||
def get_success_url(self, **kwargs):
|
||||
return f"/organization/{self.kwargs['pk']}/"
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
return Organization.objects.filter(pk=self.kwargs['pk'])
|
||||
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
context['model_docs_path'] = self.model._meta.app_label + '/' + self.model._meta.model_name + '/'
|
||||
|
||||
context['teams'] = Team.objects.filter(organization=self.kwargs['pk'])
|
||||
|
||||
context['model_pk'] = self.kwargs['pk']
|
||||
context['model_name'] = self.model._meta.verbose_name.replace(' ', '')
|
||||
|
||||
context['content_title'] = 'Organization - ' + context[self.context_object_name].name
|
||||
|
||||
return context
|
||||
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
|
||||
if not request.user.is_authenticated:
|
||||
|
||||
return self.handle_no_permission()
|
||||
|
||||
if not self.permission_check(request, [ 'access.change_organization' ]):
|
||||
|
||||
raise PermissionDenied('You are not part of this organization')
|
||||
|
||||
return super().post(request, *args, **kwargs)
|
||||
|
||||
|
||||
|
||||
class Change(OrganizationPermission, generic.DetailView):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
class Delete(OrganizationPermission, generic.DetailView):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
153
app/access/views/team.py
Normal file
153
app/access/views/team.py
Normal file
@ -0,0 +1,153 @@
|
||||
from django.contrib.auth import decorators as auth_decorator
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.urls import reverse
|
||||
|
||||
from access.forms.team import TeamForm, TeamFormAdd
|
||||
from access.models import Team, TeamUsers, Organization
|
||||
from access.mixin import *
|
||||
|
||||
from core.views.common import AddView, ChangeView, DeleteView
|
||||
|
||||
|
||||
class View(ChangeView):
|
||||
|
||||
context_object_name = "team"
|
||||
|
||||
form_class = TeamForm
|
||||
|
||||
model = Team
|
||||
|
||||
permission_required = [
|
||||
'access.view_team',
|
||||
'access.change_team',
|
||||
]
|
||||
|
||||
template_name = 'access/team.html.j2'
|
||||
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
|
||||
if not request.user.is_authenticated:
|
||||
|
||||
return self.handle_no_permission()
|
||||
|
||||
if not self.permission_check(request, [ 'access.view_team' ]):
|
||||
|
||||
raise PermissionDenied('You are not part of this organization')
|
||||
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
context['model_docs_path'] = self.model._meta.app_label + '/' + self.model._meta.model_name + '/'
|
||||
|
||||
|
||||
organization = Organization.objects.get(pk=self.kwargs['organization_id'])
|
||||
|
||||
context['organization'] = organization
|
||||
|
||||
team = Team.objects.get(pk=self.kwargs['pk'])
|
||||
|
||||
teamusers = TeamUsers.objects.filter(team=self.kwargs['pk'])
|
||||
|
||||
context['teamusers'] = teamusers
|
||||
|
||||
context['model_pk'] = self.kwargs['pk']
|
||||
context['model_name'] = self.model._meta.verbose_name.replace(' ', '')
|
||||
|
||||
return context
|
||||
|
||||
def get_success_url(self, **kwargs):
|
||||
return reverse('Access:_team_view', args=(self.kwargs['organization_id'], self.kwargs['pk'],))
|
||||
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
|
||||
if not request.user.is_authenticated:
|
||||
|
||||
return self.handle_no_permission()
|
||||
|
||||
if not self.permission_check(request, [ 'access.change_team' ]):
|
||||
|
||||
raise PermissionDenied('You are not part of this organization')
|
||||
|
||||
return super().post(request, *args, **kwargs)
|
||||
|
||||
|
||||
|
||||
class Add(AddView):
|
||||
|
||||
form_class = TeamFormAdd
|
||||
|
||||
model = Team
|
||||
|
||||
parent_model = Organization
|
||||
|
||||
permission_required = [
|
||||
'access.add_team',
|
||||
]
|
||||
|
||||
template_name = 'form.html.j2'
|
||||
|
||||
def form_valid(self, form):
|
||||
form.instance.organization = Organization.objects.get(pk=self.kwargs['pk'])
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
def get_success_url(self, **kwargs):
|
||||
return f"/organization/{self.kwargs['pk']}/"
|
||||
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
context['content_title'] = 'Add Team'
|
||||
|
||||
return context
|
||||
|
||||
|
||||
|
||||
class Delete(DeleteView):
|
||||
model = Team
|
||||
permission_required = [
|
||||
'access.delete_team'
|
||||
]
|
||||
template_name = 'form.html.j2'
|
||||
fields = [
|
||||
'team_name',
|
||||
'permissions',
|
||||
'organization'
|
||||
]
|
||||
|
||||
|
||||
def get_success_url(self, **kwargs):
|
||||
return f"/organization/{self.kwargs['organization_id']}/"
|
||||
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
context['model_pk'] = self.kwargs['pk']
|
||||
context['model_name'] = self.model._meta.verbose_name.replace(' ', '')
|
||||
|
||||
context['content_title'] = 'Delete Team'
|
||||
|
||||
return context
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
75
app/access/views/user.py
Normal file
75
app/access/views/user.py
Normal file
@ -0,0 +1,75 @@
|
||||
from django.contrib.auth import decorators as auth_decorator
|
||||
from django.urls import reverse
|
||||
|
||||
from access.forms.team_users import TeamUsersForm
|
||||
from access.models import Team, TeamUsers
|
||||
|
||||
from core.views.common import AddView, DeleteView
|
||||
|
||||
|
||||
class Add(AddView):
|
||||
|
||||
context_object_name = "teamuser"
|
||||
|
||||
form_class = TeamUsersForm
|
||||
|
||||
model = TeamUsers
|
||||
|
||||
parent_model = Team
|
||||
|
||||
permission_required = [
|
||||
'access.add_teamusers'
|
||||
]
|
||||
|
||||
template_name = 'form.html.j2'
|
||||
|
||||
|
||||
def form_valid(self, form):
|
||||
team = Team.objects.get(pk=self.kwargs['pk'])
|
||||
form.instance.team = team
|
||||
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
def get_success_url(self, **kwargs):
|
||||
|
||||
return reverse('Access:_team_view',
|
||||
kwargs={
|
||||
'organization_id': self.kwargs['organization_id'],
|
||||
'pk': self.kwargs['pk']
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
context['content_title'] = 'Add Team User'
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class Delete(DeleteView):
|
||||
model = TeamUsers
|
||||
permission_required = [
|
||||
'access.delete_teamusers'
|
||||
]
|
||||
template_name = 'form.html.j2'
|
||||
|
||||
|
||||
def get_success_url(self, **kwargs):
|
||||
|
||||
return reverse('Access:_team_view',
|
||||
kwargs={
|
||||
'organization_id': self.kwargs['organization_id'],
|
||||
'pk': self.kwargs['team_id']
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
context['content_title'] = 'Delete Team User'
|
||||
|
||||
return context
|
0
app/api/__init__.py
Normal file
0
app/api/__init__.py
Normal file
6
app/api/apps.py
Normal file
6
app/api/apps.py
Normal file
@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ApiConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'api'
|
79
app/api/auth.py
Normal file
79
app/api/auth.py
Normal file
@ -0,0 +1,79 @@
|
||||
import datetime
|
||||
|
||||
from rest_framework import exceptions
|
||||
from rest_framework.authentication import BaseAuthentication, get_authorization_header
|
||||
|
||||
from api.models.tokens import AuthToken
|
||||
|
||||
|
||||
|
||||
class TokenAuthentication(BaseAuthentication):
|
||||
""" API Token Authentication
|
||||
|
||||
Provides the ability to use the API by using a token to authenticate.
|
||||
"""
|
||||
|
||||
def authenticate_header(self, request):
|
||||
return 'Token'
|
||||
|
||||
|
||||
def authenticate(self, request):
|
||||
""" Authentication the API session using the supplied token
|
||||
|
||||
Args:
|
||||
request (object): API Request Object
|
||||
|
||||
Raises:
|
||||
exceptions.AuthenticationFailed: 'Token header invalid' - Authorization Header Value is not in format `Token <auth-token>`
|
||||
exceptions.AuthenticationFailed: 'Token header invalid. Possibly incorrectly formatted' - Authentication header value has >1 space
|
||||
exceptions.AuthenticationFailed: 'Invalid token header. Token string should not contain invalid characters.' - Authorization header contains non-unicode chars
|
||||
|
||||
Returns:
|
||||
None (None): User not authenticated
|
||||
tuple(user,token): User authenticated
|
||||
"""
|
||||
|
||||
auth = get_authorization_header(request).split()
|
||||
|
||||
if not auth:
|
||||
return None
|
||||
|
||||
if len(auth) == 1:
|
||||
|
||||
raise exceptions.AuthenticationFailed('Token header invalid')
|
||||
|
||||
elif len(auth) > 2:
|
||||
|
||||
raise exceptions.AuthenticationFailed('Token header invalid. Possibly incorrectly formatted')
|
||||
|
||||
|
||||
elif len(auth) == 2:
|
||||
|
||||
try:
|
||||
|
||||
decoded_token: str = auth[1].decode("utf-8")
|
||||
|
||||
for token in AuthToken.objects.filter():
|
||||
|
||||
provided_token: str = token.token_hash(decoded_token)
|
||||
|
||||
if token.token == provided_token:
|
||||
|
||||
if datetime.datetime.strptime(str(token.expires),'%Y-%m-%d %H:%M:%S%z') > datetime.datetime.now(datetime.timezone.utc):
|
||||
|
||||
user = token.user
|
||||
|
||||
return (user, provided_token)
|
||||
|
||||
else:
|
||||
|
||||
expired_token = AuthToken.objects.get(id=token.id)
|
||||
|
||||
expired_token.delete()
|
||||
|
||||
except UnicodeError:
|
||||
|
||||
raise exceptions.AuthenticationFailed('Invalid token header. Token string should not contain invalid characters.')
|
||||
|
||||
|
||||
return None
|
49
app/api/forms/user_token.py
Normal file
49
app/api/forms/user_token.py
Normal file
@ -0,0 +1,49 @@
|
||||
import datetime
|
||||
from django import forms
|
||||
|
||||
from api.models.tokens import AuthToken
|
||||
|
||||
from app import settings
|
||||
|
||||
from core.forms.common import CommonModelForm
|
||||
|
||||
|
||||
class AuthTokenForm(CommonModelForm):
|
||||
|
||||
prefix = 'user_token'
|
||||
|
||||
class Meta:
|
||||
|
||||
fields = [
|
||||
'note',
|
||||
'expires',
|
||||
]
|
||||
|
||||
model = AuthToken
|
||||
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.fields['expires'].widget = forms.widgets.DateTimeInput(attrs={'type': 'datetime-local', 'format': "%Y-%m-%dT%H:%M"})
|
||||
self.fields['expires'].input_formats = settings.DATETIME_FORMAT
|
||||
self.fields['expires'].format="%Y-%m-%dT%H:%M"
|
||||
self.fields['expires'].initial= datetime.datetime.now() + datetime.timedelta(days=90)
|
||||
|
||||
if self.prefix + '-gen_token' not in self.data:
|
||||
|
||||
generated_token = self.instance.generate()
|
||||
|
||||
else:
|
||||
|
||||
generated_token = self.data[self.prefix + '-gen_token']
|
||||
|
||||
self.fields['gen_token'] = forms.CharField(
|
||||
label="Generated Token",
|
||||
initial=generated_token,
|
||||
empty_value= generated_token,
|
||||
required=False,
|
||||
help_text = 'Ensure you save this token somewhere as you will never be able to obtain it again',
|
||||
)
|
||||
|
||||
self.fields['gen_token'].widget.attrs['readonly'] = True
|
31
app/api/migrations/0001_initial.py
Normal file
31
app/api/migrations/0001_initial.py
Normal file
@ -0,0 +1,31 @@
|
||||
# Generated by Django 5.0.7 on 2024-07-12 03:54
|
||||
|
||||
import access.fields
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='AuthToken',
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, serialize=False, unique=True)),
|
||||
('note', models.CharField(blank=True, default=None, max_length=50, null=True)),
|
||||
('token', models.CharField(db_index=True, max_length=64, unique=True, verbose_name='Auth Token')),
|
||||
('expires', models.DateTimeField(verbose_name='Expiry Date')),
|
||||
('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)),
|
||||
('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
0
app/api/migrations/__init__.py
Normal file
0
app/api/migrations/__init__.py
Normal file
0
app/api/models/__init__.py
Normal file
0
app/api/models/__init__.py
Normal file
109
app/api/models/tokens.py
Normal file
109
app/api/models/tokens.py
Normal file
@ -0,0 +1,109 @@
|
||||
import hashlib
|
||||
import random
|
||||
import string
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.db import models
|
||||
from django.db.models import Field
|
||||
from django.forms import ValidationError
|
||||
|
||||
from access.fields import *
|
||||
from access.models import TenancyObject
|
||||
|
||||
|
||||
|
||||
class AuthToken(models.Model):
|
||||
|
||||
|
||||
def validate_note_no_token(self, note, token):
|
||||
""" Ensure plaintext token cant be saved to notes field.
|
||||
|
||||
called from app.settings.views.user_settings.TokenAdd.form_valid()
|
||||
|
||||
Args:
|
||||
note (Field): _Note field_
|
||||
token (Field): _Token field_
|
||||
|
||||
Raises:
|
||||
ValidationError: _Validation failed_
|
||||
"""
|
||||
|
||||
validation: bool = True
|
||||
|
||||
|
||||
if str(note) == str(token):
|
||||
|
||||
validation = False
|
||||
|
||||
|
||||
if str(token)[:9] in str(note): # Allow user to use up to 8 chars so they can reference it.
|
||||
|
||||
validation = False
|
||||
|
||||
if not validation:
|
||||
|
||||
raise ValidationError('Token can not be placed in the notes field.')
|
||||
|
||||
|
||||
|
||||
id = models.AutoField(
|
||||
primary_key=True,
|
||||
unique=True,
|
||||
blank=False
|
||||
)
|
||||
|
||||
note = models.CharField(
|
||||
blank = True,
|
||||
max_length = 50,
|
||||
default = None,
|
||||
null= True,
|
||||
)
|
||||
|
||||
token = models.CharField(
|
||||
verbose_name = 'Auth Token',
|
||||
db_index=True,
|
||||
max_length = 64,
|
||||
null = False,
|
||||
blank = False,
|
||||
unique = True,
|
||||
)
|
||||
|
||||
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE
|
||||
)
|
||||
|
||||
expires = models.DateTimeField(
|
||||
verbose_name = 'Expiry Date',
|
||||
null = False,
|
||||
blank = False
|
||||
)
|
||||
|
||||
|
||||
created = AutoCreatedField()
|
||||
|
||||
modified = AutoLastModifiedField()
|
||||
|
||||
|
||||
def generate(self) -> str:
|
||||
|
||||
return str(hashlib.sha256(str(self.randomword()).encode('utf-8')).hexdigest())
|
||||
|
||||
|
||||
def token_hash(self, token:str) -> str:
|
||||
|
||||
salt = settings.SECRET_KEY
|
||||
|
||||
return str(hashlib.sha256(str(token + salt).encode('utf-8')).hexdigest())
|
||||
|
||||
|
||||
def randomword(self) -> str:
|
||||
|
||||
return ''.join(random.choice(string.ascii_letters) for i in range(120))
|
||||
|
||||
|
||||
def __str__(self):
|
||||
|
||||
return self.token
|
0
app/api/serializers/__init__.py
Normal file
0
app/api/serializers/__init__.py
Normal file
139
app/api/serializers/access.py
Normal file
139
app/api/serializers/access.py
Normal file
@ -0,0 +1,139 @@
|
||||
from rest_framework import serializers, request
|
||||
from rest_framework.reverse import reverse
|
||||
from access.models import Organization, Team
|
||||
|
||||
from django.contrib.auth.models import Permission
|
||||
|
||||
|
||||
|
||||
class TeamSerializerBase(serializers.ModelSerializer):
|
||||
|
||||
url = serializers.SerializerMethodField('get_url')
|
||||
|
||||
|
||||
class Meta:
|
||||
model = Team
|
||||
fields = (
|
||||
'team_name',
|
||||
'model_notes',
|
||||
'permissions',
|
||||
'url',
|
||||
)
|
||||
|
||||
|
||||
def get_url(self, obj):
|
||||
|
||||
request = self.context.get('request')
|
||||
|
||||
return request.build_absolute_uri(reverse("API:_api_team", args=[obj.organization.id,obj.pk]))
|
||||
|
||||
|
||||
|
||||
class TeamPermissionSerializer(serializers.ModelSerializer):
|
||||
|
||||
|
||||
class Meta:
|
||||
model = Permission
|
||||
depth = 1
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class TeamSerializer(TeamSerializerBase):
|
||||
|
||||
permissions_url = serializers.SerializerMethodField('get_url')
|
||||
|
||||
def get_url(self, obj):
|
||||
|
||||
request = self.context.get('request')
|
||||
|
||||
team = Team.objects.get(pk=obj.id)
|
||||
|
||||
return request.build_absolute_uri(reverse('API:_api_team_permission', args=[team.organization_id,team.id]))
|
||||
|
||||
|
||||
def validate(self, data):
|
||||
"""
|
||||
Check that start is before finish.
|
||||
"""
|
||||
|
||||
data['organization_id'] = self._context['view'].kwargs['organization_id']
|
||||
|
||||
return data
|
||||
|
||||
|
||||
url = serializers.SerializerMethodField('team_url')
|
||||
|
||||
def team_url(self, obj):
|
||||
|
||||
request = self.context.get('request')
|
||||
|
||||
return request.build_absolute_uri(reverse('API:_api_team', args=[obj.organization_id,obj.id]))
|
||||
|
||||
|
||||
class Meta:
|
||||
model = Team
|
||||
depth = 2
|
||||
fields = (
|
||||
"id",
|
||||
"team_name",
|
||||
'model_notes',
|
||||
'permissions',
|
||||
'permissions_url',
|
||||
'url',
|
||||
)
|
||||
read_only_fields = [
|
||||
'id',
|
||||
'organization',
|
||||
'permissions_url',
|
||||
'url'
|
||||
]
|
||||
|
||||
|
||||
|
||||
class OrganizationListSerializer(serializers.ModelSerializer):
|
||||
|
||||
url = serializers.HyperlinkedIdentityField(
|
||||
view_name="API:_api_organization", format="html"
|
||||
)
|
||||
|
||||
|
||||
class Meta:
|
||||
model = Organization
|
||||
fields = (
|
||||
"id",
|
||||
"name",
|
||||
'url',
|
||||
)
|
||||
|
||||
|
||||
|
||||
class OrganizationSerializer(serializers.ModelSerializer):
|
||||
|
||||
url = serializers.HyperlinkedIdentityField(
|
||||
view_name="API:_api_organization", format="html"
|
||||
)
|
||||
|
||||
team_url = serializers.SerializerMethodField('get_url')
|
||||
|
||||
def get_url(self, obj):
|
||||
|
||||
request = self.context.get('request')
|
||||
|
||||
team = Team.objects.filter(pk=obj.id)
|
||||
|
||||
return request.build_absolute_uri(reverse('API:_api_organization_teams', args=[obj.id]))
|
||||
|
||||
teams = TeamSerializer(source='team_set', many=True, read_only=False)
|
||||
|
||||
view_name="API:_api_organization"
|
||||
|
||||
|
||||
class Meta:
|
||||
model = Organization
|
||||
fields = (
|
||||
"id",
|
||||
"name",
|
||||
'teams',
|
||||
'url',
|
||||
'team_url',
|
||||
)
|
86
app/api/serializers/config.py
Normal file
86
app/api/serializers/config.py
Normal file
@ -0,0 +1,86 @@
|
||||
from rest_framework import serializers
|
||||
from rest_framework.reverse import reverse
|
||||
|
||||
from config_management.models.groups import ConfigGroups
|
||||
|
||||
|
||||
|
||||
class ParentGroupSerializer(serializers.ModelSerializer):
|
||||
|
||||
url = serializers.SerializerMethodField('get_url')
|
||||
|
||||
|
||||
class Meta:
|
||||
model = ConfigGroups
|
||||
fields = [
|
||||
'id',
|
||||
'name',
|
||||
'url',
|
||||
]
|
||||
read_only_fields = [
|
||||
'id',
|
||||
'name',
|
||||
'url',
|
||||
]
|
||||
|
||||
|
||||
def get_url(self, obj):
|
||||
|
||||
request = self.context.get('request')
|
||||
|
||||
return request.build_absolute_uri(reverse("API:_api_config_group", args=[obj.pk]))
|
||||
|
||||
|
||||
|
||||
class ConfigGroupsSerializerBase(serializers.ModelSerializer):
|
||||
|
||||
parent = ParentGroupSerializer(read_only=True)
|
||||
url = serializers.SerializerMethodField('get_url')
|
||||
|
||||
|
||||
class Meta:
|
||||
model = ConfigGroups
|
||||
fields = [
|
||||
'id',
|
||||
'parent',
|
||||
'name',
|
||||
'config',
|
||||
'url',
|
||||
]
|
||||
read_only_fields = [
|
||||
'id',
|
||||
'name',
|
||||
'config',
|
||||
'url',
|
||||
]
|
||||
|
||||
|
||||
def get_url(self, obj):
|
||||
|
||||
request = self.context.get('request')
|
||||
|
||||
return request.build_absolute_uri(reverse("API:_api_config_group", args=[obj.pk]))
|
||||
|
||||
|
||||
|
||||
class ConfigGroupsSerializer(ConfigGroupsSerializerBase):
|
||||
|
||||
|
||||
class Meta:
|
||||
model = ConfigGroups
|
||||
depth = 1
|
||||
fields = [
|
||||
'id',
|
||||
'parent',
|
||||
'name',
|
||||
'config',
|
||||
'url',
|
||||
]
|
||||
read_only_fields = [
|
||||
'id',
|
||||
'parent',
|
||||
'name',
|
||||
'config',
|
||||
'url',
|
||||
]
|
||||
|
168
app/api/serializers/inventory.py
Normal file
168
app/api/serializers/inventory.py
Normal file
@ -0,0 +1,168 @@
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.html import escape
|
||||
|
||||
class Inventory:
|
||||
""" Inventory Object
|
||||
|
||||
Pass in an Inventory dict that a device has provided and sanitize ready for use.
|
||||
|
||||
Raises:
|
||||
ValidationError: Malformed inventory data.
|
||||
"""
|
||||
|
||||
|
||||
class Details:
|
||||
|
||||
_name: str
|
||||
|
||||
_serial_number: str
|
||||
|
||||
_uuid: str
|
||||
|
||||
|
||||
def __init__(self, details: dict):
|
||||
|
||||
self._name = escape(details['name'])
|
||||
|
||||
self._serial_number = escape(details['serial_number'])
|
||||
|
||||
self._uuid = escape(details['uuid'])
|
||||
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
|
||||
return str(self._name)
|
||||
|
||||
|
||||
@property
|
||||
def serial_number(self) -> str:
|
||||
|
||||
return str(self._serial_number)
|
||||
|
||||
|
||||
@property
|
||||
def uuid(self) -> str:
|
||||
|
||||
return str(self._uuid)
|
||||
|
||||
|
||||
|
||||
class OperatingSystem:
|
||||
|
||||
_name: str
|
||||
|
||||
_version_major: str
|
||||
|
||||
_version: str
|
||||
|
||||
|
||||
def __init__(self, operating_system: dict):
|
||||
|
||||
self._name = escape(operating_system['name'])
|
||||
|
||||
self._version_major = escape(operating_system['version_major'])
|
||||
|
||||
self._version = escape(operating_system['version'])
|
||||
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
|
||||
return str(self._name)
|
||||
|
||||
|
||||
@property
|
||||
def version_major(self) -> str:
|
||||
|
||||
return str(self._version_major)
|
||||
|
||||
|
||||
@property
|
||||
def version(self) -> str:
|
||||
|
||||
return str(self._version)
|
||||
|
||||
|
||||
|
||||
class Software:
|
||||
|
||||
_name: str
|
||||
|
||||
_category: str
|
||||
|
||||
_version: str
|
||||
|
||||
|
||||
def __init__(self, software: dict):
|
||||
|
||||
self._name = escape(software['name'])
|
||||
|
||||
self._category = escape(software['category'])
|
||||
|
||||
self._version = escape(software['version'])
|
||||
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
|
||||
return str(self._name)
|
||||
|
||||
|
||||
@property
|
||||
def category(self) -> str:
|
||||
|
||||
return str(self._category)
|
||||
|
||||
|
||||
@property
|
||||
def version(self) -> str:
|
||||
|
||||
return str(self._version)
|
||||
|
||||
|
||||
|
||||
_details: Details = None
|
||||
|
||||
_operating_system: OperatingSystem = None
|
||||
|
||||
_software: list[Software] = []
|
||||
|
||||
|
||||
def __init__(self, inventory: dict):
|
||||
|
||||
if (
|
||||
type(inventory['details']) is dict and
|
||||
type(inventory['os']) is dict and
|
||||
type(inventory['software']) is list
|
||||
):
|
||||
|
||||
self._details = self.Details(inventory['details'])
|
||||
|
||||
self._operating_system = self.OperatingSystem(inventory['os'])
|
||||
|
||||
for software in inventory['software']:
|
||||
|
||||
self._software += [ self.Software(software) ]
|
||||
|
||||
else:
|
||||
|
||||
raise ValidationError('Inventory File is invalid')
|
||||
|
||||
|
||||
@property
|
||||
def details(self) -> Details:
|
||||
|
||||
return self._details
|
||||
|
||||
|
||||
@property
|
||||
def operating_system(self) -> OperatingSystem:
|
||||
|
||||
return self._operating_system
|
||||
|
||||
|
||||
@property
|
||||
def software(self) -> list[Software]:
|
||||
|
||||
return list(self._software)
|
81
app/api/serializers/itam/device.py
Normal file
81
app/api/serializers/itam/device.py
Normal file
@ -0,0 +1,81 @@
|
||||
from django.urls import reverse
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from api.serializers.config import ParentGroupSerializer
|
||||
|
||||
from config_management.models.groups import ConfigGroupHosts
|
||||
|
||||
from itam.models.device import Device
|
||||
|
||||
|
||||
|
||||
class DeviceConfigGroupsSerializer(serializers.ModelSerializer):
|
||||
|
||||
name = serializers.CharField(source='group.name', read_only=True)
|
||||
|
||||
url = serializers.HyperlinkedIdentityField(
|
||||
view_name="API:_api_config_group", format="html"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
|
||||
model = ConfigGroupHosts
|
||||
|
||||
fields = [
|
||||
'id',
|
||||
'name',
|
||||
'url',
|
||||
|
||||
]
|
||||
read_only_fields = [
|
||||
'id',
|
||||
'name',
|
||||
'url',
|
||||
]
|
||||
|
||||
|
||||
class DeviceSerializer(serializers.ModelSerializer):
|
||||
|
||||
url = serializers.HyperlinkedIdentityField(
|
||||
view_name="API:device-detail", format="html"
|
||||
)
|
||||
|
||||
config = serializers.SerializerMethodField('get_device_config')
|
||||
|
||||
groups = DeviceConfigGroupsSerializer(source='configgrouphosts_set', many=True, read_only=True)
|
||||
|
||||
def get_device_config(self, device):
|
||||
|
||||
request = self.context.get('request')
|
||||
return request.build_absolute_uri(reverse('API:_api_device_config', args=[device.slug]))
|
||||
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
depth = 1
|
||||
fields = [
|
||||
'id',
|
||||
'is_global',
|
||||
'name',
|
||||
'config',
|
||||
'serial_number',
|
||||
'uuid',
|
||||
'inventorydate',
|
||||
'created',
|
||||
'modified',
|
||||
'groups',
|
||||
'organization',
|
||||
'url',
|
||||
]
|
||||
|
||||
read_only_fields = [
|
||||
'id',
|
||||
'config',
|
||||
'inventorydate',
|
||||
'created',
|
||||
'modified',
|
||||
'groups',
|
||||
'url',
|
||||
]
|
||||
|
73
app/api/serializers/itam/inventory.py
Normal file
73
app/api/serializers/itam/inventory.py
Normal file
@ -0,0 +1,73 @@
|
||||
from django.urls import reverse
|
||||
|
||||
from itam.models.device import Device
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
|
||||
|
||||
class InventorySerializer(serializers.Serializer):
|
||||
""" Serializer for Inventory Upload """
|
||||
|
||||
|
||||
class DetailsSerializer(serializers.Serializer):
|
||||
|
||||
name = serializers.CharField(
|
||||
help_text = 'Host name',
|
||||
required = True
|
||||
)
|
||||
|
||||
serial_number = serializers.CharField(
|
||||
help_text = 'Devices serial number',
|
||||
required = True
|
||||
)
|
||||
|
||||
uuid = serializers.CharField(
|
||||
help_text = 'Device system UUID',
|
||||
required = True
|
||||
)
|
||||
|
||||
|
||||
class OperatingSystemSerializer(serializers.Serializer):
|
||||
|
||||
name = serializers.CharField(
|
||||
help_text='Name of the operating system installed on the device',
|
||||
required = True,
|
||||
)
|
||||
|
||||
version_major = serializers.IntegerField(
|
||||
help_text='Major semver version number of the OS version',
|
||||
required = True,
|
||||
)
|
||||
|
||||
version = serializers.CharField(
|
||||
help_text='semver version number of the OS',
|
||||
required = True
|
||||
)
|
||||
|
||||
|
||||
class SoftwareSerializer(serializers.Serializer):
|
||||
|
||||
name = serializers.CharField(
|
||||
help_text='Name of the software',
|
||||
required = True
|
||||
)
|
||||
|
||||
category = serializers.CharField(
|
||||
help_text='Category of the software',
|
||||
default = None,
|
||||
required = False
|
||||
)
|
||||
|
||||
version = serializers.CharField(
|
||||
default = None,
|
||||
help_text='semver version number of the software',
|
||||
required = False
|
||||
)
|
||||
|
||||
|
||||
details = DetailsSerializer()
|
||||
|
||||
os = OperatingSystemSerializer()
|
||||
|
||||
software = SoftwareSerializer(many = True)
|
19
app/api/serializers/itam/software.py
Normal file
19
app/api/serializers/itam/software.py
Normal file
@ -0,0 +1,19 @@
|
||||
from rest_framework import serializers
|
||||
from itam.models.device import Software
|
||||
|
||||
|
||||
|
||||
|
||||
class SoftwareSerializer(serializers.ModelSerializer):
|
||||
|
||||
url = serializers.HyperlinkedIdentityField(
|
||||
view_name="API:software-detail", format="html"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Software
|
||||
fields = '__all__'
|
||||
|
||||
read_only_fields = [
|
||||
'slug',
|
||||
]
|
446
app/api/tasks.py
Normal file
446
app/api/tasks.py
Normal file
@ -0,0 +1,446 @@
|
||||
import json
|
||||
import re
|
||||
|
||||
from django.utils import timezone
|
||||
|
||||
from celery import shared_task, current_task
|
||||
from celery.utils.log import get_task_logger
|
||||
from celery import states
|
||||
|
||||
from access.models import Organization
|
||||
|
||||
from api.serializers.inventory import Inventory
|
||||
|
||||
from itam.models.device import Device, DeviceType, DeviceOperatingSystem, DeviceSoftware
|
||||
from itam.models.operating_system import OperatingSystem, OperatingSystemVersion
|
||||
from itam.models.software import Software, SoftwareCategory, SoftwareVersion
|
||||
|
||||
from settings.models.app_settings import AppSettings
|
||||
|
||||
|
||||
logger = get_task_logger(__name__)
|
||||
|
||||
@shared_task(bind=True)
|
||||
def process_inventory(self, data, organization: int):
|
||||
|
||||
device = None
|
||||
device_operating_system = None
|
||||
operating_system = None
|
||||
operating_system_version = None
|
||||
|
||||
try:
|
||||
|
||||
logger.info('Begin Processing Inventory')
|
||||
|
||||
data = json.loads(data)
|
||||
data = Inventory(data)
|
||||
|
||||
organization = Organization.objects.get(id=organization)
|
||||
|
||||
app_settings = AppSettings.objects.get(owner_organization = None)
|
||||
|
||||
device_serial_number = None
|
||||
device_uuid = None
|
||||
|
||||
if data.details.serial_number and str(data.details.serial_number).lower() != 'na':
|
||||
|
||||
device_serial_number = str(data.details.serial_number)
|
||||
|
||||
if data.details.uuid and str(data.details.uuid).lower() != 'na':
|
||||
|
||||
device_uuid = str(data.details.uuid)
|
||||
|
||||
|
||||
if device_serial_number: # Search for device by serial number.
|
||||
|
||||
device = Device.objects.filter(
|
||||
serial_number__iexact=device_serial_number
|
||||
)
|
||||
|
||||
if device.exists():
|
||||
|
||||
device = Device.objects.get(
|
||||
serial_number__iexact=device_serial_number
|
||||
)
|
||||
|
||||
else:
|
||||
|
||||
device = None
|
||||
|
||||
|
||||
if device_uuid and not device: # Search for device by UUID.
|
||||
|
||||
device = Device.objects.filter(
|
||||
uuid__iexact=device_uuid
|
||||
)
|
||||
|
||||
if device.exists():
|
||||
|
||||
device = Device.objects.get(
|
||||
uuid__iexact=device_uuid
|
||||
)
|
||||
|
||||
else:
|
||||
|
||||
device = None
|
||||
|
||||
|
||||
if not device: # Search for device by Name.
|
||||
|
||||
device = Device.objects.filter(
|
||||
name__iexact=str(data.details.name).lower()
|
||||
)
|
||||
|
||||
if device.exists():
|
||||
|
||||
device = Device.objects.get(
|
||||
name__iexact=str(data.details.name).lower()
|
||||
)
|
||||
|
||||
else:
|
||||
|
||||
device = None
|
||||
|
||||
|
||||
|
||||
|
||||
if not device: # Create the device
|
||||
|
||||
device = Device.objects.create(
|
||||
name = data.details.name,
|
||||
device_type = None,
|
||||
serial_number = device_serial_number,
|
||||
uuid = device_uuid,
|
||||
organization = organization,
|
||||
)
|
||||
|
||||
|
||||
if device:
|
||||
|
||||
logger.info(f"Device: {device.name}, Serial: {device.serial_number}, UUID: {device.uuid}")
|
||||
|
||||
device_edited = False
|
||||
|
||||
|
||||
if not device.uuid and device_uuid:
|
||||
|
||||
device.uuid = device_uuid
|
||||
|
||||
device_edited = True
|
||||
|
||||
|
||||
if not device.serial_number and device_serial_number:
|
||||
|
||||
device.serial_number = data.details.serial_number
|
||||
|
||||
device_edited = True
|
||||
|
||||
|
||||
if str(device.name).lower() != str(data.details.name).lower(): # Update device Name
|
||||
|
||||
device.name = data.details.name
|
||||
|
||||
device_edited = True
|
||||
|
||||
|
||||
if device_edited:
|
||||
|
||||
device.save()
|
||||
|
||||
|
||||
operating_system = OperatingSystem.objects.filter(
|
||||
name=data.operating_system.name,
|
||||
is_global = True
|
||||
)
|
||||
|
||||
if operating_system.exists():
|
||||
|
||||
operating_system = OperatingSystem.objects.get(
|
||||
name=data.operating_system.name,
|
||||
is_global = True
|
||||
)
|
||||
|
||||
|
||||
else:
|
||||
|
||||
operating_system = None
|
||||
|
||||
|
||||
|
||||
if not operating_system:
|
||||
|
||||
operating_system = OperatingSystem.objects.filter(
|
||||
name=data.operating_system.name,
|
||||
organization = organization
|
||||
)
|
||||
|
||||
|
||||
if operating_system.exists():
|
||||
|
||||
operating_system = OperatingSystem.objects.get(
|
||||
name=data.operating_system.name,
|
||||
organization = organization
|
||||
)
|
||||
|
||||
else:
|
||||
|
||||
operating_system = None
|
||||
|
||||
|
||||
if not operating_system:
|
||||
|
||||
operating_system = OperatingSystem.objects.create(
|
||||
name = data.operating_system.name,
|
||||
organization = organization,
|
||||
is_global = True
|
||||
)
|
||||
|
||||
|
||||
operating_system_version = OperatingSystemVersion.objects.filter(
|
||||
name=data.operating_system.version_major,
|
||||
operating_system=operating_system
|
||||
)
|
||||
|
||||
if operating_system_version.exists():
|
||||
|
||||
operating_system_version = OperatingSystemVersion.objects.get(
|
||||
name=data.operating_system.version_major,
|
||||
operating_system=operating_system
|
||||
)
|
||||
|
||||
else:
|
||||
|
||||
operating_system_version = None
|
||||
|
||||
|
||||
if not operating_system_version:
|
||||
|
||||
operating_system_version = OperatingSystemVersion.objects.create(
|
||||
organization = organization,
|
||||
is_global = True,
|
||||
name = data.operating_system.version_major,
|
||||
operating_system = operating_system,
|
||||
)
|
||||
|
||||
device_operating_system = DeviceOperatingSystem.objects.filter(
|
||||
device=device,
|
||||
)
|
||||
|
||||
if device_operating_system.exists():
|
||||
|
||||
device_operating_system = DeviceOperatingSystem.objects.get(
|
||||
device=device,
|
||||
)
|
||||
|
||||
else:
|
||||
|
||||
device_operating_system = None
|
||||
|
||||
|
||||
if not device_operating_system:
|
||||
|
||||
device_operating_system = DeviceOperatingSystem.objects.create(
|
||||
organization = organization,
|
||||
device=device,
|
||||
version = data.operating_system.version,
|
||||
operating_system_version = operating_system_version,
|
||||
installdate = timezone.now()
|
||||
)
|
||||
|
||||
if not device_operating_system.installdate: # Only update install date if empty
|
||||
|
||||
device_operating_system.installdate = timezone.now()
|
||||
|
||||
device_operating_system.save()
|
||||
|
||||
|
||||
if device_operating_system.operating_system_version != operating_system_version:
|
||||
|
||||
device_operating_system.operating_system_version = operating_system_version
|
||||
|
||||
device_operating_system.save()
|
||||
|
||||
|
||||
if device_operating_system.version != data.operating_system.version:
|
||||
|
||||
device_operating_system.version = data.operating_system.version
|
||||
|
||||
device_operating_system.save()
|
||||
|
||||
|
||||
if app_settings.software_is_global:
|
||||
|
||||
software_organization = app_settings.global_organization
|
||||
|
||||
else:
|
||||
|
||||
software_organization = device.organization
|
||||
|
||||
|
||||
if app_settings.software_categories_is_global:
|
||||
|
||||
software_category_organization = app_settings.global_organization
|
||||
|
||||
else:
|
||||
|
||||
software_category_organization = device.organization
|
||||
|
||||
inventoried_software: list = []
|
||||
|
||||
for inventory in list(data.software):
|
||||
|
||||
software = None
|
||||
software_category = None
|
||||
software_version = None
|
||||
|
||||
device_software = None
|
||||
|
||||
software_category = SoftwareCategory.objects.filter( name = inventory.category )
|
||||
|
||||
|
||||
if software_category.exists():
|
||||
|
||||
software_category = SoftwareCategory.objects.get(
|
||||
name = inventory.category
|
||||
)
|
||||
|
||||
else: # Create Software Category
|
||||
|
||||
software_category = SoftwareCategory.objects.create(
|
||||
organization = software_category_organization,
|
||||
is_global = True,
|
||||
name = inventory.category,
|
||||
)
|
||||
|
||||
|
||||
if software_category.name == inventory.category:
|
||||
|
||||
if Software.objects.filter( name = inventory.name ).exists():
|
||||
|
||||
software = Software.objects.get(
|
||||
name = inventory.name
|
||||
)
|
||||
|
||||
if not software.category:
|
||||
|
||||
software.category = software_category
|
||||
software.save()
|
||||
|
||||
else: # Create Software
|
||||
|
||||
software = Software.objects.create(
|
||||
organization = software_organization,
|
||||
is_global = True,
|
||||
name = inventory.name,
|
||||
category = software_category,
|
||||
)
|
||||
|
||||
|
||||
if software.name == inventory.name:
|
||||
|
||||
pattern = r"^(\d+:)?(?P<semver>\d+\.\d+(\.\d+)?)"
|
||||
|
||||
semver = re.search(pattern, str(inventory.version), re.DOTALL)
|
||||
|
||||
|
||||
if semver:
|
||||
|
||||
semver = semver['semver']
|
||||
|
||||
else:
|
||||
semver = inventory.version
|
||||
|
||||
|
||||
if SoftwareVersion.objects.filter( name = semver, software = software ).exists():
|
||||
|
||||
software_version = SoftwareVersion.objects.get(
|
||||
name = semver,
|
||||
software = software,
|
||||
)
|
||||
|
||||
else: # Create Software Category
|
||||
|
||||
software_version = SoftwareVersion.objects.create(
|
||||
organization = organization,
|
||||
is_global = True,
|
||||
name = semver,
|
||||
software = software,
|
||||
)
|
||||
|
||||
|
||||
if software_version.name == semver:
|
||||
|
||||
if DeviceSoftware.objects.filter( software = software, device=device ).exists():
|
||||
|
||||
device_software = DeviceSoftware.objects.get(
|
||||
device = device,
|
||||
software = software
|
||||
)
|
||||
|
||||
logger.debug(f"Select Existing Device Software: {device_software.software.name}")
|
||||
|
||||
else: # Create Software
|
||||
|
||||
device_software = DeviceSoftware.objects.create(
|
||||
organization = organization,
|
||||
is_global = True,
|
||||
installedversion = software_version,
|
||||
software = software,
|
||||
device = device,
|
||||
action=None
|
||||
)
|
||||
|
||||
|
||||
logger.debug(f"Create Device Software: {device_software.software.name}")
|
||||
|
||||
|
||||
if device_software: # Update the Inventoried software
|
||||
|
||||
inventoried_software += [ device_software.id ]
|
||||
|
||||
|
||||
if not device_software.installed: # Only update install date if blank
|
||||
|
||||
device_software.installed = timezone.now()
|
||||
|
||||
device_software.save()
|
||||
|
||||
logger.debug(f"Update Device Software (installed): {device_software.software.name}")
|
||||
|
||||
|
||||
if device_software.installedversion.name != software_version.name:
|
||||
|
||||
device_software.installedversion = software_version
|
||||
|
||||
device_software.save()
|
||||
|
||||
logger.debug(f"Update Device Software (installedversion): {device_software.software.name}")
|
||||
|
||||
for not_installed in DeviceSoftware.objects.filter( device=device ):
|
||||
|
||||
if not_installed.id not in inventoried_software:
|
||||
|
||||
not_installed.delete()
|
||||
|
||||
logger.debug(f"Remove Device Software: {not_installed.software.name}")
|
||||
|
||||
|
||||
if device and operating_system and operating_system_version and device_operating_system:
|
||||
|
||||
|
||||
device.inventorydate = timezone.now()
|
||||
|
||||
device.save()
|
||||
|
||||
|
||||
logger.info('Finish Processing Inventory')
|
||||
|
||||
return str('finished...')
|
||||
|
||||
except Exception as e:
|
||||
|
||||
logger.critical('Exception')
|
||||
|
||||
raise Exception(e)
|
||||
|
||||
return str(f'Exception Occured: {e}')
|
0
app/api/tests/__init__.py
Normal file
0
app/api/tests/__init__.py
Normal file
0
app/api/tests/abstract/__init__.py
Normal file
0
app/api/tests/abstract/__init__.py
Normal file
470
app/api/tests/abstract/api_permissions.py
Normal file
470
app/api/tests/abstract/api_permissions.py
Normal file
@ -0,0 +1,470 @@
|
||||
import pytest
|
||||
import unittest
|
||||
|
||||
from django.shortcuts import reverse
|
||||
from django.test import TestCase, Client
|
||||
|
||||
|
||||
|
||||
class APIPermissionView:
|
||||
|
||||
|
||||
model: object
|
||||
""" Item Model to test """
|
||||
|
||||
app_namespace: str = None
|
||||
""" URL namespace """
|
||||
|
||||
url_name: str
|
||||
""" URL name of the view to test """
|
||||
|
||||
url_view_kwargs: dict = None
|
||||
""" URL kwargs of the item page """
|
||||
|
||||
|
||||
def test_view_user_anon_denied(self):
|
||||
""" Check correct permission for view
|
||||
|
||||
Attempt to view as anon user
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs)
|
||||
|
||||
response = client.get(url)
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
def test_view_no_permission_denied(self):
|
||||
""" Check correct permission for view
|
||||
|
||||
Attempt to view with user missing permission
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs)
|
||||
|
||||
|
||||
client.force_login(self.no_permissions_user)
|
||||
response = client.get(url)
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
def test_view_different_organizaiton_denied(self):
|
||||
""" Check correct permission for view
|
||||
|
||||
Attempt to view with user from different organization
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs)
|
||||
|
||||
|
||||
client.force_login(self.different_organization_user)
|
||||
response = client.get(url)
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
def test_view_has_permission(self):
|
||||
""" Check correct permission for view
|
||||
|
||||
Attempt to view as user with view permission
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs)
|
||||
|
||||
|
||||
client.force_login(self.view_user)
|
||||
response = client.get(url)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
|
||||
class APIPermissionAdd:
|
||||
|
||||
|
||||
model: object
|
||||
""" Item Model to test """
|
||||
|
||||
app_namespace: str = None
|
||||
""" URL namespace """
|
||||
|
||||
url_list: str
|
||||
""" URL view name of the item list page """
|
||||
|
||||
url_kwargs: dict = None
|
||||
""" URL view kwargs for the item list page """
|
||||
|
||||
add_data: dict = None
|
||||
|
||||
|
||||
def test_add_user_anon_denied(self):
|
||||
""" Check correct permission for add
|
||||
|
||||
Attempt to add as anon user
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
if self.url_kwargs:
|
||||
|
||||
url = reverse(self.app_namespace + ':' + self.url_list, kwargs = self.url_kwargs)
|
||||
|
||||
else:
|
||||
|
||||
url = reverse(self.app_namespace + ':' + self.url_list)
|
||||
|
||||
|
||||
response = client.put(url, data=self.add_data)
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
# @pytest.mark.skip(reason="ToDO: figure out why fails")
|
||||
def test_add_no_permission_denied(self):
|
||||
""" Check correct permission for add
|
||||
|
||||
Attempt to add as user with no permissions
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
if self.url_kwargs:
|
||||
|
||||
url = reverse(self.app_namespace + ':' + self.url_list, kwargs = self.url_kwargs)
|
||||
|
||||
else:
|
||||
|
||||
url = reverse(self.app_namespace + ':' + self.url_list)
|
||||
|
||||
|
||||
client.force_login(self.no_permissions_user)
|
||||
response = client.post(url, data=self.add_data)
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
# @pytest.mark.skip(reason="ToDO: figure out why fails")
|
||||
def test_add_different_organization_denied(self):
|
||||
""" Check correct permission for add
|
||||
|
||||
attempt to add as user from different organization
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
if self.url_kwargs:
|
||||
|
||||
url = reverse(self.app_namespace + ':' + self.url_list, kwargs = self.url_kwargs)
|
||||
|
||||
else:
|
||||
|
||||
url = reverse(self.app_namespace + ':' + self.url_list)
|
||||
|
||||
|
||||
client.force_login(self.different_organization_user)
|
||||
response = client.post(url, data=self.add_data)
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
def test_add_permission_view_denied(self):
|
||||
""" Check correct permission for add
|
||||
|
||||
Attempt to add a user with view permission
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
if self.url_kwargs:
|
||||
|
||||
url = reverse(self.app_namespace + ':' + self.url_list, kwargs = self.url_kwargs)
|
||||
|
||||
else:
|
||||
|
||||
url = reverse(self.app_namespace + ':' + self.url_list)
|
||||
|
||||
|
||||
client.force_login(self.view_user)
|
||||
response = client.post(url, data=self.add_data)
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
def test_add_has_permission(self):
|
||||
""" Check correct permission for add
|
||||
|
||||
Attempt to add as user with no permission
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
if self.url_kwargs:
|
||||
|
||||
url = reverse(self.app_namespace + ':' + self.url_list, kwargs = self.url_kwargs)
|
||||
|
||||
else:
|
||||
|
||||
url = reverse(self.app_namespace + ':' + self.url_list)
|
||||
|
||||
|
||||
client.force_login(self.add_user)
|
||||
response = client.post(url, data=self.add_data)
|
||||
|
||||
assert response.status_code == 201
|
||||
|
||||
|
||||
|
||||
class APIPermissionChange:
|
||||
|
||||
|
||||
model: object
|
||||
""" Item Model to test """
|
||||
|
||||
app_namespace: str = None
|
||||
""" URL namespace """
|
||||
|
||||
url_name: str
|
||||
""" URL name of the view to test """
|
||||
|
||||
url_view_kwargs: dict = None
|
||||
""" URL kwargs of the item page """
|
||||
|
||||
change_data: dict = None
|
||||
|
||||
|
||||
def test_change_user_anon_denied(self):
|
||||
""" Check correct permission for change
|
||||
|
||||
Attempt to change as anon
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs)
|
||||
|
||||
|
||||
response = client.patch(url, data=self.change_data, content_type='application/json')
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
def test_change_no_permission_denied(self):
|
||||
""" Ensure permission view cant make change
|
||||
|
||||
Attempt to make change as user without permissions
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs)
|
||||
|
||||
|
||||
client.force_login(self.no_permissions_user)
|
||||
response = client.patch(url, data=self.change_data, content_type='application/json')
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
def test_change_different_organization_denied(self):
|
||||
""" Ensure permission view cant make change
|
||||
|
||||
Attempt to make change as user from different organization
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs)
|
||||
|
||||
|
||||
client.force_login(self.different_organization_user)
|
||||
response = client.patch(url, data=self.change_data, content_type='application/json')
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
def test_change_permission_view_denied(self):
|
||||
""" Ensure permission view cant make change
|
||||
|
||||
Attempt to make change as user with view permission
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs)
|
||||
|
||||
|
||||
client.force_login(self.view_user)
|
||||
response = client.patch(url, data=self.change_data, content_type='application/json')
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
def test_change_permission_add_denied(self):
|
||||
""" Ensure permission view cant make change
|
||||
|
||||
Attempt to make change as user with add permission
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs)
|
||||
|
||||
|
||||
client.force_login(self.add_user)
|
||||
response = client.patch(url, data=self.change_data, content_type='application/json')
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
def test_change_has_permission(self):
|
||||
""" Check correct permission for change
|
||||
|
||||
Make change with user who has change permission
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs)
|
||||
|
||||
|
||||
client.force_login(self.change_user)
|
||||
response = client.patch(url, data=self.change_data, content_type='application/json')
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
|
||||
class APIPermissionDelete:
|
||||
|
||||
|
||||
model: object
|
||||
""" Item Model to test """
|
||||
|
||||
app_namespace: str = None
|
||||
""" URL namespace """
|
||||
|
||||
url_name: str
|
||||
""" URL name of the view to test """
|
||||
|
||||
url_view_kwargs: dict = None
|
||||
""" URL kwargs of the item page """
|
||||
|
||||
delete_data: dict = None
|
||||
|
||||
|
||||
def test_delete_user_anon_denied(self):
|
||||
""" Check correct permission for delete
|
||||
|
||||
Attempt to delete item as anon user
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs)
|
||||
|
||||
|
||||
response = client.delete(url, data=self.delete_data)
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
def test_delete_no_permission_denied(self):
|
||||
""" Check correct permission for delete
|
||||
|
||||
Attempt to delete as user with no permissons
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs)
|
||||
|
||||
|
||||
client.force_login(self.no_permissions_user)
|
||||
response = client.delete(url, data=self.delete_data)
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
def test_delete_different_organization_denied(self):
|
||||
""" Check correct permission for delete
|
||||
|
||||
Attempt to delete as user from different organization
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs)
|
||||
|
||||
|
||||
client.force_login(self.different_organization_user)
|
||||
response = client.delete(url, data=self.delete_data)
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
def test_delete_permission_view_denied(self):
|
||||
""" Check correct permission for delete
|
||||
|
||||
Attempt to delete as user with veiw permission only
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs)
|
||||
|
||||
|
||||
client.force_login(self.view_user)
|
||||
response = client.delete(url, data=self.delete_data)
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
def test_delete_permission_add_denied(self):
|
||||
""" Check correct permission for delete
|
||||
|
||||
Attempt to delete as user with add permission only
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs)
|
||||
|
||||
|
||||
client.force_login(self.add_user)
|
||||
response = client.delete(url, data=self.delete_data)
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
def test_delete_permission_change_denied(self):
|
||||
""" Check correct permission for delete
|
||||
|
||||
Attempt to delete as user with change permission only
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs)
|
||||
|
||||
|
||||
client.force_login(self.change_user)
|
||||
response = client.delete(url, data=self.delete_data)
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
def test_delete_has_permission(self):
|
||||
""" Check correct permission for delete
|
||||
|
||||
Delete item as user with delete permission
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.app_namespace + ':' + self.url_name, kwargs=self.url_view_kwargs)
|
||||
|
||||
|
||||
client.force_login(self.delete_user)
|
||||
response = client.delete(url, data=self.delete_data)
|
||||
|
||||
assert response.status_code == 204
|
||||
|
||||
|
||||
|
||||
class APIPermissions(
|
||||
APIPermissionAdd,
|
||||
APIPermissionChange,
|
||||
APIPermissionDelete,
|
||||
APIPermissionView
|
||||
):
|
||||
""" Abstract class containing all API Permission test cases """
|
||||
|
||||
model: object
|
||||
""" Item Model to test """
|
0
app/api/tests/unit/__init__.py
Normal file
0
app/api/tests/unit/__init__.py
Normal file
989
app/api/tests/unit/inventory/test_api_inventory.py
Normal file
989
app/api/tests/unit/inventory/test_api_inventory.py
Normal file
@ -0,0 +1,989 @@
|
||||
import datetime
|
||||
import json
|
||||
import pytest
|
||||
import unittest
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.shortcuts import reverse
|
||||
from django.test import TestCase, Client
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from access.models import Organization, Team, TeamUsers, Permission
|
||||
|
||||
from api.views.mixin import OrganizationPermissionAPI
|
||||
from api.serializers.inventory import Inventory
|
||||
|
||||
from api.tasks import process_inventory
|
||||
|
||||
from itam.models.device import Device, DeviceOperatingSystem, DeviceSoftware
|
||||
from itam.models.operating_system import OperatingSystem, OperatingSystemVersion
|
||||
from itam.models.software import Software, SoftwareCategory, SoftwareVersion
|
||||
|
||||
from settings.models.user_settings import UserSettings
|
||||
|
||||
|
||||
|
||||
class InventoryAPI(TestCase):
|
||||
|
||||
model = Device
|
||||
|
||||
model_name = 'device'
|
||||
app_label = 'itam'
|
||||
|
||||
inventory = {
|
||||
"details": {
|
||||
"name": "device_name",
|
||||
"serial_number": "a serial number",
|
||||
"uuid": "string"
|
||||
},
|
||||
"os": {
|
||||
"name": "os_name",
|
||||
"version_major": "12",
|
||||
"version": "12.1"
|
||||
},
|
||||
"software": [
|
||||
{
|
||||
"name": "software_name",
|
||||
"category": "category_name",
|
||||
"version": "1.2.3"
|
||||
},
|
||||
{
|
||||
"name": "software_name_not_semver",
|
||||
"category": "category_name",
|
||||
"version": "2024.4"
|
||||
},
|
||||
{
|
||||
"name": "software_name_semver_contained",
|
||||
"category": "category_name",
|
||||
"version": "1.2.3-rc1"
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
"""Setup Test
|
||||
|
||||
1. Create an organization for user
|
||||
2. Create a team for user with correct permissions
|
||||
3. add user to the teeam
|
||||
4. upload the inventory
|
||||
5. conduct queries for tests
|
||||
"""
|
||||
|
||||
organization = Organization.objects.create(name='test_org')
|
||||
|
||||
self.organization = organization
|
||||
|
||||
add_permissions = Permission.objects.get(
|
||||
codename = 'add_' + self.model_name,
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = self.app_label,
|
||||
model = self.model_name,
|
||||
)
|
||||
)
|
||||
|
||||
add_team = Team.objects.create(
|
||||
team_name = 'add_team',
|
||||
organization = organization,
|
||||
)
|
||||
|
||||
add_team.permissions.set([add_permissions])
|
||||
|
||||
self.add_user = User.objects.create_user(username="test_user_add", password="password")
|
||||
|
||||
add_user_settings = UserSettings.objects.get(user=self.add_user)
|
||||
|
||||
add_user_settings.default_organization = organization
|
||||
|
||||
add_user_settings.save()
|
||||
|
||||
teamuser = TeamUsers.objects.create(
|
||||
team = add_team,
|
||||
user = self.add_user
|
||||
)
|
||||
|
||||
# upload the inventory
|
||||
process_inventory(json.dumps(self.inventory), organization.id)
|
||||
|
||||
|
||||
self.device = Device.objects.get(name=self.inventory['details']['name'])
|
||||
|
||||
self.operating_system = OperatingSystem.objects.get(name=self.inventory['os']['name'])
|
||||
|
||||
self.operating_system_version = OperatingSystemVersion.objects.get(name=self.inventory['os']['version_major'])
|
||||
|
||||
self.device_operating_system = DeviceOperatingSystem.objects.get(version=self.inventory['os']['version'])
|
||||
|
||||
self.software = Software.objects.get(name=self.inventory['software'][0]['name'])
|
||||
|
||||
self.software_category = SoftwareCategory.objects.get(name=self.inventory['software'][0]['category'])
|
||||
|
||||
self.software_version = SoftwareVersion.objects.get(
|
||||
name = self.inventory['software'][0]['version'],
|
||||
software = self.software,
|
||||
)
|
||||
|
||||
self.software_not_semver = Software.objects.get(name=self.inventory['software'][1]['name'])
|
||||
|
||||
self.software_version_not_semver = SoftwareVersion.objects.get(
|
||||
name = self.inventory['software'][1]['version'],
|
||||
software = self.software_not_semver
|
||||
)
|
||||
|
||||
self.software_is_semver = Software.objects.get(name=self.inventory['software'][2]['name'])
|
||||
|
||||
self.software_version_is_semver = SoftwareVersion.objects.get(
|
||||
software = self.software_is_semver
|
||||
)
|
||||
|
||||
self.device_software = DeviceSoftware.objects.get(device=self.device,software=self.software)
|
||||
|
||||
|
||||
|
||||
|
||||
@override_settings(CELERY_TASK_ALWAYS_EAGER=True,
|
||||
CELERY_TASK_EAGER_PROPOGATES=True)
|
||||
@patch.object(OrganizationPermissionAPI, 'permission_check')
|
||||
def test_inventory_function_called_permission_check(self, permission_check):
|
||||
""" Inventory Upload checks permissions
|
||||
|
||||
Function 'permission_check' is the function that checks permissions
|
||||
|
||||
As the non-established way of authentication an API permission is being done
|
||||
confimation that the permissions are still checked is required.
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse('API:_api_device_inventory')
|
||||
|
||||
client.force_login(self.add_user)
|
||||
response = client.post(url, data=self.inventory, content_type='application/json')
|
||||
|
||||
assert permission_check.called
|
||||
|
||||
|
||||
|
||||
@override_settings(CELERY_TASK_ALWAYS_EAGER=True,
|
||||
CELERY_TASK_EAGER_PROPOGATES=True)
|
||||
@patch.object(Inventory, '__init__')
|
||||
def test_inventory_serializer_inventory_called(self, serializer):
|
||||
""" Inventory Upload checks permissions
|
||||
|
||||
Function 'permission_check' is the function that checks permissions
|
||||
|
||||
As the non-established way of authentication an API permission is being done
|
||||
confimation that the permissions are still checked is required.
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse('API:_api_device_inventory')
|
||||
|
||||
client.force_login(self.add_user)
|
||||
response = client.post(url, data=self.inventory, content_type='application/json')
|
||||
|
||||
assert serializer.called
|
||||
|
||||
|
||||
|
||||
@override_settings(CELERY_TASK_ALWAYS_EAGER=True,
|
||||
CELERY_TASK_EAGER_PROPOGATES=True)
|
||||
@patch.object(Inventory.Details, '__init__')
|
||||
def test_inventory_serializer_inventory_details_called(self, serializer):
|
||||
""" Inventory Upload uses Inventory serializer
|
||||
|
||||
Details Serializer is called for inventory details dict.
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse('API:_api_device_inventory')
|
||||
|
||||
client.force_login(self.add_user)
|
||||
response = client.post(url, data=self.inventory, content_type='application/json')
|
||||
|
||||
assert serializer.called
|
||||
|
||||
|
||||
|
||||
@override_settings(CELERY_TASK_ALWAYS_EAGER=True,
|
||||
CELERY_TASK_EAGER_PROPOGATES=True)
|
||||
@patch.object(Inventory.OperatingSystem, '__init__')
|
||||
def test_inventory_serializer_inventory_operating_system_called(self, serializer):
|
||||
""" Inventory Upload uses Inventory serializer
|
||||
|
||||
Operating System Serializer is called for inventory Operating system dict.
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse('API:_api_device_inventory')
|
||||
|
||||
client.force_login(self.add_user)
|
||||
response = client.post(url, data=self.inventory, content_type='application/json')
|
||||
|
||||
assert serializer.called
|
||||
|
||||
|
||||
|
||||
@override_settings(CELERY_TASK_ALWAYS_EAGER=True,
|
||||
CELERY_TASK_EAGER_PROPOGATES=True)
|
||||
@patch.object(Inventory.Software, '__init__')
|
||||
def test_inventory_serializer_inventory_software_called(self, serializer):
|
||||
""" Inventory Upload uses Inventory serializer
|
||||
|
||||
Software Serializer is called for inventory software list.
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse('API:_api_device_inventory')
|
||||
|
||||
client.force_login(self.add_user)
|
||||
response = client.post(url, data=self.inventory, content_type='application/json')
|
||||
|
||||
assert serializer.called
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_device_added(self):
|
||||
""" Device is created """
|
||||
|
||||
assert self.device.name == self.inventory['details']['name']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_device_uuid_match(self):
|
||||
""" Device uuid match """
|
||||
|
||||
assert self.device.uuid == self.inventory['details']['uuid']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_device_serial_number_match(self):
|
||||
""" Device SN match """
|
||||
|
||||
assert self.device.serial_number == self.inventory['details']['serial_number']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_operating_system_added(self):
|
||||
""" Operating System is created """
|
||||
|
||||
assert self.operating_system.name == self.inventory['os']['name']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_operating_system_version_added(self):
|
||||
""" Operating System version is created """
|
||||
|
||||
assert self.operating_system_version.name == self.inventory['os']['version_major']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_device_has_operating_system_added(self):
|
||||
""" Operating System version linked to device """
|
||||
|
||||
assert self.device_operating_system.version == self.inventory['os']['version']
|
||||
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_api_inventory_device_operating_system_version_is_semver(self):
|
||||
""" Operating System version is full semver
|
||||
|
||||
Operating system versions name is the major version number of semver.
|
||||
The device version is to be full semver
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_api_inventory_software_no_version_cleaned(self):
|
||||
""" Check softare cleaned up
|
||||
|
||||
As part of the inventory upload the software versions of software found on the device is set to null
|
||||
and before the processing is completed, the version=null software is supposed to be cleaned up.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_software_category_added(self):
|
||||
""" Software category exists """
|
||||
|
||||
assert self.software_category.name == self.inventory['software'][0]['category']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_software_added(self):
|
||||
""" Test software exists """
|
||||
|
||||
assert self.software.name == self.inventory['software'][0]['name']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_software_category_linked_to_software(self):
|
||||
""" Software category linked to software """
|
||||
|
||||
assert self.software.category == self.software_category
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_software_version_added(self):
|
||||
""" Test software version exists """
|
||||
|
||||
assert self.software_version.name == self.inventory['software'][0]['version']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_software_version_returns_semver(self):
|
||||
""" Software Version from inventory returns semver if within version string """
|
||||
|
||||
assert self.software_version_is_semver.name == str(self.inventory['software'][2]['version']).split('-')[0]
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_software_version_returns_original_version(self):
|
||||
""" Software Version from inventory returns inventoried version if no semver found """
|
||||
|
||||
assert self.software_version_not_semver.name == self.inventory['software'][1]['version']
|
||||
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_software_version_linked_to_software(self):
|
||||
""" Test software version linked to software it belongs too """
|
||||
|
||||
assert self.software_version.software == self.software
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_device_has_software_version(self):
|
||||
""" Inventoried software is linked to device and it's the corret one"""
|
||||
|
||||
assert self.software_version.name == self.inventory['software'][0]['version']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_device_software_has_installed_date(self):
|
||||
""" Inventoried software version has install date """
|
||||
|
||||
assert self.device_software.installed is not None
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_device_software_installed_date_type(self):
|
||||
""" Inventoried software version has install date """
|
||||
|
||||
assert type(self.device_software.installed) is datetime.datetime
|
||||
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_api_inventory_device_software_blank_installed_date_is_updated(self):
|
||||
""" A blank installed date of software is updated if the software was already attached to the device """
|
||||
pass
|
||||
|
||||
|
||||
@override_settings(CELERY_TASK_ALWAYS_EAGER=True,
|
||||
CELERY_TASK_EAGER_PROPOGATES=True)
|
||||
def test_api_inventory_valid_status_ok_existing_device(self):
|
||||
""" Successful inventory upload returns 200 for existing device"""
|
||||
|
||||
client = Client()
|
||||
url = reverse('API:_api_device_inventory')
|
||||
|
||||
client.force_login(self.add_user)
|
||||
response = client.post(url, data=self.inventory, content_type='application/json')
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
@override_settings(CELERY_TASK_ALWAYS_EAGER=True,
|
||||
CELERY_TASK_EAGER_PROPOGATES=True)
|
||||
def test_api_inventory_invalid_status_bad_request(self):
|
||||
""" Incorrectly formated inventory upload returns 400 """
|
||||
|
||||
client = Client()
|
||||
url = reverse('API:_api_device_inventory')
|
||||
|
||||
mod_inventory = self.inventory.copy()
|
||||
|
||||
mod_inventory.update({
|
||||
'details': {
|
||||
'name': 'test_api_inventory_invalid_status_bad_request'
|
||||
},
|
||||
'software': {
|
||||
'not_within_a': 'list'
|
||||
}
|
||||
})
|
||||
|
||||
client.force_login(self.add_user)
|
||||
response = client.post(url, data=mod_inventory, content_type='application/json')
|
||||
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_api_inventory_exeception_status_sever_error(self):
|
||||
""" if the method throws an exception 500 must be returned.
|
||||
|
||||
idea to test: add a random key to the report that is not documented
|
||||
and perform some action against it that will cause a python exception.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class InventoryAPIDifferentNameSerialNumberMatch(TestCase):
|
||||
""" Test inventory upload with different name
|
||||
|
||||
should match by serial number
|
||||
"""
|
||||
|
||||
model = Device
|
||||
|
||||
model_name = 'device'
|
||||
app_label = 'itam'
|
||||
|
||||
inventory = {
|
||||
"details": {
|
||||
"name": "device_name",
|
||||
"serial_number": "serial_number_123",
|
||||
"uuid": "string"
|
||||
},
|
||||
"os": {
|
||||
"name": "os_name",
|
||||
"version_major": "12",
|
||||
"version": "12.1"
|
||||
},
|
||||
"software": [
|
||||
{
|
||||
"name": "software_name",
|
||||
"category": "category_name",
|
||||
"version": "1.2.3"
|
||||
},
|
||||
{
|
||||
"name": "software_name_not_semver",
|
||||
"category": "category_name",
|
||||
"version": "2024.4"
|
||||
},
|
||||
{
|
||||
"name": "software_name_semver_contained",
|
||||
"category": "category_name",
|
||||
"version": "1.2.3-rc1"
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
"""Setup Test
|
||||
|
||||
1. Create an organization for user
|
||||
2. Create a team for user with correct permissions
|
||||
3. add user to the teeam
|
||||
4. upload the inventory
|
||||
5. conduct queries for tests
|
||||
"""
|
||||
|
||||
organization = Organization.objects.create(name='test_org')
|
||||
|
||||
self.organization = organization
|
||||
|
||||
Device.objects.create(
|
||||
name='random device name',
|
||||
serial_number='serial_number_123'
|
||||
)
|
||||
|
||||
add_permissions = Permission.objects.get(
|
||||
codename = 'add_' + self.model_name,
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = self.app_label,
|
||||
model = self.model_name,
|
||||
)
|
||||
)
|
||||
|
||||
add_team = Team.objects.create(
|
||||
team_name = 'add_team',
|
||||
organization = organization,
|
||||
)
|
||||
|
||||
add_team.permissions.set([add_permissions])
|
||||
|
||||
self.add_user = User.objects.create_user(username="test_user_add", password="password")
|
||||
|
||||
add_user_settings = UserSettings.objects.get(user=self.add_user)
|
||||
|
||||
add_user_settings.default_organization = organization
|
||||
|
||||
add_user_settings.save()
|
||||
|
||||
teamuser = TeamUsers.objects.create(
|
||||
team = add_team,
|
||||
user = self.add_user
|
||||
)
|
||||
|
||||
# upload the inventory
|
||||
process_inventory(json.dumps(self.inventory), organization.id)
|
||||
|
||||
|
||||
self.device = Device.objects.get(name=self.inventory['details']['name'])
|
||||
|
||||
self.operating_system = OperatingSystem.objects.get(name=self.inventory['os']['name'])
|
||||
|
||||
self.operating_system_version = OperatingSystemVersion.objects.get(name=self.inventory['os']['version_major'])
|
||||
|
||||
self.device_operating_system = DeviceOperatingSystem.objects.get(version=self.inventory['os']['version'])
|
||||
|
||||
self.software = Software.objects.get(name=self.inventory['software'][0]['name'])
|
||||
|
||||
self.software_category = SoftwareCategory.objects.get(name=self.inventory['software'][0]['category'])
|
||||
|
||||
self.software_version = SoftwareVersion.objects.get(
|
||||
name = self.inventory['software'][0]['version'],
|
||||
software = self.software,
|
||||
)
|
||||
|
||||
self.software_not_semver = Software.objects.get(name=self.inventory['software'][1]['name'])
|
||||
|
||||
self.software_version_not_semver = SoftwareVersion.objects.get(
|
||||
name = self.inventory['software'][1]['version'],
|
||||
software = self.software_not_semver
|
||||
)
|
||||
|
||||
self.software_is_semver = Software.objects.get(name=self.inventory['software'][2]['name'])
|
||||
|
||||
self.software_version_is_semver = SoftwareVersion.objects.get(
|
||||
software = self.software_is_semver
|
||||
)
|
||||
|
||||
self.device_software = DeviceSoftware.objects.get(device=self.device,software=self.software)
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_device_added(self):
|
||||
""" Device is created """
|
||||
|
||||
assert self.device.name == self.inventory['details']['name']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_device_uuid_match(self):
|
||||
""" Device uuid match """
|
||||
|
||||
assert self.device.uuid == self.inventory['details']['uuid']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_device_serial_number_match(self):
|
||||
""" Device SN match """
|
||||
|
||||
assert self.device.serial_number == self.inventory['details']['serial_number']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_operating_system_added(self):
|
||||
""" Operating System is created """
|
||||
|
||||
assert self.operating_system.name == self.inventory['os']['name']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_operating_system_version_added(self):
|
||||
""" Operating System version is created """
|
||||
|
||||
assert self.operating_system_version.name == self.inventory['os']['version_major']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_device_has_operating_system_added(self):
|
||||
""" Operating System version linked to device """
|
||||
|
||||
assert self.device_operating_system.version == self.inventory['os']['version']
|
||||
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_api_inventory_device_operating_system_version_is_semver(self):
|
||||
""" Operating System version is full semver
|
||||
|
||||
Operating system versions name is the major version number of semver.
|
||||
The device version is to be full semver
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_api_inventory_software_no_version_cleaned(self):
|
||||
""" Check softare cleaned up
|
||||
|
||||
As part of the inventory upload the software versions of software found on the device is set to null
|
||||
and before the processing is completed, the version=null software is supposed to be cleaned up.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_software_category_added(self):
|
||||
""" Software category exists """
|
||||
|
||||
assert self.software_category.name == self.inventory['software'][0]['category']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_software_added(self):
|
||||
""" Test software exists """
|
||||
|
||||
assert self.software.name == self.inventory['software'][0]['name']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_software_category_linked_to_software(self):
|
||||
""" Software category linked to software """
|
||||
|
||||
assert self.software.category == self.software_category
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_software_version_added(self):
|
||||
""" Test software version exists """
|
||||
|
||||
assert self.software_version.name == self.inventory['software'][0]['version']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_software_version_returns_semver(self):
|
||||
""" Software Version from inventory returns semver if within version string """
|
||||
|
||||
assert self.software_version_is_semver.name == str(self.inventory['software'][2]['version']).split('-')[0]
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_software_version_returns_original_version(self):
|
||||
""" Software Version from inventory returns inventoried version if no semver found """
|
||||
|
||||
assert self.software_version_not_semver.name == self.inventory['software'][1]['version']
|
||||
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_software_version_linked_to_software(self):
|
||||
""" Test software version linked to software it belongs too """
|
||||
|
||||
assert self.software_version.software == self.software
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_device_has_software_version(self):
|
||||
""" Inventoried software is linked to device and it's the corret one"""
|
||||
|
||||
assert self.software_version.name == self.inventory['software'][0]['version']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_device_software_has_installed_date(self):
|
||||
""" Inventoried software version has install date """
|
||||
|
||||
assert self.device_software.installed is not None
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_device_software_installed_date_type(self):
|
||||
""" Inventoried software version has install date """
|
||||
|
||||
assert type(self.device_software.installed) is datetime.datetime
|
||||
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_api_inventory_device_software_blank_installed_date_is_updated(self):
|
||||
""" A blank installed date of software is updated if the software was already attached to the device """
|
||||
pass
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class InventoryAPIDifferentNameUUIDMatch(TestCase):
|
||||
""" Test inventory upload with different name
|
||||
|
||||
should match by uuid
|
||||
"""
|
||||
|
||||
model = Device
|
||||
|
||||
model_name = 'device'
|
||||
app_label = 'itam'
|
||||
|
||||
inventory = {
|
||||
"details": {
|
||||
"name": "device_name",
|
||||
"serial_number": "serial_number_123",
|
||||
"uuid": "123-456-789"
|
||||
},
|
||||
"os": {
|
||||
"name": "os_name",
|
||||
"version_major": "12",
|
||||
"version": "12.1"
|
||||
},
|
||||
"software": [
|
||||
{
|
||||
"name": "software_name",
|
||||
"category": "category_name",
|
||||
"version": "1.2.3"
|
||||
},
|
||||
{
|
||||
"name": "software_name_not_semver",
|
||||
"category": "category_name",
|
||||
"version": "2024.4"
|
||||
},
|
||||
{
|
||||
"name": "software_name_semver_contained",
|
||||
"category": "category_name",
|
||||
"version": "1.2.3-rc1"
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
"""Setup Test
|
||||
|
||||
1. Create an organization for user
|
||||
2. Create a team for user with correct permissions
|
||||
3. add user to the teeam
|
||||
4. upload the inventory
|
||||
5. conduct queries for tests
|
||||
"""
|
||||
|
||||
organization = Organization.objects.create(name='test_org')
|
||||
|
||||
self.organization = organization
|
||||
|
||||
Device.objects.create(
|
||||
name='random device name',
|
||||
uuid='123-456-789'
|
||||
)
|
||||
|
||||
add_permissions = Permission.objects.get(
|
||||
codename = 'add_' + self.model_name,
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = self.app_label,
|
||||
model = self.model_name,
|
||||
)
|
||||
)
|
||||
|
||||
add_team = Team.objects.create(
|
||||
team_name = 'add_team',
|
||||
organization = organization,
|
||||
)
|
||||
|
||||
add_team.permissions.set([add_permissions])
|
||||
|
||||
self.add_user = User.objects.create_user(username="test_user_add", password="password")
|
||||
|
||||
add_user_settings = UserSettings.objects.get(user=self.add_user)
|
||||
|
||||
add_user_settings.default_organization = organization
|
||||
|
||||
add_user_settings.save()
|
||||
|
||||
teamuser = TeamUsers.objects.create(
|
||||
team = add_team,
|
||||
user = self.add_user
|
||||
)
|
||||
|
||||
# upload the inventory
|
||||
process_inventory(json.dumps(self.inventory), organization.id)
|
||||
|
||||
|
||||
self.device = Device.objects.get(name=self.inventory['details']['name'])
|
||||
|
||||
self.operating_system = OperatingSystem.objects.get(name=self.inventory['os']['name'])
|
||||
|
||||
self.operating_system_version = OperatingSystemVersion.objects.get(name=self.inventory['os']['version_major'])
|
||||
|
||||
self.device_operating_system = DeviceOperatingSystem.objects.get(version=self.inventory['os']['version'])
|
||||
|
||||
self.software = Software.objects.get(name=self.inventory['software'][0]['name'])
|
||||
|
||||
self.software_category = SoftwareCategory.objects.get(name=self.inventory['software'][0]['category'])
|
||||
|
||||
self.software_version = SoftwareVersion.objects.get(
|
||||
name = self.inventory['software'][0]['version'],
|
||||
software = self.software,
|
||||
)
|
||||
|
||||
self.software_not_semver = Software.objects.get(name=self.inventory['software'][1]['name'])
|
||||
|
||||
self.software_version_not_semver = SoftwareVersion.objects.get(
|
||||
name = self.inventory['software'][1]['version'],
|
||||
software = self.software_not_semver
|
||||
)
|
||||
|
||||
self.software_is_semver = Software.objects.get(name=self.inventory['software'][2]['name'])
|
||||
|
||||
self.software_version_is_semver = SoftwareVersion.objects.get(
|
||||
software = self.software_is_semver
|
||||
)
|
||||
|
||||
self.device_software = DeviceSoftware.objects.get(device=self.device,software=self.software)
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_device_added(self):
|
||||
""" Device is created """
|
||||
|
||||
assert self.device.name == self.inventory['details']['name']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_device_uuid_match(self):
|
||||
""" Device uuid match """
|
||||
|
||||
assert self.device.uuid == self.inventory['details']['uuid']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_device_serial_number_match(self):
|
||||
""" Device SN match """
|
||||
|
||||
assert self.device.serial_number == self.inventory['details']['serial_number']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_operating_system_added(self):
|
||||
""" Operating System is created """
|
||||
|
||||
assert self.operating_system.name == self.inventory['os']['name']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_operating_system_version_added(self):
|
||||
""" Operating System version is created """
|
||||
|
||||
assert self.operating_system_version.name == self.inventory['os']['version_major']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_device_has_operating_system_added(self):
|
||||
""" Operating System version linked to device """
|
||||
|
||||
assert self.device_operating_system.version == self.inventory['os']['version']
|
||||
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_api_inventory_device_operating_system_version_is_semver(self):
|
||||
""" Operating System version is full semver
|
||||
|
||||
Operating system versions name is the major version number of semver.
|
||||
The device version is to be full semver
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_api_inventory_software_no_version_cleaned(self):
|
||||
""" Check softare cleaned up
|
||||
|
||||
As part of the inventory upload the software versions of software found on the device is set to null
|
||||
and before the processing is completed, the version=null software is supposed to be cleaned up.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_software_category_added(self):
|
||||
""" Software category exists """
|
||||
|
||||
assert self.software_category.name == self.inventory['software'][0]['category']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_software_added(self):
|
||||
""" Test software exists """
|
||||
|
||||
assert self.software.name == self.inventory['software'][0]['name']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_software_category_linked_to_software(self):
|
||||
""" Software category linked to software """
|
||||
|
||||
assert self.software.category == self.software_category
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_software_version_added(self):
|
||||
""" Test software version exists """
|
||||
|
||||
assert self.software_version.name == self.inventory['software'][0]['version']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_software_version_returns_semver(self):
|
||||
""" Software Version from inventory returns semver if within version string """
|
||||
|
||||
assert self.software_version_is_semver.name == str(self.inventory['software'][2]['version']).split('-')[0]
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_software_version_returns_original_version(self):
|
||||
""" Software Version from inventory returns inventoried version if no semver found """
|
||||
|
||||
assert self.software_version_not_semver.name == self.inventory['software'][1]['version']
|
||||
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_software_version_linked_to_software(self):
|
||||
""" Test software version linked to software it belongs too """
|
||||
|
||||
assert self.software_version.software == self.software
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_device_has_software_version(self):
|
||||
""" Inventoried software is linked to device and it's the corret one"""
|
||||
|
||||
assert self.software_version.name == self.inventory['software'][0]['version']
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_device_software_has_installed_date(self):
|
||||
""" Inventoried software version has install date """
|
||||
|
||||
assert self.device_software.installed is not None
|
||||
|
||||
|
||||
|
||||
def test_api_inventory_device_software_installed_date_type(self):
|
||||
""" Inventoried software version has install date """
|
||||
|
||||
assert type(self.device_software.installed) is datetime.datetime
|
||||
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_api_inventory_device_software_blank_installed_date_is_updated(self):
|
||||
""" A blank installed date of software is updated if the software was already attached to the device """
|
||||
pass
|
||||
|
283
app/api/tests/unit/inventory/test_inventory_permission_api.py
Normal file
283
app/api/tests/unit/inventory/test_inventory_permission_api.py
Normal file
@ -0,0 +1,283 @@
|
||||
import celery
|
||||
import pytest
|
||||
import unittest
|
||||
import requests
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import AnonymousUser, User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.shortcuts import reverse
|
||||
from django.test import TestCase, Client
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from access.models import Organization, Team, TeamUsers, Permission
|
||||
|
||||
from itam.models.device import Device
|
||||
|
||||
from settings.models.user_settings import UserSettings
|
||||
|
||||
class InventoryPermissionsAPI(TestCase):
|
||||
|
||||
model = Device
|
||||
|
||||
model_name = 'device'
|
||||
app_label = 'itam'
|
||||
|
||||
inventory = {
|
||||
"details": {
|
||||
"name": "device_name",
|
||||
"serial_number": "a serial number",
|
||||
"uuid": "string"
|
||||
},
|
||||
"os": {
|
||||
"name": "os_name",
|
||||
"version_major": "12",
|
||||
"version": "12.1"
|
||||
},
|
||||
"software": [
|
||||
{
|
||||
"name": "software_name",
|
||||
"category": "category_name",
|
||||
"version": "1.2.3"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
"""Setup Test
|
||||
|
||||
1. Create an organization for user and item
|
||||
. create an organization that is different to item
|
||||
2. Create a device
|
||||
3. create teams with each permission: view, add, change, delete
|
||||
4. create a user per team
|
||||
"""
|
||||
|
||||
organization = Organization.objects.create(name='test_org')
|
||||
|
||||
self.organization = organization
|
||||
|
||||
different_organization = Organization.objects.create(name='test_different_organization')
|
||||
|
||||
|
||||
# self.item = self.model.objects.create(
|
||||
# organization=organization,
|
||||
# name = 'deviceone'
|
||||
# )
|
||||
|
||||
view_permissions = Permission.objects.get(
|
||||
codename = 'view_' + self.model_name,
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = self.app_label,
|
||||
model = self.model_name,
|
||||
)
|
||||
)
|
||||
|
||||
view_team = Team.objects.create(
|
||||
team_name = 'view_team',
|
||||
organization = organization,
|
||||
)
|
||||
|
||||
view_team.permissions.set([view_permissions])
|
||||
|
||||
|
||||
|
||||
add_permissions = Permission.objects.get(
|
||||
codename = 'add_' + self.model_name,
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = self.app_label,
|
||||
model = self.model_name,
|
||||
)
|
||||
)
|
||||
|
||||
add_team = Team.objects.create(
|
||||
team_name = 'add_team',
|
||||
organization = organization,
|
||||
)
|
||||
|
||||
add_team.permissions.set([add_permissions])
|
||||
|
||||
|
||||
|
||||
change_permissions = Permission.objects.get(
|
||||
codename = 'change_' + self.model_name,
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = self.app_label,
|
||||
model = self.model_name,
|
||||
)
|
||||
)
|
||||
|
||||
change_team = Team.objects.create(
|
||||
team_name = 'change_team',
|
||||
organization = organization,
|
||||
)
|
||||
|
||||
change_team.permissions.set([change_permissions])
|
||||
|
||||
|
||||
|
||||
delete_permissions = Permission.objects.get(
|
||||
codename = 'delete_' + self.model_name,
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = self.app_label,
|
||||
model = self.model_name,
|
||||
)
|
||||
)
|
||||
|
||||
delete_team = Team.objects.create(
|
||||
team_name = 'delete_team',
|
||||
organization = organization,
|
||||
)
|
||||
|
||||
delete_team.permissions.set([delete_permissions])
|
||||
|
||||
|
||||
self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password")
|
||||
|
||||
|
||||
self.view_user = User.objects.create_user(username="test_user_view", password="password")
|
||||
teamuser = TeamUsers.objects.create(
|
||||
team = view_team,
|
||||
user = self.view_user
|
||||
)
|
||||
|
||||
self.add_user = User.objects.create_user(username="test_user_add", password="password")
|
||||
|
||||
add_user_settings = UserSettings.objects.get(user=self.add_user)
|
||||
|
||||
add_user_settings.default_organization = organization
|
||||
|
||||
add_user_settings.save()
|
||||
|
||||
teamuser = TeamUsers.objects.create(
|
||||
team = add_team,
|
||||
user = self.add_user
|
||||
)
|
||||
|
||||
self.change_user = User.objects.create_user(username="test_user_change", password="password")
|
||||
teamuser = TeamUsers.objects.create(
|
||||
team = change_team,
|
||||
user = self.change_user
|
||||
)
|
||||
|
||||
self.delete_user = User.objects.create_user(username="test_user_delete", password="password")
|
||||
teamuser = TeamUsers.objects.create(
|
||||
team = delete_team,
|
||||
user = self.delete_user
|
||||
)
|
||||
|
||||
|
||||
self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password")
|
||||
|
||||
|
||||
different_organization_team = Team.objects.create(
|
||||
team_name = 'different_organization_team',
|
||||
organization = different_organization,
|
||||
)
|
||||
|
||||
different_organization_team.permissions.set([
|
||||
view_permissions,
|
||||
add_permissions,
|
||||
change_permissions,
|
||||
delete_permissions,
|
||||
])
|
||||
|
||||
TeamUsers.objects.create(
|
||||
team = different_organization_team,
|
||||
user = self.different_organization_user
|
||||
)
|
||||
|
||||
|
||||
|
||||
@override_settings(CELERY_TASK_ALWAYS_EAGER=True,
|
||||
CELERY_TASK_EAGER_PROPOGATES=True)
|
||||
def test_device_auth_add_user_anon_denied(self):
|
||||
""" Check correct permission for add
|
||||
|
||||
Attempt to add as anon user
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse('API:_api_device_inventory')
|
||||
|
||||
|
||||
response = client.put(url, data=self.inventory, content_type='application/json')
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
@override_settings(CELERY_TASK_ALWAYS_EAGER=True,
|
||||
CELERY_TASK_EAGER_PROPOGATES=True)
|
||||
def test_device_auth_add_no_permission_denied(self):
|
||||
""" Check correct permission for add
|
||||
|
||||
Attempt to add as user with no permissions
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse('API:_api_device_inventory')
|
||||
|
||||
|
||||
client.force_login(self.no_permissions_user)
|
||||
response = client.post(url, data=self.inventory, content_type='application/json')
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
@override_settings(CELERY_TASK_ALWAYS_EAGER=True,
|
||||
CELERY_TASK_EAGER_PROPOGATES=True)
|
||||
def test_device_auth_add_different_organization_denied(self):
|
||||
""" Check correct permission for add
|
||||
|
||||
attempt to add as user from different organization
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse('API:_api_device_inventory')
|
||||
|
||||
|
||||
client.force_login(self.different_organization_user)
|
||||
response = client.post(url, data=self.inventory, content_type='application/json')
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
@override_settings(CELERY_TASK_ALWAYS_EAGER=True,
|
||||
CELERY_TASK_EAGER_PROPOGATES=True)
|
||||
def test_device_auth_add_permission_view_denied(self):
|
||||
""" Check correct permission for add
|
||||
|
||||
Attempt to add a user with view permission
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse('API:_api_device_inventory')
|
||||
|
||||
|
||||
client.force_login(self.view_user)
|
||||
response = client.post(url, data=self.inventory, content_type='application/json')
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
@override_settings(CELERY_TASK_ALWAYS_EAGER=True,
|
||||
CELERY_TASK_EAGER_PROPOGATES=True)
|
||||
def test_device_auth_add_has_permission(self):
|
||||
""" Check correct permission for add
|
||||
|
||||
Attempt to add as user with no permission
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse('API:_api_device_inventory')
|
||||
|
||||
|
||||
client.force_login(self.add_user)
|
||||
response = client.post(url, data=self.inventory, content_type='application/json')
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
|
35
app/api/tests/unit/test_api_access.py
Normal file
35
app/api/tests/unit/test_api_access.py
Normal file
@ -0,0 +1,35 @@
|
||||
from django.shortcuts import reverse
|
||||
from django.test import TestCase, Client
|
||||
|
||||
import pytest
|
||||
import unittest
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_api_access_auth_required(user):
|
||||
"""Ensure that no api access has been granted
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_api_access_home(user):
|
||||
"""Ensure api home view visible once logged in
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
326
app/api/tests/unit/token/test_token.py
Normal file
326
app/api/tests/unit/token/test_token.py
Normal file
@ -0,0 +1,326 @@
|
||||
import hashlib
|
||||
import json
|
||||
import pytest
|
||||
import requests
|
||||
import unittest
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from django.contrib.auth.models import AnonymousUser, User
|
||||
from django.shortcuts import reverse
|
||||
from django.test import TestCase, Client
|
||||
|
||||
from access.models import Organization, Team, TeamUsers, Permission
|
||||
|
||||
from api.models.tokens import AuthToken
|
||||
|
||||
from settings.models.user_settings import UserSettings
|
||||
|
||||
class APIAuthToken(TestCase):
|
||||
|
||||
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
"""Setup Test
|
||||
|
||||
1. Create an organization for user
|
||||
3. create user
|
||||
4. create user settings
|
||||
5. create API key (valid)
|
||||
6. generate an API key that does not exist
|
||||
5. create API key (expired)
|
||||
"""
|
||||
|
||||
organization = Organization.objects.create(name='test_org')
|
||||
|
||||
self.organization = organization
|
||||
|
||||
self.add_user = User.objects.create_user(username="test_user_add", password="password")
|
||||
|
||||
add_user_settings = UserSettings.objects.get(user=self.add_user)
|
||||
|
||||
add_user_settings.default_organization = organization
|
||||
|
||||
add_user_settings.save()
|
||||
|
||||
expires = datetime.utcnow() + timedelta(days = 10)
|
||||
|
||||
expires = expires.strftime('%Y-%m-%d %H:%M:%S%z')
|
||||
|
||||
token = AuthToken.objects.create(
|
||||
user = self.add_user,
|
||||
expires=expires
|
||||
)
|
||||
|
||||
self.api_token_valid = token.generate()
|
||||
self.hashed_token = token.token_hash(self.api_token_valid)
|
||||
token.token = self.hashed_token
|
||||
|
||||
token.save()
|
||||
|
||||
self.api_token_does_not_exist = hashlib.sha256(str('a random string').encode('utf-8')).hexdigest()
|
||||
|
||||
|
||||
expires = datetime.utcnow() + timedelta(days = -10)
|
||||
|
||||
expires = expires.strftime('%Y-%m-%d %H:%M:%S%z')
|
||||
|
||||
|
||||
self.api_token_expired = token.generate()
|
||||
|
||||
self.hashed_token_expired = token.token_hash(self.api_token_expired)
|
||||
|
||||
token = AuthToken.objects.create(
|
||||
user = self.add_user,
|
||||
expires=expires,
|
||||
token = self.hashed_token_expired
|
||||
)
|
||||
|
||||
|
||||
|
||||
def test_token_create_own(self):
|
||||
""" Check correct permission for add
|
||||
|
||||
User can only create token for self.
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
client.force_login(self.add_user)
|
||||
url = reverse('_user_auth_token_add', kwargs={'user_id': self.add_user.id})
|
||||
|
||||
|
||||
response = client.post(url, kwargs={'user_id': self.add_user.id})
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
|
||||
def test_token_create_other_user(self):
|
||||
""" Check correct permission for add
|
||||
|
||||
User can not create token for another user.
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
client.force_login(self.add_user)
|
||||
url = reverse('_user_auth_token_add', kwargs={'user_id': 999})
|
||||
|
||||
|
||||
response = client.post(url, kwargs={'user_id': 999})
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
|
||||
def test_token_delete_own(self):
|
||||
""" Check correct permission for delete
|
||||
|
||||
User can only delete token for self.
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
client.force_login(self.add_user)
|
||||
url = reverse('_user_auth_token_delete', kwargs={'user_id': self.add_user.id, 'pk': 1})
|
||||
|
||||
|
||||
response = client.post(url, kwargs={'user_id': self.add_user.id, 'pk': 1})
|
||||
|
||||
assert response.status_code == 302 and response.url == '/account/settings/1'
|
||||
|
||||
|
||||
|
||||
def test_token_delete_other_user(self):
|
||||
""" Check correct permission for delete
|
||||
|
||||
User can not delete another users token.
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
client.force_login(self.add_user)
|
||||
url = reverse('_user_auth_token_delete', kwargs={'user_id': 999, 'pk': 1})
|
||||
|
||||
|
||||
response = client.post(url, data={'id': 1}, kwargs={'user_id': 999, 'pk': 1})
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
|
||||
def test_auth_invalid_token(self):
|
||||
""" Check token authentication
|
||||
|
||||
Invalid token does not allow login
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse('home') + 'api/'
|
||||
|
||||
|
||||
response = client.get(
|
||||
url,
|
||||
content_type='application/json',
|
||||
headers = {
|
||||
'Accept': 'application/json',
|
||||
'Authorization': 'Token ' + self.api_token_does_not_exist,
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
|
||||
def test_auth_no_token(self):
|
||||
""" Check token authentication
|
||||
|
||||
providing no token does not allow login
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse('home') + 'api/'
|
||||
|
||||
|
||||
response = client.get(
|
||||
url,
|
||||
content_type='application/json',
|
||||
headers = {
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
|
||||
def test_auth_expired_token(self):
|
||||
""" Check token authentication
|
||||
|
||||
expired token does not allow login
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse('home') + 'api/'
|
||||
|
||||
|
||||
response = client.get(
|
||||
url,
|
||||
content_type='application/json',
|
||||
headers = {
|
||||
'Accept': 'application/json',
|
||||
'Authorization': 'Token ' + self.api_token_expired,
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
|
||||
def test_auth_valid_token(self):
|
||||
""" Check token authentication
|
||||
|
||||
Valid token allows login
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse('home') + 'api/'
|
||||
|
||||
|
||||
response = client.get(
|
||||
url,
|
||||
content_type='application/json',
|
||||
headers = {
|
||||
'Accept': 'application/json',
|
||||
'Authorization': 'Token ' + self.api_token_valid,
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
|
||||
def test_feat_expired_token_is_removed(self):
|
||||
""" token feature confirmation
|
||||
|
||||
expired token is deleted
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse('home') + 'api/'
|
||||
|
||||
|
||||
response = client.get(
|
||||
url,
|
||||
content_type='application/json',
|
||||
headers = {
|
||||
'Accept': 'application/json',
|
||||
'Authorization': 'Token ' + self.api_token_expired,
|
||||
}
|
||||
)
|
||||
|
||||
db_query = AuthToken.objects.filter(
|
||||
token = self.hashed_token_expired
|
||||
)
|
||||
|
||||
assert not db_query.exists()
|
||||
|
||||
|
||||
|
||||
def test_token_not_saved_to_db(self):
|
||||
""" confirm generated token not saved to the database """
|
||||
|
||||
db_query = AuthToken.objects.filter(
|
||||
token = self.api_token_valid
|
||||
)
|
||||
|
||||
assert not db_query.exists()
|
||||
|
||||
|
||||
|
||||
def test_header_format_invalid_token(self):
|
||||
""" token header format check
|
||||
|
||||
header missing 'Token' prefix reports invalid
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse('home') + 'api/'
|
||||
|
||||
|
||||
response = client.get(
|
||||
url,
|
||||
content_type='application/json',
|
||||
headers = {
|
||||
'Accept': 'application/json',
|
||||
'Authorization': '' + self.api_token_valid,
|
||||
}
|
||||
)
|
||||
|
||||
content: dict = json.loads(response.content.decode('utf-8'))
|
||||
|
||||
assert response.status_code == 401 and content['detail'] == 'Token header invalid'
|
||||
|
||||
|
||||
|
||||
def test_header_format_invalid_token_spaces(self):
|
||||
""" token header format check
|
||||
|
||||
auth header with extra spaces reports invalid
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse('home') + 'api/'
|
||||
|
||||
|
||||
response = client.get(
|
||||
url,
|
||||
content_type='application/json',
|
||||
headers = {
|
||||
'Accept': 'application/json',
|
||||
'Authorization': 'Token A space ' + self.api_token_valid,
|
||||
}
|
||||
)
|
||||
|
||||
content: dict = json.loads(response.content.decode('utf-8'))
|
||||
|
||||
assert response.status_code == 401 and content['detail'] == 'Token header invalid. Possibly incorrectly formatted'
|
||||
|
43
app/api/urls.py
Normal file
43
app/api/urls.py
Normal file
@ -0,0 +1,43 @@
|
||||
from django.urls import path
|
||||
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from rest_framework.urlpatterns import format_suffix_patterns
|
||||
|
||||
from .views import access, config, index
|
||||
|
||||
from .views.itam import software, config as itam_config
|
||||
from .views.itam.device import DeviceViewSet
|
||||
from .views.itam import inventory
|
||||
|
||||
|
||||
app_name = "API"
|
||||
|
||||
|
||||
router = DefaultRouter()
|
||||
|
||||
router.register('', index.Index, basename='_api_home')
|
||||
router.register('device', DeviceViewSet, basename='device')
|
||||
router.register('software', software.SoftwareViewSet, basename='software')
|
||||
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path("config/<slug:slug>/", itam_config.View.as_view(), name="_api_device_config"),
|
||||
|
||||
path("configuration/", config.ConfigGroupsList.as_view(), name='_api_config_groups'),
|
||||
path("configuration/<int:pk>", config.ConfigGroupsDetail.as_view(), name='_api_config_group'),
|
||||
|
||||
path("device/inventory", inventory.Collect.as_view(), name="_api_device_inventory"),
|
||||
|
||||
path("organization/", access.OrganizationList.as_view(), name='_api_orgs'),
|
||||
path("organization/<int:pk>/", access.OrganizationDetail.as_view(), name='_api_organization'),
|
||||
path("organization/<int:organization_id>/team", access.TeamList.as_view(), name='_api_organization_teams'),
|
||||
path("organization/<int:organization_id>/team/<int:group_ptr_id>/", access.TeamDetail.as_view(), name='_api_team'),
|
||||
path("organization/<int:organization_id>/team/<int:group_ptr_id>/permissions", access.TeamPermissionDetail.as_view(), name='_api_team_permission'),
|
||||
path("organization/team/", access.TeamList.as_view(), name='_api_teams'),
|
||||
|
||||
]
|
||||
|
||||
urlpatterns = format_suffix_patterns(urlpatterns)
|
||||
|
||||
urlpatterns += router.urls
|
317
app/api/views/access.py
Normal file
317
app/api/views/access.py
Normal file
@ -0,0 +1,317 @@
|
||||
from django.contrib.auth.models import Permission
|
||||
|
||||
from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse
|
||||
|
||||
from rest_framework import generics, routers, serializers, views
|
||||
from rest_framework.permissions import DjangoObjectPermissions
|
||||
from rest_framework.response import Response
|
||||
|
||||
from access.mixin import OrganizationMixin
|
||||
from access.models import Organization, Team
|
||||
|
||||
from api.serializers.access import OrganizationSerializer, OrganizationListSerializer, TeamSerializer, TeamPermissionSerializer
|
||||
from api.views.mixin import OrganizationPermissionAPI
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
get=extend_schema(
|
||||
summary = "Fetch Organizations",
|
||||
description="Returns a list of organizations."
|
||||
),
|
||||
)
|
||||
class OrganizationList(generics.ListAPIView):
|
||||
|
||||
permission_classes = [
|
||||
OrganizationPermissionAPI
|
||||
]
|
||||
|
||||
queryset = Organization.objects.all()
|
||||
lookup_field = 'pk'
|
||||
serializer_class = OrganizationListSerializer
|
||||
|
||||
|
||||
def get_view_name(self):
|
||||
return "Organizations"
|
||||
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
get=extend_schema(
|
||||
summary = "Get An Organization",
|
||||
),
|
||||
patch=extend_schema(
|
||||
summary = "Update an organization",
|
||||
),
|
||||
put=extend_schema(
|
||||
summary = "Update an organization",
|
||||
),
|
||||
)
|
||||
class OrganizationDetail(generics.RetrieveUpdateAPIView):
|
||||
|
||||
permission_classes = [
|
||||
OrganizationPermissionAPI
|
||||
]
|
||||
|
||||
queryset = Organization.objects.all()
|
||||
lookup_field = 'pk'
|
||||
serializer_class = OrganizationSerializer
|
||||
|
||||
|
||||
def get_view_name(self):
|
||||
return "Organization"
|
||||
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
post=extend_schema(
|
||||
summary = "Create a Team",
|
||||
description = """Create a team within the defined organization.""",
|
||||
tags = ['team',],
|
||||
request = TeamSerializer,
|
||||
responses = {
|
||||
200: OpenApiResponse(description='Team has been updated with the supplied permissions'),
|
||||
401: OpenApiResponse(description='User Not logged in'),
|
||||
403: OpenApiResponse(description='User is missing permission or in different organization'),
|
||||
}
|
||||
),
|
||||
create=extend_schema(exclude=True),
|
||||
)
|
||||
class TeamList(generics.ListCreateAPIView):
|
||||
|
||||
permission_classes = [
|
||||
OrganizationPermissionAPI
|
||||
]
|
||||
|
||||
queryset = Team.objects.all()
|
||||
serializer_class = TeamSerializer
|
||||
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
self.queryset = Team.objects.filter(organization=self.kwargs['organization_id'])
|
||||
|
||||
return self.queryset
|
||||
|
||||
|
||||
def get_view_name(self):
|
||||
return "Organization Teams"
|
||||
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
get=extend_schema(
|
||||
summary = "Fetch a Team",
|
||||
description = """Fetch a team within the defined organization.
|
||||
""",
|
||||
methods=["GET"],
|
||||
tags = ['team',],
|
||||
request = TeamSerializer,
|
||||
responses = {
|
||||
200: OpenApiResponse(description='Team has been updated with the supplied permissions'),
|
||||
401: OpenApiResponse(description='User Not logged in'),
|
||||
403: OpenApiResponse(description='User is missing permission or in different organization'),
|
||||
}
|
||||
),
|
||||
patch=extend_schema(
|
||||
summary = "Update a Team",
|
||||
description = """Update a team within the defined organization.
|
||||
""",
|
||||
methods=["Patch"],
|
||||
tags = ['team',],
|
||||
request = TeamSerializer,
|
||||
responses = {
|
||||
200: OpenApiResponse(description='Team has been updated with the supplied permissions'),
|
||||
401: OpenApiResponse(description='User Not logged in'),
|
||||
403: OpenApiResponse(description='User is missing permission or in different organization'),
|
||||
}
|
||||
),
|
||||
put = extend_schema(
|
||||
summary = "Amend a team",
|
||||
tags = ['team',],
|
||||
),
|
||||
delete=extend_schema(
|
||||
summary = "Delete a Team",
|
||||
tags = ['team',],
|
||||
),
|
||||
post = extend_schema(
|
||||
exclude = True,
|
||||
)
|
||||
)
|
||||
class TeamDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
|
||||
permission_classes = [
|
||||
OrganizationPermissionAPI
|
||||
]
|
||||
|
||||
queryset = Team.objects.all()
|
||||
serializer_class = TeamSerializer
|
||||
|
||||
lookup_field = 'group_ptr_id'
|
||||
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
get=extend_schema(
|
||||
summary = "Fetch a teams permissions",
|
||||
tags = ['team',],
|
||||
),
|
||||
post=extend_schema(
|
||||
summary = "Replace team Permissions",
|
||||
description = """Replace the teams permissions with the permissions supplied.
|
||||
|
||||
Teams Permissions will be replaced with the permissions supplied. **ALL** existing permissions will be
|
||||
removed.
|
||||
|
||||
permissions are required to be in format `<module name>_<permission>_<table name>`
|
||||
""",
|
||||
|
||||
methods=["POST"],
|
||||
tags = ['team',],
|
||||
request = TeamPermissionSerializer,
|
||||
responses = {
|
||||
200: OpenApiResponse(description='Team has been updated with the supplied permissions'),
|
||||
401: OpenApiResponse(description='User Not logged in'),
|
||||
403: OpenApiResponse(description='User is missing permission or in different organization'),
|
||||
}
|
||||
),
|
||||
delete=extend_schema(
|
||||
summary = "Delete permissions",
|
||||
tags = ['team',],
|
||||
),
|
||||
patch = extend_schema(
|
||||
summary = "Amend team Permissions",
|
||||
description = """Amend the teams permissions with the permissions supplied.
|
||||
|
||||
Teams permissions will include the existing permissions along with the ones supplied.
|
||||
permissions are required to be in format `<module name>_<permission>_<table name>`
|
||||
""",
|
||||
|
||||
methods=["PATCH"],
|
||||
parameters = None,
|
||||
tags = ['team',],
|
||||
request = TeamPermissionSerializer,
|
||||
responses = {
|
||||
200: OpenApiResponse(description='Team has been updated with the supplied permissions'),
|
||||
401: OpenApiResponse(description='User Not logged in'),
|
||||
403: OpenApiResponse(description='User is missing permission or in different organization'),
|
||||
}
|
||||
),
|
||||
put = extend_schema(
|
||||
summary = "Amend team Permissions",
|
||||
tags = ['team',],
|
||||
)
|
||||
)
|
||||
class TeamPermissionDetail(views.APIView):
|
||||
|
||||
permission_classes = [
|
||||
OrganizationPermissionAPI
|
||||
]
|
||||
|
||||
queryset = Team.objects.all()
|
||||
|
||||
serializer_class = TeamPermissionSerializer
|
||||
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
|
||||
return Response(data=Team.objects.get(pk=self.kwargs['group_ptr_id']).permission_list()[0])
|
||||
|
||||
|
||||
def get_view_name(self):
|
||||
return "Team Permissions"
|
||||
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
|
||||
vals = self.process_request()
|
||||
|
||||
remove = vals['remove']
|
||||
|
||||
new_permission = Team.objects.get(pk=self.kwargs['group_ptr_id'])
|
||||
|
||||
|
||||
for remove_permission in remove:
|
||||
new_permission.permissions.remove(remove_permission)
|
||||
new_permission.save()
|
||||
|
||||
return Response(data=Team.objects.get(pk=self.kwargs['group_ptr_id']).permission_list()[0])
|
||||
|
||||
|
||||
def patch(self, request, *args, **kwargs):
|
||||
|
||||
vals = self.process_request()
|
||||
|
||||
add = vals['add']
|
||||
|
||||
new_permission = Team.objects.get(pk=self.kwargs['group_ptr_id'])
|
||||
|
||||
for add_permission in add:
|
||||
new_permission.permissions.add(add_permission)
|
||||
new_permission.save()
|
||||
|
||||
|
||||
return Response(data=Team.objects.get(pk=self.kwargs['group_ptr_id']).permission_list()[0])
|
||||
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
|
||||
vals = self.process_request()
|
||||
|
||||
add = vals['add']
|
||||
remove = vals['remove']
|
||||
exists = vals['exists']
|
||||
|
||||
new_permission = Team.objects.get(pk=self.kwargs['group_ptr_id'])
|
||||
|
||||
for add_permission in add:
|
||||
new_permission.permissions.add(add_permission)
|
||||
new_permission.save()
|
||||
|
||||
for remove_permission in remove:
|
||||
new_permission.permissions.remove(remove_permission)
|
||||
new_permission.save()
|
||||
|
||||
|
||||
return Response(data=Team.objects.get(pk=self.kwargs['group_ptr_id']).permission_list()[0])
|
||||
|
||||
|
||||
def process_request(self) -> dict({
|
||||
"add": list,
|
||||
"remove": list,
|
||||
"exists": list
|
||||
}):
|
||||
|
||||
initial_values = Team.objects.get(pk=self.kwargs['group_ptr_id']).permission_list()
|
||||
|
||||
add = []
|
||||
remove = []
|
||||
exists = []
|
||||
|
||||
|
||||
for request_permission in self.request.data:
|
||||
|
||||
fields = request_permission.split('.')
|
||||
|
||||
try:
|
||||
|
||||
permission = Permission.objects.get(codename=str(fields[1]), content_type__app_label=str(fields[0]))
|
||||
|
||||
exists += [ permission.id ]
|
||||
|
||||
if permission and request_permission not in initial_values[0]:
|
||||
add += [ permission.id ]
|
||||
|
||||
except:
|
||||
|
||||
raise serializers.ValidationError(f'Value was invalid: {request_permission}')
|
||||
|
||||
for existing_permission in initial_values[1].all():
|
||||
|
||||
if existing_permission.id not in add and existing_permission.id not in exists:
|
||||
remove += [ existing_permission.id ]
|
||||
|
||||
return {
|
||||
"add": add,
|
||||
"remove": remove,
|
||||
"exists": exists
|
||||
}
|
54
app/api/views/config.py
Normal file
54
app/api/views/config.py
Normal file
@ -0,0 +1,54 @@
|
||||
from drf_spectacular.utils import extend_schema, extend_schema_view
|
||||
|
||||
from rest_framework import generics
|
||||
|
||||
from api.serializers.config import ConfigGroupsSerializer
|
||||
from api.views.mixin import OrganizationPermissionAPI
|
||||
|
||||
from config_management.models.groups import ConfigGroups
|
||||
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
get=extend_schema(
|
||||
summary = "Fetch Config groups",
|
||||
description="Returns a list of Config Groups."
|
||||
),
|
||||
)
|
||||
class ConfigGroupsList(generics.ListAPIView):
|
||||
|
||||
permission_classes = [
|
||||
OrganizationPermissionAPI
|
||||
]
|
||||
|
||||
queryset = ConfigGroups.objects.all()
|
||||
lookup_field = 'pk'
|
||||
serializer_class = ConfigGroupsSerializer
|
||||
|
||||
|
||||
def get_view_name(self):
|
||||
return "Config Groups"
|
||||
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
get=extend_schema(
|
||||
summary = "Get A Config Group",
|
||||
# responses = {}
|
||||
),
|
||||
)
|
||||
class ConfigGroupsDetail(generics.RetrieveAPIView):
|
||||
|
||||
permission_classes = [
|
||||
OrganizationPermissionAPI
|
||||
]
|
||||
|
||||
queryset = ConfigGroups.objects.all()
|
||||
lookup_field = 'pk'
|
||||
serializer_class = ConfigGroupsSerializer
|
||||
|
||||
|
||||
def get_view_name(self):
|
||||
return "Config Group"
|
||||
|
||||
|
34
app/api/views/index.py
Normal file
34
app/api/views/index.py
Normal file
@ -0,0 +1,34 @@
|
||||
# from django.contrib.auth.mixins import PermissionRequiredMixin, LoginRequiredMixin
|
||||
from django.contrib.auth.models import User
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from rest_framework import generics, permissions, routers, viewsets
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.reverse import reverse
|
||||
|
||||
class Index(viewsets.ViewSet):
|
||||
|
||||
# permission_required = 'access.view_organization'
|
||||
|
||||
def get_view_name(self):
|
||||
return "API Index"
|
||||
|
||||
def get_view_description(self, html=False) -> str:
|
||||
text = "My REST API"
|
||||
if html:
|
||||
return mark_safe(f"<p>{text}</p>")
|
||||
else:
|
||||
return text
|
||||
|
||||
|
||||
def list(self, request, pk=None):
|
||||
return Response(
|
||||
{
|
||||
# "teams": reverse("_api_teams", request=request),
|
||||
"devices": reverse("API:device-list", request=request),
|
||||
"config_groups": reverse("API:_api_config_groups", request=request),
|
||||
"organizations": reverse("API:_api_orgs", request=request),
|
||||
"software": reverse("API:software-list", request=request),
|
||||
}
|
||||
)
|
0
app/api/views/itam/__init__.py
Normal file
0
app/api/views/itam/__init__.py
Normal file
19
app/api/views/itam/config.py
Normal file
19
app/api/views/itam/config.py
Normal file
@ -0,0 +1,19 @@
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin, LoginRequiredMixin
|
||||
|
||||
from itam.models.device import Device
|
||||
|
||||
from rest_framework import views
|
||||
from rest_framework.response import Response
|
||||
|
||||
|
||||
class View(views.APIView):
|
||||
|
||||
def get(self, request, slug):
|
||||
|
||||
device = Device.objects.get(slug=slug)
|
||||
|
||||
return Response(device.get_configuration(device.id))
|
||||
|
||||
|
||||
def get_view_name(self):
|
||||
return "Device Config"
|
55
app/api/views/itam/device.py
Normal file
55
app/api/views/itam/device.py
Normal file
@ -0,0 +1,55 @@
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from drf_spectacular.utils import extend_schema
|
||||
|
||||
from rest_framework import generics, viewsets
|
||||
|
||||
from access.mixin import OrganizationMixin
|
||||
|
||||
from api.serializers.itam.device import DeviceSerializer
|
||||
from api.views.mixin import OrganizationPermissionAPI
|
||||
|
||||
from itam.models.device import Device
|
||||
|
||||
|
||||
|
||||
class DeviceViewSet(OrganizationMixin, viewsets.ModelViewSet):
|
||||
|
||||
permission_classes = [
|
||||
OrganizationPermissionAPI
|
||||
]
|
||||
|
||||
queryset = Device.objects.all()
|
||||
|
||||
serializer_class = DeviceSerializer
|
||||
|
||||
|
||||
@extend_schema( description='Fetch devices that are from the users assigned organization(s)', methods=["GET"])
|
||||
def list(self, request):
|
||||
|
||||
return super().list(request)
|
||||
|
||||
|
||||
@extend_schema( description='Fetch the selected device', methods=["GET"])
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
|
||||
return super().retrieve(request, *args, **kwargs)
|
||||
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
if self.request.user.is_superuser:
|
||||
|
||||
return self.queryset.filter().order_by('name')
|
||||
|
||||
else:
|
||||
|
||||
return self.queryset.filter(Q(organization__in=self.user_organizations()) | Q(is_global = True)).order_by('name')
|
||||
|
||||
|
||||
def get_view_name(self):
|
||||
if self.detail:
|
||||
return "Device"
|
||||
|
||||
return 'Devices'
|
123
app/api/views/itam/inventory.py
Normal file
123
app/api/views/itam/inventory.py
Normal file
@ -0,0 +1,123 @@
|
||||
import json
|
||||
import re
|
||||
|
||||
from django.core.exceptions import ValidationError, PermissionDenied
|
||||
|
||||
from drf_spectacular.utils import extend_schema, OpenApiResponse
|
||||
|
||||
from rest_framework import generics, views
|
||||
from rest_framework.response import Response
|
||||
|
||||
from api.views.mixin import OrganizationPermissionAPI
|
||||
from api.serializers.itam.inventory import InventorySerializer
|
||||
from api.serializers.inventory import Inventory
|
||||
|
||||
from core.http.common import Http
|
||||
|
||||
from itam.models.device import Device
|
||||
|
||||
from settings.models.user_settings import UserSettings
|
||||
|
||||
from api.tasks import process_inventory
|
||||
|
||||
|
||||
|
||||
class InventoryPermissions(OrganizationPermissionAPI):
|
||||
|
||||
def permission_check(self, request, view, obj=None) -> bool:
|
||||
|
||||
data = view.request.data
|
||||
|
||||
self.obj = Device.objects.get(slug=str(data.details.name).lower())
|
||||
|
||||
return super().permission_check(request, view, obj=None)
|
||||
|
||||
|
||||
|
||||
class Collect(OrganizationPermissionAPI, views.APIView):
|
||||
|
||||
queryset = Device.objects.all()
|
||||
|
||||
|
||||
@extend_schema(
|
||||
summary = "Upload a device's inventory",
|
||||
description = """After inventorying a device, it's inventory file, `.json` is uploaded to this endpoint.
|
||||
If the device does not exist, it will be created. If the device does exist the existing
|
||||
device will be updated with the information within the inventory.
|
||||
|
||||
matching for an existing device is by slug which is the hostname converted to lower case
|
||||
letters. This conversion is automagic.
|
||||
|
||||
**NOTE:** _for device creation, the API user must have user setting 'Default Organization'. Without
|
||||
this setting populated, no device will be created and the endpoint will return HTTP/403_
|
||||
|
||||
## Permissions
|
||||
|
||||
- `itam.add_device` Required to upload inventory
|
||||
""",
|
||||
|
||||
methods=["POST"],
|
||||
parameters = None,
|
||||
tags = ['device', 'inventory',],
|
||||
request = InventorySerializer,
|
||||
responses = {
|
||||
200: OpenApiResponse(description='Inventory upload successful'),
|
||||
401: OpenApiResponse(description='User Not logged in'),
|
||||
403: OpenApiResponse(description='User is missing permission or in different organization'),
|
||||
500: OpenApiResponse(description='Exception occured. View server logs for the Stack Trace'),
|
||||
}
|
||||
)
|
||||
def post(self, request, *args, **kwargs):
|
||||
|
||||
status = Http.Status.OK
|
||||
response_data = 'OK'
|
||||
|
||||
try:
|
||||
|
||||
data = json.loads(request.body)
|
||||
data = Inventory(data)
|
||||
|
||||
device = None
|
||||
|
||||
|
||||
self.default_organization = UserSettings.objects.get(user=request.user).default_organization
|
||||
|
||||
if Device.objects.filter(slug=str(data.details.name).lower()).exists():
|
||||
|
||||
self.obj = Device.objects.get(slug=str(data.details.name).lower())
|
||||
|
||||
device = self.obj
|
||||
|
||||
|
||||
if not self.permission_check(request=request, view=self, obj=device):
|
||||
|
||||
raise Http404
|
||||
|
||||
task = process_inventory.delay(request.body, self.default_organization.id)
|
||||
|
||||
response_data: dict = {"task_id": f"{task.id}"}
|
||||
|
||||
except PermissionDenied as e:
|
||||
|
||||
status = Http.Status.FORBIDDEN
|
||||
response_data = ''
|
||||
|
||||
except ValidationError as e:
|
||||
|
||||
status = Http.Status.BAD_REQUEST
|
||||
response_data = e.message
|
||||
|
||||
except Exception as e:
|
||||
|
||||
print(f'An error occured{e}')
|
||||
|
||||
status = Http.Status.SERVER_ERROR
|
||||
response_data = 'Unknown Server Error occured'
|
||||
|
||||
|
||||
return Response(data=response_data,status=status)
|
||||
|
||||
|
||||
|
||||
def get_view_name(self):
|
||||
return "Inventory"
|
43
app/api/views/itam/software.py
Normal file
43
app/api/views/itam/software.py
Normal file
@ -0,0 +1,43 @@
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from rest_framework import generics, viewsets
|
||||
|
||||
from access.mixin import OrganizationMixin
|
||||
|
||||
from api.serializers.itam.software import SoftwareSerializer
|
||||
from api.views.mixin import OrganizationPermissionAPI
|
||||
|
||||
from itam.models.software import Software
|
||||
|
||||
|
||||
|
||||
class SoftwareViewSet(OrganizationMixin, viewsets.ModelViewSet):
|
||||
|
||||
permission_classes = [
|
||||
OrganizationPermissionAPI
|
||||
]
|
||||
|
||||
queryset = Software.objects.all()
|
||||
|
||||
serializer_class = SoftwareSerializer
|
||||
|
||||
|
||||
def get_object(self, queryset=None, **kwargs):
|
||||
item = self.kwargs.get('pk')
|
||||
return get_object_or_404(Software, pk=item)
|
||||
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
if self.request.user.is_superuser:
|
||||
|
||||
return self.queryset.filter().order_by('name')
|
||||
|
||||
else:
|
||||
|
||||
return self.queryset.filter(Q(organization__in=self.user_organizations()) | Q(is_global = True)).order_by('name')
|
||||
|
||||
|
||||
def get_view_name(self):
|
||||
return "Software"
|
146
app/api/views/mixin.py
Normal file
146
app/api/views/mixin.py
Normal file
@ -0,0 +1,146 @@
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.forms import ValidationError
|
||||
|
||||
from rest_framework import exceptions
|
||||
from rest_framework.permissions import DjangoObjectPermissions
|
||||
|
||||
from access.mixin import OrganizationMixin
|
||||
|
||||
|
||||
|
||||
class OrganizationPermissionAPI(DjangoObjectPermissions, OrganizationMixin):
|
||||
"""checking organization membership"""
|
||||
|
||||
def has_permission(self, request, view):
|
||||
|
||||
return self.permission_check(request, view)
|
||||
|
||||
|
||||
def has_object_permission(self, request, view, obj):
|
||||
|
||||
return self.permission_check(request, view, obj)
|
||||
|
||||
|
||||
def permission_check(self, request, view, obj=None) -> bool:
|
||||
|
||||
if request.user.is_anonymous:
|
||||
|
||||
return False
|
||||
|
||||
self.request = request
|
||||
|
||||
method = self.request._request.method.lower()
|
||||
|
||||
if method.upper() not in view.allowed_methods:
|
||||
|
||||
view.http_method_not_allowed(request._request)
|
||||
|
||||
if hasattr(view, 'queryset'):
|
||||
if view.queryset.model._meta:
|
||||
self.obj = view.queryset.model
|
||||
|
||||
object_organization = None
|
||||
|
||||
if method == 'get':
|
||||
|
||||
action = 'view'
|
||||
|
||||
elif method == 'post':
|
||||
|
||||
action = 'add'
|
||||
|
||||
if 'organization' in request.data:
|
||||
|
||||
if not request.data['organization']:
|
||||
raise ValidationError('you must provide an organization')
|
||||
|
||||
object_organization = int(request.data['organization'])
|
||||
elif method == 'patch':
|
||||
|
||||
action = 'change'
|
||||
|
||||
elif method == 'put':
|
||||
|
||||
action = 'change'
|
||||
|
||||
elif method == 'delete':
|
||||
|
||||
action = 'delete'
|
||||
|
||||
else:
|
||||
|
||||
action = 'view'
|
||||
|
||||
permission = self.obj._meta.app_label + '.' + action + '_' + self.obj._meta.model_name
|
||||
|
||||
self.permission_required = [ permission ]
|
||||
|
||||
|
||||
if view:
|
||||
if 'organization_id' in view.kwargs:
|
||||
|
||||
if view.kwargs['organization_id']:
|
||||
|
||||
object_organization = view.kwargs['organization_id']
|
||||
|
||||
if object_organization is None and 'pk' in view.kwargs:
|
||||
|
||||
self.obj = view.queryset.get(pk=view.kwargs['pk'])
|
||||
|
||||
|
||||
if obj:
|
||||
|
||||
if obj.get_organization():
|
||||
|
||||
object_organization = obj.get_organization().id
|
||||
|
||||
if hasattr(self.obj, 'is_global'):
|
||||
|
||||
if obj.is_global:
|
||||
|
||||
object_organization = 0
|
||||
|
||||
|
||||
if 'pk' in view.kwargs:
|
||||
|
||||
if object_organization is None and view.queryset.model._meta.model_name == 'organization' and view.kwargs['pk']:
|
||||
|
||||
object_organization = view.kwargs['pk']
|
||||
|
||||
if object_organization is None:
|
||||
|
||||
self.obj = view.queryset.get()
|
||||
|
||||
|
||||
if hasattr(self, 'obj') and object_organization is None and 'pk' in view.kwargs:
|
||||
|
||||
if self.obj.get_organization():
|
||||
|
||||
object_organization = self.obj.get_organization().id
|
||||
|
||||
if hasattr(self.obj, 'is_global'):
|
||||
|
||||
if self.obj.is_global:
|
||||
|
||||
object_organization = 0
|
||||
|
||||
|
||||
# ToDo: implement proper checking of listview as this if allows ALL.
|
||||
if 'pk' not in view.kwargs and method == 'get' and object_organization is None:
|
||||
|
||||
return True
|
||||
|
||||
if hasattr(self, 'default_organization'):
|
||||
object_organization = self.default_organization
|
||||
|
||||
if method == 'post' and hasattr(self, 'default_organization'):
|
||||
|
||||
if self.default_organization:
|
||||
|
||||
object_organization = self.default_organization.id
|
||||
|
||||
if not self.has_organization_permission(object_organization) and not request.user.is_superuser:
|
||||
|
||||
raise PermissionDenied('You are not part of this organization')
|
||||
|
||||
return True
|
3
app/app/__init__.py
Normal file
3
app/app/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from .celery import worker as celery_app
|
||||
|
||||
__all__ = ('celery_app',)
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user