Compare commits
636 Commits
spike/mast
...
master
Author | SHA1 | Date | |
---|---|---|---|
e97b177a92 | |||
6191bf16e0 | |||
63af33d003 | |||
3b94f117fb | |||
c181500a1d | |||
c26ae77a09 | |||
cb8a5190ce | |||
60341bf644 | |||
cc2b04472f | |||
4811c0328c | |||
cb4aecb9c8 | |||
d949604d70 | |||
f33a3a2df7 | |||
23b60641e3 | |||
285e6fbeb5 | |||
1eed0e9b21 | |||
80d79de5f4 | |||
860df4c69f | |||
b1ab1afbb6 | |||
13f258fb90 | |||
a7d586f0f7 | |||
8e02610679 | |||
fbd17a21e2 | |||
e57f4bf0c8 | |||
8b5cf8adc1 | |||
0c9931d73a | |||
2bacea7ad9 | |||
a1163bfc8d | |||
1eaeade155 | |||
2138b3eed0 | |||
0763511edd | |||
99a26aed8b | |||
5046e9a296 | |||
085876c772 | |||
e4e1216a85 | |||
d6a0511d98 | |||
e1fda412ae | |||
4d27a98c9a | |||
c191af2ea1 | |||
05e97f4844 | |||
c32361a83a | |||
46fce275ae | |||
9d2692add3 | |||
0af389d7c6 | |||
afb6771ed7 | |||
f6d66d5712 | |||
a77eaefb94 | |||
c5722e494f | |||
409f5e97c7 | |||
3391ec6cc9 | |||
6167ef2221 | |||
de88f1d842 | |||
9418303b7c | |||
d157730de7 | |||
04d9b43301 | |||
62867a4cac | |||
cbadc6e2c7 | |||
46dc653174 | |||
fc2b437a55 | |||
c23baca47a | |||
23a6f89943 | |||
2e9e5d6ef0 | |||
6c25dddcda | |||
|
85376d51af | ||
a93c097f64 | |||
1201c16094 | |||
c953b815d5 | |||
e09a09cf92 | |||
dbe695c214 | |||
66332b6de2 | |||
9806bcd78f | |||
4eda99b95a | |||
|
d8b1d18952 | ||
5b18681e96 | |||
65a4647af9 | |||
661b06859f | |||
f0eb76ee61 | |||
8d8c5df0b6 | |||
b0a28200f9 | |||
216886e5f4 | |||
f5de2a8850 | |||
48f4cf8ed6 | |||
4314b647f6 | |||
ec1deb8903 | |||
44ff30c54a | |||
896968e110 | |||
|
552146a98c | ||
|
898aa858b1 | ||
|
ca952ce748 | ||
|
4438e7abd5 | ||
73c378b456 | |||
ad04faa21d | |||
277369a960 | |||
87af20a3a1 | |||
7f418c12a1 | |||
f8fb273918 | |||
d3ca2b7e23 | |||
4572c6bda0 | |||
|
cc7b8fcf9b | ||
|
1a3fad80ee | ||
67c1b50239 | |||
3faf2ea99e | |||
907e27ec19 | |||
187c0db8e2 | |||
d9558f2cfe | |||
c7003148ae | |||
|
36654a69d8 | ||
496cdf295b | |||
2c0101b46d | |||
188cb9733e | |||
fb974a3b43 | |||
f150ea2091 | |||
fd1bd897b1 | |||
e427bb1784 | |||
|
238e413aa7 | ||
5f9a6d53d8 | |||
|
fef9fba073 | ||
3d77dcc2ce | |||
|
38e4b00107 | ||
|
96ef490207 | ||
|
0a996a9a8f | ||
|
242b6f88c9 | ||
0f71c6a88d | |||
5c651891c3 | |||
|
727644736d | ||
|
1f49970e66 | ||
e5e9f26856 | |||
|
85abe5c3cb | ||
|
47338cead8 | ||
63b02ff9cb | |||
|
07dbc45c80 | ||
|
b15464bed5 | ||
|
650ae365e5 | ||
|
d97827c01e | ||
|
e9dde69c40 | ||
|
7f98bbde64 | ||
|
ea130581a3 | ||
|
f8de575b77 | ||
|
d2f9f0ae8f | ||
|
4c44f42b79 | ||
|
73f147c557 | ||
|
378e1ec584 | ||
|
ec53934f30 | ||
|
063fcf90a3 | ||
845857c14d | |||
994a0e13c0 | |||
|
51aebc65b2 | ||
|
287c1ad9dc | ||
|
53ffe9e738 | ||
|
cec31055a5 | ||
|
f1e977c905 | ||
|
95457980d8 | ||
|
70d73d8caa | ||
|
bdac905958 | ||
|
1c19afefa6 | ||
|
0edc2cca91 | ||
|
51c658cdc7 | ||
|
796d1a0991 | ||
|
5986ca26fe | ||
|
db17a2e990 | ||
|
ddfabef9b7 | ||
|
6df93bfbb6 | ||
|
b69af8fa57 | ||
|
cdc21f05ba | ||
|
64461fc4da | ||
|
3b340a92ed | ||
|
909312a8a1 | ||
|
dc1a17e915 | ||
|
1d0d2372ea | ||
|
09c19649e4 | ||
|
d7eed08420 | ||
|
e5ec867819 | ||
|
6f3c03e6b6 | ||
|
0b0f57c176 | ||
|
b3b70aaaf4 | ||
|
8eab3b7cd5 | ||
|
7d200efbb5 | ||
|
97fa23165c | ||
|
a39cf73cf0 | ||
|
5ada0dae35 | ||
|
8864a17b2b | ||
|
5a296e0a9f | ||
|
810c3923d2 | ||
|
26efbd5652 | ||
|
e1895e3735 | ||
|
dc0835fa25 | ||
|
22bca20c49 | ||
|
e2b6986d2b | ||
|
5f4f50a325 | ||
|
ca0589d084 | ||
|
5764accbc5 | ||
|
c2dd3d8de9 | ||
|
7376301ed4 | ||
|
ea2d2c9100 | ||
|
cb47d526ac | ||
|
bec559c9c3 | ||
|
61473abf68 | ||
|
c862df7846 | ||
|
27f29ef665 | ||
|
0232ff078d | ||
|
e6f9484f99 | ||
|
28bdd9220d | ||
|
63db939583 | ||
|
368da1cc95 | ||
|
080f9279b3 | ||
|
221889e6ca | ||
|
7663825a30 | ||
|
77ef126a7e | ||
|
e305c3c935 | ||
|
67e850f9b2 | ||
|
4f22dffe5d | ||
|
c6d92e271f | ||
|
d666e22297 | ||
|
bece972a4e | ||
|
7f5b2358d3 | ||
|
560cd9cf9f | ||
|
2506acc531 | ||
|
64fbabf606 | ||
|
d77d853c80 | ||
|
472734a516 | ||
|
df493e40d7 | ||
|
a56af5af25 | ||
|
0b60b9f0ff | ||
|
de0c8dcfbc | ||
|
639925e834 | ||
|
6b6f8127bb | ||
|
d98b8feaad | ||
|
398f15d5de | ||
|
a93143ff00 | ||
|
c3195662dd | ||
|
d3312c4444 | ||
|
31854bb838 | ||
|
16d1372d41 | ||
|
956ee581c6 | ||
|
49ba141ca5 | ||
|
89415067ef | ||
|
21a4519743 | ||
|
d13f0cbcdf | ||
|
b431a6653d | ||
|
3e6da45302 | ||
|
d08e60f8dc | ||
|
09be849b09 | ||
|
082bdd5527 | ||
|
08f51a196e | ||
|
0feea6b2d6 | ||
|
6ec4c86ad6 | ||
|
00174e4c4a | ||
|
0bab27d723 | ||
|
863f0e2811 | ||
|
44eb59c918 | ||
|
2cae17a045 | ||
|
d63e3f31e9 | ||
|
1dd63161ab | ||
|
430f75ea15 | ||
|
01d9cbd711 | ||
|
bad7d146fa | ||
|
68c3375a08 | ||
|
7d8d6bb495 | ||
|
e51485eb63 | ||
|
9cd4525e2b | ||
|
3fa02d4a10 | ||
|
a3d2dd3db1 | ||
|
bc27e6dc89 | ||
|
8041553734 | ||
|
4e90f53bf3 | ||
|
7465b9df63 | ||
|
0b48e8d1b7 | ||
|
ac5f19e399 | ||
|
3eec8a4138 | ||
|
2c5ad094f1 | ||
|
2f0f18182c | ||
|
35efa40ebb | ||
|
0fe1f85549 | ||
|
cacfbabcfb | ||
|
765d3c689e | ||
|
ec00e445a0 | ||
|
37e7b5179d | ||
|
9720b37d85 | ||
|
23796c56f9 | ||
|
756d5e1ae6 | ||
|
18a3718c75 | ||
|
2afdb3c3d7 | ||
|
da793ee546 | ||
|
87e2b05926 | ||
|
4ec26108fa | ||
|
af90fefd49 | ||
|
2abe88eb15 | ||
|
1451e7b661 | ||
|
cd9be1db75 | ||
|
3541b0c48c | ||
|
fd96bfffb2 | ||
|
df48bfc0da | ||
|
a06feff42e | ||
|
8731f4a7b2 | ||
|
a117258085 | ||
|
84ce90b28f | ||
|
a33cb4ec29 | ||
|
817c1a9e58 | ||
|
7f6e363c8f | ||
|
8af93603d5 | ||
|
a1c3e95032 | ||
|
8045b66324 | ||
|
1c45443da6 | ||
|
4aa8b85bb6 | ||
|
7d4815e2cf | ||
|
9fb6610ec8 | ||
|
2124d448bf | ||
|
1a18ba4a3d | ||
|
2531b9071f | ||
|
9a7b683e7d | ||
|
68dfadf0e2 | ||
|
595c11c7ac | ||
|
8b4e7869ad | ||
|
6b4c9f6c51 | ||
|
258f8b1f66 | ||
|
e880a1c2c8 | ||
|
cb641eb8c6 | ||
|
81cfbc62e4 | ||
|
661f23e1e2 | ||
|
3de8b00672 | ||
|
a7f669a1f9 | ||
|
d0b201aff9 | ||
|
2022492f21 | ||
|
dd4f39902f | ||
|
a04929453c | ||
|
5ea8069608 | ||
|
a66ed8e59f | ||
|
86802a2aab | ||
|
eebe2d40a6 | ||
|
8a62d9802e | ||
|
787400c089 | ||
|
6be5cfd923 | ||
|
2cb9375d03 | ||
|
c8e835f880 | ||
|
7869d07d30 | ||
|
bc05fb1eeb | ||
|
322736cd01 | ||
|
c03697ccd9 | ||
|
8529ec9949 | ||
|
41d3b678c4 | ||
|
dfc7162675 | ||
|
1dd3b3e8e3 | ||
|
4616bb133d | ||
|
9aa86a1652 | ||
|
5d63544a15 | ||
|
cb84efe63d | ||
|
5ebed0f75d | ||
|
0e4602aac6 | ||
|
0486dc3fae | ||
|
c4cf6c4312 | ||
|
eeab68d63a | ||
|
80f342eeae | ||
|
c4531cb217 | ||
|
86128f5994 | ||
|
f16953877f | ||
|
bef358eda6 | ||
|
f2bc42bd85 | ||
|
433d0e19f5 | ||
|
06996e4dc4 | ||
|
18f3234272 | ||
|
e2bdb96e83 | ||
|
58200eee1f | ||
|
507bee0953 | ||
|
80b1e0ff25 | ||
|
d9ee0017dd | ||
|
60f2af6404 | ||
|
9945eddf7a | ||
|
45c1bed43b | ||
|
bf3da34d32 | ||
|
2eb8b70517 | ||
|
a2f2fdb6a4 | ||
|
8605d4c4f1 | ||
|
212b1077c8 | ||
|
57cf316c00 | ||
|
8fb92f9978 | ||
|
5f08b41724 | ||
|
dc6445544a | ||
|
ece36209a5 | ||
|
bb05eb4ac4 | ||
|
142aac2bce | ||
|
2ac476d99b | ||
|
03ee2cfd62 | ||
|
f58a68d1cc | ||
|
27c5699c36 | ||
|
5d4fb85383 | ||
|
53d3d68021 | ||
|
1463807b89 | ||
|
08804bd45c | ||
|
fe4fef2752 | ||
|
2610ecd311 | ||
|
5b8688cd63 | ||
|
b20920d646 | ||
|
4721d1be23 | ||
|
46957dc590 | ||
|
5126514877 | ||
|
414149f0a1 | ||
|
2b630aadbc | ||
|
61c50c46ed | ||
|
8ba952a41d | ||
|
a478fe4cf1 | ||
|
4edff1c2f0 | ||
|
9beaf5e684 | ||
|
4c403b0436 | ||
|
fb8862c37e | ||
|
583c45c85d | ||
|
d234ac3227 | ||
|
6c33bbe780 | ||
|
feff1b5794 | ||
|
306f8d1fa8 | ||
|
457641a2dd | ||
|
1dde6b2609 | ||
|
6f6320565c | ||
|
bafae52ce5 | ||
|
e97022fb02 | ||
|
190d39400a | ||
|
46c5f5e53e | ||
|
04a3438182 | ||
|
4814e7899c | ||
|
16513f0786 | ||
|
d4eeb35e91 | ||
|
377b63ca3d | ||
|
f2d0fbe67a | ||
|
31cd92f3be | ||
|
f79c4bd7a1 | ||
|
7db2c23de1 | ||
|
7983aa7e52 | ||
|
57b6399950 | ||
|
354cfbbebc | ||
|
cbb5532394 | ||
|
e7cb3622f3 | ||
|
f9b68df901 | ||
|
60612f6c41 | ||
|
3143f27b6c | ||
|
a2b90b0a36 | ||
|
72e79e2134 | ||
|
01d28a85d4 | ||
|
6eaab336c0 | ||
|
671d80cc71 | ||
|
735f672ea1 | ||
|
8ca206e1c0 | ||
|
bac012f9b4 | ||
|
167bac8a1d | ||
|
c756a251c2 | ||
|
3228b0d0da | ||
|
a45e809228 | ||
|
807ed74091 | ||
|
6d85a59744 | ||
|
5337e7a7e8 | ||
|
e49323e99f | ||
|
f3c207528e | ||
|
295e45e8b8 | ||
|
1284c2acaa | ||
|
ecd5573654 | ||
|
f8ed5069fb | ||
|
4c42d15c12 | ||
|
6efa167427 | ||
|
24e76e03d1 | ||
|
1e0d34306c | ||
|
22e7511952 | ||
|
5e001c59f9 | ||
|
fbb356c8e9 | ||
|
d672d5b0cd | ||
|
967b06cfa7 | ||
|
cd94cb118d | ||
|
b7aac92807 | ||
|
20b9d2f105 | ||
|
0a9f2584f1 | ||
|
3c6618e3cb | ||
|
ab88505144 | ||
|
1a8fb9276e | ||
|
447fbb773d | ||
|
5f536ad043 | ||
|
da60a3020f | ||
|
6816930793 | ||
|
0b3f4d4a03 | ||
|
8c367ab566 | ||
|
64a91c82ae | ||
|
d758e3a979 | ||
|
30b4d1f95c | ||
|
5415806621 | ||
|
1bb712276c | ||
|
dccafcc900 | ||
|
8ae7547775 | ||
|
2dd3deb17b | ||
|
13100a9010 | ||
|
84f1abf0b0 | ||
|
7f358cc7fe | ||
|
89437ca067 | ||
|
d182637f41 | ||
|
f40a574ec3 | ||
|
be52181b7e | ||
|
6df2cbce0b | ||
|
4529527e3b | ||
|
3c89878c21 | ||
|
3908ff3d74 | ||
|
084c12d2c4 | ||
|
1aed3236a5 | ||
|
2c94d6a985 | ||
|
1373756877 | ||
|
dcf7bdc9b1 | ||
|
13fb78a622 | ||
|
b5d50acfbd | ||
|
a5f69a8cad | ||
|
29a2178a48 | ||
|
de6c7c5d4a | ||
|
087e6617d3 | ||
|
2980103764 | ||
|
d3db9d1392 | ||
|
2fa728cc45 | ||
|
b433f91bea | ||
|
0fd30a5700 | ||
|
cc0857e33b | ||
|
1916347490 | ||
|
328e809a2b | ||
|
474d51fbae | ||
|
0de11a2548 | ||
|
8267cef973 | ||
|
5835f15cd9 | ||
|
2cd92d4e2c | ||
|
0257c83fa5 | ||
|
405568fee4 | ||
|
187ccfdcc7 | ||
|
c2880a3914 | ||
|
acfef3dfbe | ||
|
3abc201a8d | ||
|
2fdb914f6d | ||
|
d4adbb972d | ||
|
5edf97b230 | ||
|
456a2f4811 | ||
|
956cadeb09 | ||
|
efa2972375 | ||
|
3b0d46a4c8 | ||
|
b9a7c33c6a | ||
|
5d843f4725 | ||
|
53382beb88 | ||
|
bed81ee7f8 | ||
|
c8bb8cb2ca | ||
|
6570981dc2 | ||
|
51ff0c4990 | ||
|
638a35ea5a | ||
|
7e3d3ec5de | ||
|
f301364d84 | ||
|
f08a1d89b3 | ||
|
411342eb0b | ||
|
318ecd0be4 | ||
|
d81b71cd3a | ||
|
a86e4d7afb | ||
|
590a3031c7 | ||
|
da5b667607 | ||
|
4ad0d5d954 | ||
|
7b039fcedb | ||
|
40ce312880 | ||
|
9a19a65edc | ||
|
6092afe32b | ||
|
a10a3a62e5 | ||
|
ef3e8c968b | ||
|
ea08f4a6c6 | ||
|
7592a1d459 | ||
|
9c1bed2189 | ||
|
a6fd210de5 | ||
|
070f321a06 | ||
|
da00c80b34 | ||
|
fb3b79cfc4 | ||
|
3e30cf2d17 | ||
|
fb961ca4a1 | ||
|
7c7d79c870 | ||
|
5ac1277e7e | ||
|
2a4ee0507c | ||
|
ad1517a16e | ||
|
639ea06243 | ||
|
a94516b3ce | ||
|
1505e7bd66 | ||
|
d2b0f477f2 | ||
|
63bd602397 | ||
|
bb0fb4aa78 | ||
|
90316a262b | ||
|
741e91bb78 | ||
|
1dae396d99 | ||
|
1bc7044527 | ||
|
1ac8df5ed9 | ||
|
7ba20b3687 | ||
|
9f95af7547 | ||
|
98f96a72b1 | ||
|
de439a1658 | ||
|
70db76b439 | ||
|
a89967e91b | ||
|
bc1c94a364 | ||
|
998a5a8aa1 | ||
|
0b7ebac472 | ||
|
6e017aba49 | ||
|
a24ca35bd7 | ||
|
1ad74907bd | ||
|
d5a37ddfae | ||
|
f4960f260e | ||
|
6ab67995ff | ||
|
16cf845015 | ||
|
ce083e928a | ||
|
e3792d9570 | ||
|
633ae1d7d1 | ||
|
5287d78fcb | ||
|
0e417fa772 | ||
|
fa0ba61de3 | ||
|
c6be30895e | ||
|
1dc03456b2 | ||
|
bc87739d6f | ||
|
2459dd3b64 | ||
|
bff4547d40 | ||
|
7f0dfebaa4 | ||
|
35565e1b43 | ||
|
7b7598871b | ||
|
3e66a21805 | ||
|
73969f5a40 | ||
|
3fa514c982 | ||
|
bd5bb859d9 | ||
|
39950f0d69 | ||
|
4dbbd5ecde | ||
|
ca9879db92 | ||
|
d1d6001842 | ||
|
c6bf856b34 | ||
|
4fdcf77e8e | ||
|
19c0049b0e | ||
|
e7e38f68b5 | ||
|
19a02460e8 | ||
|
ec70357ea0 | ||
|
933e4f3d3d | ||
|
c6d40c8219 | ||
|
9039fa5109 | ||
|
fe60e789d1 | ||
|
0419e7cd21 | ||
|
09da027bed | ||
|
d787544ebf | ||
|
bb4bebaee6 | ||
|
cdde4db66c | ||
|
8799cc86e9 | ||
|
31aafd3b86 | ||
|
603bd4ad67 |
97
.aliases
Normal file
97
.aliases
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
# For using the alias gw-importOfficeData or gw-importHostingAssets,
|
||||||
|
# copy the file .tc-environment to .environment (ignored by git)
|
||||||
|
# and amend them according to your external DB.
|
||||||
|
|
||||||
|
gradleWrapper () {
|
||||||
|
if [ ! -f gradlew ]; then
|
||||||
|
echo "No 'gradlew' found. Maybe you are not in the root dir of a gradle project?"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
TEMPFILE=$(mktemp /tmp/gw.XXXXXX)
|
||||||
|
unbuffer ./gradlew "$@" | tee $TEMPFILE
|
||||||
|
|
||||||
|
echo
|
||||||
|
grep --color=never "Report:" $TEMPFILE
|
||||||
|
rm $TEMPFILE
|
||||||
|
}
|
||||||
|
|
||||||
|
postgresAutodoc () {
|
||||||
|
if ! [ -x "$(command -v postgresql_autodoc)" ]; then
|
||||||
|
echo "Program 'postgresql_autodoc' not found. Please install, e.g. via: sudo apt install postgresql-autodoc" >&2
|
||||||
|
echo "See also https://github.com/cbbrowne/autodoc" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! [ -x "$(command -v dot)" ]; then
|
||||||
|
echo "Program 'graphviz dot' not found. Please install, e.g. via: sudo apt install graphviz" >&2
|
||||||
|
echo "See also https://graphviz.org" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
postgresql_autodoc -d postgres -f build/postgres-autodoc -h localhost -u postgres --password=password \
|
||||||
|
-m '(rbacobject|hs).*' \
|
||||||
|
-l /usr/share/postgresql-autodoc -t neato &&
|
||||||
|
dot -Tsvg build/postgres-autodoc.neato >build/postgres-autodoc-hs.svg && \
|
||||||
|
echo "generated: $PWD/build/postgres-autodoc-hs.svg"
|
||||||
|
|
||||||
|
postgresql_autodoc -d postgres -f build/postgres-autodoc -h localhost -u postgres --password=password \
|
||||||
|
-m '(global|rbac).*' \
|
||||||
|
-l /usr/share/postgresql-autodoc -t neato &&
|
||||||
|
dot -Tsvg build/postgres-autodoc.neato >build/postgres-autodoc-rbac.svg && \
|
||||||
|
echo "generated $PWD/build/postgres-autodoc-rbac.svg"
|
||||||
|
}
|
||||||
|
alias postgres-autodoc=postgresAutodoc
|
||||||
|
|
||||||
|
function importLegacyData() {
|
||||||
|
export target=$1
|
||||||
|
if [ -z "$target" ]; then
|
||||||
|
echo "importLegacyData needs target argument, but none was given" >&2
|
||||||
|
else
|
||||||
|
source .tc-environment
|
||||||
|
|
||||||
|
if [ -f .environment ]; then
|
||||||
|
source .environment
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "using environment (with ending ';' for use in IntelliJ IDEA):"
|
||||||
|
echo "--- BEGIN: ---"
|
||||||
|
set | grep ^HSADMINNG_ | sed 's/$/;/'
|
||||||
|
echo "---- END. ----"
|
||||||
|
echo
|
||||||
|
|
||||||
|
echo ./gradlew $target --rerun
|
||||||
|
./gradlew $target --rerun
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
alias gw-importOfficeData='importLegacyData importOfficeData'
|
||||||
|
alias gw-importHostingAssets='importLegacyData importHostingAssets'
|
||||||
|
|
||||||
|
alias podman-start='systemctl --user enable --now podman.socket && systemctl --user status podman.socket && ls -la /run/user/$UID/podman/podman.sock'
|
||||||
|
alias podman-stop='systemctl --user disable --now podman.socket && systemctl --user status podman.socket && ls -la /run/user/$UID/podman/podman.sock'
|
||||||
|
alias podman-use='export DOCKER_HOST="unix:///run/user/$UID/podman/podman.sock"; export TESTCONTAINERS_RYUK_DISABLED=true'
|
||||||
|
|
||||||
|
alias gw=gradleWrapper
|
||||||
|
alias pg-sql-run='docker run --name hsadmin-ng-postgres -e POSTGRES_PASSWORD=password -p 5432:5432 -d postgres:15.5-bookworm'
|
||||||
|
alias pg-sql-stop='docker stop hsadmin-ng-postgres'
|
||||||
|
alias pg-sql-start='docker container start hsadmin-ng-postgres'
|
||||||
|
alias pg-sql-remove='docker rm hsadmin-ng-postgres'
|
||||||
|
alias pg-sql-reset='pg-sql-stop; pg-sql-remove; pg-sql-run'
|
||||||
|
alias pg-sql-backup='docker exec -i hsadmin-ng-postgres /usr/bin/pg_dump --clean --create -U postgres postgres | gzip -9'
|
||||||
|
alias pg-sql-restore='gunzip --stdout | docker exec -i hsadmin-ng-postgres psql -U postgres -d postgres'
|
||||||
|
|
||||||
|
alias fp='grep -r '@Accepts' src | sed -e 's/^.*@/@/g' | sort -u | wc -l'
|
||||||
|
|
||||||
|
alias gw-spotless='./gradlew spotlessApply -x pitest -x test -x :processResources'
|
||||||
|
alias gw-test='. .aliases; ./gradlew test'
|
||||||
|
alias gw-check='. .aliases; gw test check -x pitest'
|
||||||
|
|
||||||
|
# etc/docker-compose.yml limits CPUs+MEM and includes a PostgreSQL config for analysing slow queries
|
||||||
|
alias gw-importOfficeData-in-docker-compose='
|
||||||
|
docker-compose -f etc/docker-compose.yml down &&
|
||||||
|
docker-compose -f etc/docker-compose.yml up -d && sleep 10 &&
|
||||||
|
time gw-importHostingAssets'
|
||||||
|
|
||||||
|
if [ ! -f .environment ]; then
|
||||||
|
cp .tc-environment .environment
|
||||||
|
fi
|
||||||
|
source .environment
|
@ -18,7 +18,3 @@ insert_final_newline = true
|
|||||||
|
|
||||||
[*.md]
|
[*.md]
|
||||||
trim_trailing_whitespace = false
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
[package.json]
|
|
||||||
indent_style = space
|
|
||||||
indent_size = 2
|
|
||||||
|
20
.gitignore
vendored
20
.gitignore
vendored
@ -3,6 +3,7 @@
|
|||||||
######################
|
######################
|
||||||
/build/www/**
|
/build/www/**
|
||||||
/src/test/javascript/coverage/
|
/src/test/javascript/coverage/
|
||||||
|
/worktrees/
|
||||||
|
|
||||||
######################
|
######################
|
||||||
# Node
|
# Node
|
||||||
@ -77,7 +78,10 @@ out/
|
|||||||
# Gradle
|
# Gradle
|
||||||
######################
|
######################
|
||||||
.gradle/
|
.gradle/
|
||||||
|
!gradle/wrapper/gradle-wrapper.jar
|
||||||
/build/
|
/build/
|
||||||
|
.gradletasknamecache
|
||||||
|
gradle-app.setting
|
||||||
|
|
||||||
######################
|
######################
|
||||||
# Package Files
|
# Package Files
|
||||||
@ -128,17 +132,13 @@ Desktop.ini
|
|||||||
*~
|
*~
|
||||||
.merge_file*
|
.merge_file*
|
||||||
|
|
||||||
######################
|
|
||||||
# Gradle Wrapper
|
|
||||||
######################
|
|
||||||
!gradle/wrapper/gradle-wrapper.jar
|
|
||||||
|
|
||||||
######################
|
|
||||||
# Maven Wrapper
|
|
||||||
######################
|
|
||||||
!.mvn/wrapper/maven-wrapper.jar
|
|
||||||
|
|
||||||
######################
|
######################
|
||||||
# ESLint
|
# ESLint
|
||||||
######################
|
######################
|
||||||
.eslintcache
|
.eslintcache
|
||||||
|
|
||||||
|
######################
|
||||||
|
# Project Related
|
||||||
|
######################
|
||||||
|
/.environment*
|
||||||
|
/src/test/resources/migration-prod/*
|
||||||
|
@ -1,53 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Asset",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldName": "date",
|
|
||||||
"fieldType": "LocalDate",
|
|
||||||
"fieldValidateRules": [
|
|
||||||
"required"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldName": "action",
|
|
||||||
"fieldType": "AssetAction",
|
|
||||||
"fieldValues": "PAYMENT,HANDOVER,ADOPTION,LOSS,CLEARING,PAYBACK",
|
|
||||||
"fieldValidateRules": [
|
|
||||||
"required"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldName": "amount",
|
|
||||||
"fieldType": "BigDecimal",
|
|
||||||
"fieldValidateRules": [
|
|
||||||
"required"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldName": "comment",
|
|
||||||
"fieldType": "String",
|
|
||||||
"fieldValidateRules": [
|
|
||||||
"maxlength"
|
|
||||||
],
|
|
||||||
"fieldValidateRulesMaxlength": 160
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"relationships": [
|
|
||||||
{
|
|
||||||
"relationshipType": "many-to-one",
|
|
||||||
"otherEntityName": "membership",
|
|
||||||
"otherEntityRelationshipName": "asset",
|
|
||||||
"relationshipName": "member",
|
|
||||||
"otherEntityField": "id"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"changelogDate": "20190403083740",
|
|
||||||
"entityTableName": "asset",
|
|
||||||
"dto": "mapstruct",
|
|
||||||
"pagination": "infinite-scroll",
|
|
||||||
"service": "serviceClass",
|
|
||||||
"jpaMetamodelFiltering": true,
|
|
||||||
"fluentMethods": true,
|
|
||||||
"clientRootFolder": "",
|
|
||||||
"applications": "*"
|
|
||||||
}
|
|
@ -1,49 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Contact",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldName": "firstName",
|
|
||||||
"fieldType": "String",
|
|
||||||
"fieldValidateRules": [
|
|
||||||
"required",
|
|
||||||
"maxlength"
|
|
||||||
],
|
|
||||||
"fieldValidateRulesMaxlength": 80
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldName": "lastName",
|
|
||||||
"fieldType": "String",
|
|
||||||
"fieldValidateRules": [
|
|
||||||
"required",
|
|
||||||
"maxlength"
|
|
||||||
],
|
|
||||||
"fieldValidateRulesMaxlength": 80
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldName": "email",
|
|
||||||
"fieldType": "String",
|
|
||||||
"fieldValidateRules": [
|
|
||||||
"required",
|
|
||||||
"maxlength"
|
|
||||||
],
|
|
||||||
"fieldValidateRulesMaxlength": 80
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"relationships": [
|
|
||||||
{
|
|
||||||
"relationshipType": "one-to-many",
|
|
||||||
"otherEntityName": "customerContact",
|
|
||||||
"otherEntityRelationshipName": "contact",
|
|
||||||
"relationshipName": "role"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"changelogDate": "20190403083736",
|
|
||||||
"entityTableName": "contact",
|
|
||||||
"dto": "mapstruct",
|
|
||||||
"pagination": "infinite-scroll",
|
|
||||||
"service": "serviceClass",
|
|
||||||
"jpaMetamodelFiltering": true,
|
|
||||||
"fluentMethods": true,
|
|
||||||
"clientRootFolder": "",
|
|
||||||
"applications": "*"
|
|
||||||
}
|
|
@ -1,50 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Customer",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldName": "number",
|
|
||||||
"fieldType": "Integer",
|
|
||||||
"fieldValidateRules": [
|
|
||||||
"required",
|
|
||||||
"unique",
|
|
||||||
"min",
|
|
||||||
"max"
|
|
||||||
],
|
|
||||||
"fieldValidateRulesMin": 10000,
|
|
||||||
"fieldValidateRulesMax": 99999
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldName": "prefix",
|
|
||||||
"fieldType": "String",
|
|
||||||
"fieldValidateRules": [
|
|
||||||
"required",
|
|
||||||
"unique",
|
|
||||||
"pattern"
|
|
||||||
],
|
|
||||||
"fieldValidateRulesPattern": "[a-z][a-z0-9]+"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"relationships": [
|
|
||||||
{
|
|
||||||
"relationshipType": "one-to-many",
|
|
||||||
"otherEntityName": "membership",
|
|
||||||
"otherEntityRelationshipName": "customer",
|
|
||||||
"relationshipName": "membership"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"relationshipType": "one-to-many",
|
|
||||||
"otherEntityName": "customerContact",
|
|
||||||
"otherEntityRelationshipName": "customer",
|
|
||||||
"relationshipName": "role"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"changelogDate": "20190403083735",
|
|
||||||
"entityTableName": "customer",
|
|
||||||
"dto": "mapstruct",
|
|
||||||
"pagination": "infinite-scroll",
|
|
||||||
"service": "serviceClass",
|
|
||||||
"jpaMetamodelFiltering": true,
|
|
||||||
"fluentMethods": true,
|
|
||||||
"clientRootFolder": "",
|
|
||||||
"applications": "*"
|
|
||||||
}
|
|
@ -1,40 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "CustomerContact",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldName": "role",
|
|
||||||
"fieldType": "CustomerContactRole",
|
|
||||||
"fieldValues": "CONTRACTUAL,TECHNICAL,FINANCIAL",
|
|
||||||
"fieldValidateRules": [
|
|
||||||
"required"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"relationships": [
|
|
||||||
{
|
|
||||||
"relationshipType": "many-to-one",
|
|
||||||
"otherEntityName": "contact",
|
|
||||||
"otherEntityRelationshipName": "role",
|
|
||||||
"relationshipValidateRules": "required",
|
|
||||||
"relationshipName": "contact",
|
|
||||||
"otherEntityField": "email"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"relationshipType": "many-to-one",
|
|
||||||
"otherEntityName": "customer",
|
|
||||||
"otherEntityRelationshipName": "role",
|
|
||||||
"relationshipValidateRules": "required",
|
|
||||||
"relationshipName": "customer",
|
|
||||||
"otherEntityField": "prefix"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"changelogDate": "20190403083737",
|
|
||||||
"entityTableName": "customer_contact",
|
|
||||||
"dto": "mapstruct",
|
|
||||||
"pagination": "infinite-scroll",
|
|
||||||
"service": "serviceClass",
|
|
||||||
"jpaMetamodelFiltering": true,
|
|
||||||
"fluentMethods": true,
|
|
||||||
"clientRootFolder": "",
|
|
||||||
"applications": "*"
|
|
||||||
}
|
|
@ -1,46 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Membership",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldName": "sinceDate",
|
|
||||||
"fieldType": "LocalDate",
|
|
||||||
"fieldValidateRules": [
|
|
||||||
"required"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldName": "untilDate",
|
|
||||||
"fieldType": "LocalDate"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"relationships": [
|
|
||||||
{
|
|
||||||
"relationshipType": "one-to-many",
|
|
||||||
"otherEntityName": "share",
|
|
||||||
"otherEntityRelationshipName": "member",
|
|
||||||
"relationshipName": "share"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"relationshipType": "one-to-many",
|
|
||||||
"otherEntityName": "asset",
|
|
||||||
"otherEntityRelationshipName": "member",
|
|
||||||
"relationshipName": "asset"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"relationshipType": "many-to-one",
|
|
||||||
"otherEntityName": "customer",
|
|
||||||
"otherEntityRelationshipName": "membership",
|
|
||||||
"relationshipName": "customer",
|
|
||||||
"otherEntityField": "prefix"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"changelogDate": "20190403083738",
|
|
||||||
"entityTableName": "membership",
|
|
||||||
"dto": "mapstruct",
|
|
||||||
"pagination": "infinite-scroll",
|
|
||||||
"service": "serviceClass",
|
|
||||||
"jpaMetamodelFiltering": true,
|
|
||||||
"fluentMethods": true,
|
|
||||||
"clientRootFolder": "",
|
|
||||||
"applications": "*"
|
|
||||||
}
|
|
@ -1,53 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Share",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldName": "date",
|
|
||||||
"fieldType": "LocalDate",
|
|
||||||
"fieldValidateRules": [
|
|
||||||
"required"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldName": "action",
|
|
||||||
"fieldType": "ShareAction",
|
|
||||||
"fieldValues": "SUBSCRIPTION,CANCELLATION",
|
|
||||||
"fieldValidateRules": [
|
|
||||||
"required"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldName": "quantity",
|
|
||||||
"fieldType": "Integer",
|
|
||||||
"fieldValidateRules": [
|
|
||||||
"required"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldName": "comment",
|
|
||||||
"fieldType": "String",
|
|
||||||
"fieldValidateRules": [
|
|
||||||
"maxlength"
|
|
||||||
],
|
|
||||||
"fieldValidateRulesMaxlength": 160
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"relationships": [
|
|
||||||
{
|
|
||||||
"relationshipType": "many-to-one",
|
|
||||||
"otherEntityName": "membership",
|
|
||||||
"otherEntityRelationshipName": "share",
|
|
||||||
"relationshipName": "member",
|
|
||||||
"otherEntityField": "id"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"changelogDate": "20190403083739",
|
|
||||||
"entityTableName": "share",
|
|
||||||
"dto": "mapstruct",
|
|
||||||
"pagination": "infinite-scroll",
|
|
||||||
"service": "serviceClass",
|
|
||||||
"jpaMetamodelFiltering": true,
|
|
||||||
"fluentMethods": true,
|
|
||||||
"clientRootFolder": "",
|
|
||||||
"applications": "*"
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
node_modules
|
|
||||||
target
|
|
||||||
package-lock.json
|
|
12
.prettierrc
12
.prettierrc
@ -1,12 +0,0 @@
|
|||||||
# Prettier configuration
|
|
||||||
|
|
||||||
printWidth: 140
|
|
||||||
singleQuote: true
|
|
||||||
tabWidth: 4
|
|
||||||
useTabs: false
|
|
||||||
|
|
||||||
# js and ts rules:
|
|
||||||
arrowParens: avoid
|
|
||||||
|
|
||||||
# jsx and tsx rules:
|
|
||||||
jsxBracketSameLine: false
|
|
37
.run/ImportHostingAssets into local.run.xml
Normal file
37
.run/ImportHostingAssets into local.run.xml
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="ImportHostingAssets into local" type="GradleRunConfiguration" factoryName="Gradle">
|
||||||
|
<ExternalSystemSettings>
|
||||||
|
<option name="env">
|
||||||
|
<map>
|
||||||
|
<entry key="HSADMINNG_POSTGRES_ADMIN_PASSWORD" value="password" />
|
||||||
|
<entry key="HSADMINNG_POSTGRES_ADMIN_USERNAME" value="postgres" />
|
||||||
|
<entry key="HSADMINNG_POSTGRES_JDBC_URL" value="jdbc:postgresql://localhost:5432/postgres" />
|
||||||
|
<entry key="HSADMINNG_POSTGRES_RESTRICTED_USERNAME" value="restricted" />
|
||||||
|
</map>
|
||||||
|
</option>
|
||||||
|
<option name="executionName" />
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
|
<option name="externalSystemIdString" value="GRADLE" />
|
||||||
|
<option name="scriptParameters" value="" />
|
||||||
|
<option name="taskDescriptions">
|
||||||
|
<list />
|
||||||
|
</option>
|
||||||
|
<option name="taskNames">
|
||||||
|
<list>
|
||||||
|
<option value=":importHostingAssets" />
|
||||||
|
<option value="--tests" />
|
||||||
|
<option value=""net.hostsharing.hsadminng.hs.migration.ImportHostingAssets"" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
<option name="vmOptions" />
|
||||||
|
</ExternalSystemSettings>
|
||||||
|
<ExternalSystemDebugServerProcess>false</ExternalSystemDebugServerProcess>
|
||||||
|
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
|
||||||
|
<EXTENSION ID="com.intellij.execution.ExternalSystemRunConfigurationJavaExtension">
|
||||||
|
<extension name="coverage" sample_coverage="false" />
|
||||||
|
</EXTENSION>
|
||||||
|
<DebugAllEnabled>false</DebugAllEnabled>
|
||||||
|
<RunAsTest>true</RunAsTest>
|
||||||
|
<method v="2" />
|
||||||
|
</configuration>
|
||||||
|
</component>
|
36
.run/ImportHostingAssets.run.xml
Normal file
36
.run/ImportHostingAssets.run.xml
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="ImportHostingAssets" type="GradleRunConfiguration" factoryName="Gradle">
|
||||||
|
<ExternalSystemSettings>
|
||||||
|
<option name="env">
|
||||||
|
<map>
|
||||||
|
<entry key="HSADMINNG_MIGRATION_DATA_PATH" value="migration" />
|
||||||
|
<entry key="HSADMINNG_POSTGRES_ADMIN_USERNAME" value="admin" />
|
||||||
|
<entry key="HSADMINNG_POSTGRES_RESTRICTED_USERNAME" value="restricted" />
|
||||||
|
</map>
|
||||||
|
</option>
|
||||||
|
<option name="executionName" />
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
|
<option name="externalSystemIdString" value="GRADLE" />
|
||||||
|
<option name="scriptParameters" value="" />
|
||||||
|
<option name="taskDescriptions">
|
||||||
|
<list />
|
||||||
|
</option>
|
||||||
|
<option name="taskNames">
|
||||||
|
<list>
|
||||||
|
<option value=":importHostingAssets" />
|
||||||
|
<option value="--tests" />
|
||||||
|
<option value=""net.hostsharing.hsadminng.hs.migration.ImportHostingAssets"" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
<option name="vmOptions" />
|
||||||
|
</ExternalSystemSettings>
|
||||||
|
<ExternalSystemDebugServerProcess>false</ExternalSystemDebugServerProcess>
|
||||||
|
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
|
||||||
|
<EXTENSION ID="com.intellij.execution.ExternalSystemRunConfigurationJavaExtension">
|
||||||
|
<extension name="coverage" sample_coverage="false" />
|
||||||
|
</EXTENSION>
|
||||||
|
<DebugAllEnabled>false</DebugAllEnabled>
|
||||||
|
<RunAsTest>true</RunAsTest>
|
||||||
|
<method v="2" />
|
||||||
|
</configuration>
|
||||||
|
</component>
|
103
.run/ImportOfficeData.run.xml
Normal file
103
.run/ImportOfficeData.run.xml
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="ImportOfficeData" type="GradleRunConfiguration" factoryName="Gradle">
|
||||||
|
<ExternalSystemSettings>
|
||||||
|
<option name="env">
|
||||||
|
<map>
|
||||||
|
<entry key="HSADMINNG_MIGRATION_DATA_PATH" value="migration" />
|
||||||
|
<entry key="HSADMINNG_POSTGRES_ADMIN_USERNAME" value="admin" />
|
||||||
|
<entry key="HSADMINNG_POSTGRES_RESTRICTED_USERNAME" value="restricted" />
|
||||||
|
</map>
|
||||||
|
</option>
|
||||||
|
<option name="executionName" />
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
|
<option name="externalSystemIdString" value="GRADLE" />
|
||||||
|
<option name="scriptParameters" value="" />
|
||||||
|
<option name="taskDescriptions">
|
||||||
|
<list />
|
||||||
|
</option>
|
||||||
|
<option name="taskNames">
|
||||||
|
<list>
|
||||||
|
<option value=":importOfficeData" />
|
||||||
|
<option value="--tests" />
|
||||||
|
<option value=""net.hostsharing.hsadminng.hs.migration.ImportOfficeData"" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
<option name="vmOptions" />
|
||||||
|
</ExternalSystemSettings>
|
||||||
|
<ExternalSystemDebugServerProcess>false</ExternalSystemDebugServerProcess>
|
||||||
|
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
|
||||||
|
<EXTENSION ID="com.intellij.execution.ExternalSystemRunConfigurationJavaExtension">
|
||||||
|
<extension name="coverage" sample_coverage="false" />
|
||||||
|
</EXTENSION>
|
||||||
|
<DebugAllEnabled>false</DebugAllEnabled>
|
||||||
|
<RunAsTest>true</RunAsTest>
|
||||||
|
<method v="2" />
|
||||||
|
</configuration>
|
||||||
|
<configuration default="false" name="ImportOfficeData" type="GradleRunConfiguration" factoryName="Gradle">
|
||||||
|
<ExternalSystemSettings>
|
||||||
|
<option name="env">
|
||||||
|
<map>
|
||||||
|
<entry key="HSADMINNG_MIGRATION_DATA_PATH" value="migration" />
|
||||||
|
<entry key="HSADMINNG_POSTGRES_ADMIN_USERNAME" value="admin" />
|
||||||
|
<entry key="HSADMINNG_POSTGRES_RESTRICTED_USERNAME" value="restricted" />
|
||||||
|
</map>
|
||||||
|
</option>
|
||||||
|
<option name="executionName" />
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
|
<option name="externalSystemIdString" value="GRADLE" />
|
||||||
|
<option name="scriptParameters" value="" />
|
||||||
|
<option name="taskDescriptions">
|
||||||
|
<list />
|
||||||
|
</option>
|
||||||
|
<option name="taskNames">
|
||||||
|
<list>
|
||||||
|
<option value=":importOfficeData" />
|
||||||
|
<option value="--tests" />
|
||||||
|
<option value=""net.hostsharing.hsadminng.hs.office.migration.ImportOfficeData"" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
<option name="vmOptions" />
|
||||||
|
</ExternalSystemSettings>
|
||||||
|
<ExternalSystemDebugServerProcess>false</ExternalSystemDebugServerProcess>
|
||||||
|
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
|
||||||
|
<EXTENSION ID="com.intellij.execution.ExternalSystemRunConfigurationJavaExtension">
|
||||||
|
<extension name="coverage" sample_coverage="false" />
|
||||||
|
</EXTENSION>
|
||||||
|
<DebugAllEnabled>false</DebugAllEnabled>
|
||||||
|
<RunAsTest>true</RunAsTest>
|
||||||
|
<method v="2" />
|
||||||
|
</configuration>
|
||||||
|
<configuration default="false" name="ImportOfficeData" type="GradleRunConfiguration" factoryName="Gradle">
|
||||||
|
<ExternalSystemSettings>
|
||||||
|
<option name="env">
|
||||||
|
<map>
|
||||||
|
<entry key="HSADMINNG_POSTGRES_ADMIN_USERNAME" value="admin" />
|
||||||
|
<entry key="HSADMINNG_POSTGRES_RESTRICTED_USERNAME" value="restricted" />
|
||||||
|
</map>
|
||||||
|
</option>
|
||||||
|
<option name="executionName" />
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
|
<option name="externalSystemIdString" value="GRADLE" />
|
||||||
|
<option name="scriptParameters" value="" />
|
||||||
|
<option name="taskDescriptions">
|
||||||
|
<list />
|
||||||
|
</option>
|
||||||
|
<option name="taskNames">
|
||||||
|
<list>
|
||||||
|
<option value=":importOfficeData" />
|
||||||
|
<option value="--tests" />
|
||||||
|
<option value=""net.hostsharing.hsadminng.hs.migration.ImportOfficeData"" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
<option name="vmOptions" />
|
||||||
|
</ExternalSystemSettings>
|
||||||
|
<ExternalSystemDebugServerProcess>false</ExternalSystemDebugServerProcess>
|
||||||
|
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
|
||||||
|
<EXTENSION ID="com.intellij.execution.ExternalSystemRunConfigurationJavaExtension">
|
||||||
|
<extension name="coverage" sample_coverage="false" />
|
||||||
|
</EXTENSION>
|
||||||
|
<DebugAllEnabled>false</DebugAllEnabled>
|
||||||
|
<RunAsTest>true</RunAsTest>
|
||||||
|
<method v="2" />
|
||||||
|
</configuration>
|
||||||
|
</component>
|
1
.run/README.txt
Normal file
1
.run/README.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
Stored run-Configurations for IntelliJ IDEA.
|
8
.tc-environment
Normal file
8
.tc-environment
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
unset HSADMINNG_POSTGRES_JDBC_URL # dynamically set, different for normal tests and imports
|
||||||
|
export HSADMINNG_POSTGRES_ADMIN_USERNAME=admin
|
||||||
|
export HSADMINNG_POSTGRES_ADMIN_PASSWORD=
|
||||||
|
export HSADMINNG_POSTGRES_RESTRICTED_USERNAME=restricted
|
||||||
|
export HSADMINNG_SUPERUSER=superuser-alex@hostsharing.net
|
||||||
|
export HSADMINNG_MIGRATION_DATA_PATH=migration
|
||||||
|
export LIQUIBASE_CONTEXT=
|
||||||
|
export LANG=en_US.UTF-8
|
8
.unset-environment
Normal file
8
.unset-environment
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
unset HSADMINNG_POSTGRES_JDBC_URL
|
||||||
|
unset HSADMINNG_POSTGRES_ADMIN_USERNAME
|
||||||
|
unset HSADMINNG_POSTGRES_ADMIN_PASSWORD
|
||||||
|
unset HSADMINNG_POSTGRES_RESTRICTED_USERNAME
|
||||||
|
unset HSADMINNG_SUPERUSER
|
||||||
|
unset HSADMINNG_MIGRATION_DATA_PATH
|
||||||
|
unset LIQUIBASE_CONTEXT
|
||||||
|
|
38
.yo-rc.json
38
.yo-rc.json
@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
"generator-jhipster": {
|
|
||||||
"promptValues": {
|
|
||||||
"packageName": "org.hostsharing.hsadminng",
|
|
||||||
"nativeLanguage": "de"
|
|
||||||
},
|
|
||||||
"jhipsterVersion": "5.8.2",
|
|
||||||
"applicationType": "monolith",
|
|
||||||
"baseName": "hsadminNg",
|
|
||||||
"packageName": "org.hostsharing.hsadminng",
|
|
||||||
"packageFolder": "org/hostsharing/hsadminng",
|
|
||||||
"serverPort": "8080",
|
|
||||||
"authenticationType": "jwt",
|
|
||||||
"cacheProvider": "ehcache",
|
|
||||||
"enableHibernateCache": false,
|
|
||||||
"websocket": false,
|
|
||||||
"databaseType": "sql",
|
|
||||||
"devDatabaseType": "h2Memory",
|
|
||||||
"prodDatabaseType": "postgresql",
|
|
||||||
"searchEngine": false,
|
|
||||||
"messageBroker": false,
|
|
||||||
"serviceDiscoveryType": false,
|
|
||||||
"buildTool": "gradle",
|
|
||||||
"enableSwaggerCodegen": true,
|
|
||||||
"jwtSecretKey": "ZDFlMDUzODIzMTUzZDEwZjExN2E5ZjAzY2VhZmYzNDE1YjhlYWUxZGRhMGU3ODZiNjRkNjVlNzEwZjExYWY4YzczM2NlYzI5YWE1OTRkNWM0YThlYjZjZjA5Zjc5YWJkOTgzYjdhZjQxZWQyZGUyYjFlYjI5ZDE3NmE4M2UzYjQ=",
|
|
||||||
"clientFramework": "angularX",
|
|
||||||
"useSass": false,
|
|
||||||
"clientPackageManager": "npm",
|
|
||||||
"testFrameworks": ["cucumber"],
|
|
||||||
"jhiPrefix": "jhi",
|
|
||||||
"entitySuffix": "",
|
|
||||||
"dtoSuffix": "DTO",
|
|
||||||
"otherModules": [],
|
|
||||||
"enableTranslation": true,
|
|
||||||
"nativeLanguage": "de",
|
|
||||||
"languages": ["de", "en"]
|
|
||||||
}
|
|
||||||
}
|
|
10
Dockerfile
Normal file
10
Dockerfile
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# build using:
|
||||||
|
# docker build -t postgres-with-contrib:15.5-bookworm .
|
||||||
|
|
||||||
|
FROM postgres:15.5-bookworm
|
||||||
|
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y postgresql-contrib && \
|
||||||
|
apt-get clean
|
||||||
|
|
||||||
|
COPY etc/postgresql-log-slow-queries.conf /etc/postgresql/postgresql.conf
|
196
JHIPSTER.md
196
JHIPSTER.md
@ -1,196 +0,0 @@
|
|||||||
# hsadminNg
|
|
||||||
|
|
||||||
This application was generated using JHipster 5.8.2, you can find documentation and help at [https://www.jhipster.tech/documentation-archive/v5.8.2](https://www.jhipster.tech/documentation-archive/v5.8.2).
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
Before you can build this project, you must install and configure the following dependencies on your machine:
|
|
||||||
|
|
||||||
1. [Node.js][]: We use Node to run a development web server and build the project.
|
|
||||||
Depending on your system, you can install Node either from source or as a pre-packaged bundle.
|
|
||||||
|
|
||||||
After installing Node, you should be able to run the following command to install development tools.
|
|
||||||
You will only need to run this command when dependencies change in [package.json](package.json).
|
|
||||||
|
|
||||||
npm install
|
|
||||||
|
|
||||||
We use npm scripts and [Webpack][] as our build system.
|
|
||||||
|
|
||||||
Run the following commands in two separate terminals to create a blissful development experience where your browser
|
|
||||||
auto-refreshes when files change on your hard drive.
|
|
||||||
|
|
||||||
./gradlew
|
|
||||||
npm start
|
|
||||||
|
|
||||||
Npm is also used to manage CSS and JavaScript dependencies used in this application. You can upgrade dependencies by
|
|
||||||
specifying a newer version in [package.json](package.json). You can also run `npm update` and `npm install` to manage dependencies.
|
|
||||||
Add the `help` flag on any command to see how you can use it. For example, `npm help update`.
|
|
||||||
|
|
||||||
The `npm run` command will list all of the scripts available to run for this project.
|
|
||||||
|
|
||||||
### Service workers
|
|
||||||
|
|
||||||
Service workers are commented by default, to enable them please uncomment the following code.
|
|
||||||
|
|
||||||
- The service worker registering script in index.html
|
|
||||||
|
|
||||||
```html
|
|
||||||
<script>
|
|
||||||
if ('serviceWorker' in navigator) {
|
|
||||||
navigator.serviceWorker.register('./service-worker.js').then(function() {
|
|
||||||
console.log('Service Worker Registered');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
```
|
|
||||||
|
|
||||||
Note: workbox creates the respective service worker and dynamically generate the `service-worker.js`
|
|
||||||
|
|
||||||
### Managing dependencies
|
|
||||||
|
|
||||||
For example, to add [Leaflet][] library as a runtime dependency of your application, you would run following command:
|
|
||||||
|
|
||||||
npm install --save --save-exact leaflet
|
|
||||||
|
|
||||||
To benefit from TypeScript type definitions from [DefinitelyTyped][] repository in development, you would run following command:
|
|
||||||
|
|
||||||
npm install --save-dev --save-exact @types/leaflet
|
|
||||||
|
|
||||||
Then you would import the JS and CSS files specified in library's installation instructions so that [Webpack][] knows about them:
|
|
||||||
Edit [src/main/webapp/app/vendor.ts](src/main/webapp/app/vendor.ts) file:
|
|
||||||
|
|
||||||
```
|
|
||||||
import 'leaflet/dist/leaflet.js';
|
|
||||||
```
|
|
||||||
|
|
||||||
Edit [src/main/webapp/content/css/vendor.css](src/main/webapp/content/css/vendor.css) file:
|
|
||||||
|
|
||||||
```
|
|
||||||
@import '~leaflet/dist/leaflet.css';
|
|
||||||
```
|
|
||||||
|
|
||||||
Note: there are still few other things remaining to do for Leaflet that we won't detail here.
|
|
||||||
|
|
||||||
For further instructions on how to develop with JHipster, have a look at [Using JHipster in development][].
|
|
||||||
|
|
||||||
### Using angular-cli
|
|
||||||
|
|
||||||
You can also use [Angular CLI][] to generate some custom client code.
|
|
||||||
|
|
||||||
For example, the following command:
|
|
||||||
|
|
||||||
ng generate component my-component
|
|
||||||
|
|
||||||
will generate few files:
|
|
||||||
|
|
||||||
create src/main/webapp/app/my-component/my-component.component.html
|
|
||||||
create src/main/webapp/app/my-component/my-component.component.ts
|
|
||||||
update src/main/webapp/app/app.module.ts
|
|
||||||
|
|
||||||
### Doing API-First development using openapi-generator
|
|
||||||
|
|
||||||
[OpenAPI-Generator]() is configured for this application. You can generate API code from the `src/main/resources/swagger/api.yml` definition file by running:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./gradlew openApiGenerate
|
|
||||||
```
|
|
||||||
|
|
||||||
Then implements the generated delegate classes with `@Service` classes.
|
|
||||||
|
|
||||||
To edit the `api.yml` definition file, you can use a tool such as [Swagger-Editor](). Start a local instance of the swagger-editor using docker by running: `docker-compose -f src/main/docker/swagger-editor.yml up -d`. The editor will then be reachable at [http://localhost:7742](http://localhost:7742).
|
|
||||||
|
|
||||||
Refer to [Doing API-First development][] for more details.
|
|
||||||
|
|
||||||
## Building for production
|
|
||||||
|
|
||||||
To optimize the hsadminNg application for production, run:
|
|
||||||
|
|
||||||
./gradlew -Pprod clean bootWar
|
|
||||||
|
|
||||||
This will concatenate and minify the client CSS and JavaScript files. It will also modify `index.html` so it references these new files.
|
|
||||||
To ensure everything worked, run:
|
|
||||||
|
|
||||||
java -jar build/libs/*.war
|
|
||||||
|
|
||||||
Then navigate to [http://localhost:8080](http://localhost:8080) in your browser.
|
|
||||||
|
|
||||||
Refer to [Using JHipster in production][] for more details.
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
To launch your application's tests, run:
|
|
||||||
|
|
||||||
./gradlew test
|
|
||||||
|
|
||||||
### Client tests
|
|
||||||
|
|
||||||
Unit tests are run by [Jest][] and written with [Jasmine][]. They're located in [src/test/javascript/](src/test/javascript/) and can be run with:
|
|
||||||
|
|
||||||
npm test
|
|
||||||
|
|
||||||
For more information, refer to the [Running tests page][].
|
|
||||||
|
|
||||||
### Code quality
|
|
||||||
|
|
||||||
Sonar is used to analyse code quality. You can start a local Sonar server (accessible on http://localhost:9001) with:
|
|
||||||
|
|
||||||
```
|
|
||||||
docker-compose -f src/main/docker/sonar.yml up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
Then, run a Sonar analysis:
|
|
||||||
|
|
||||||
```
|
|
||||||
./gradlew -Pprod clean test sonarqube
|
|
||||||
```
|
|
||||||
|
|
||||||
For more information, refer to the [Code quality page][].
|
|
||||||
|
|
||||||
## Using Docker to simplify development (optional)
|
|
||||||
|
|
||||||
You can use Docker to improve your JHipster development experience. A number of docker-compose configuration are available in the [src/main/docker](src/main/docker) folder to launch required third party services.
|
|
||||||
|
|
||||||
For example, to start a postgresql database in a docker container, run:
|
|
||||||
|
|
||||||
docker-compose -f src/main/docker/postgresql.yml up -d
|
|
||||||
|
|
||||||
To stop it and remove the container, run:
|
|
||||||
|
|
||||||
docker-compose -f src/main/docker/postgresql.yml down
|
|
||||||
|
|
||||||
You can also fully dockerize your application and all the services that it depends on.
|
|
||||||
To achieve this, first build a docker image of your app by running:
|
|
||||||
|
|
||||||
./gradlew bootWar -Pprod jibDockerBuild
|
|
||||||
|
|
||||||
Then run:
|
|
||||||
|
|
||||||
docker-compose -f src/main/docker/app.yml up -d
|
|
||||||
|
|
||||||
For more information refer to [Using Docker and Docker-Compose][], this page also contains information on the docker-compose sub-generator (`jhipster docker-compose`), which is able to generate docker configurations for one or several JHipster applications.
|
|
||||||
|
|
||||||
## Continuous Integration (optional)
|
|
||||||
|
|
||||||
To configure CI for your project, run the ci-cd sub-generator (`jhipster ci-cd`), this will let you generate configuration files for a number of Continuous Integration systems. Consult the [Setting up Continuous Integration][] page for more information.
|
|
||||||
|
|
||||||
[jhipster homepage and latest documentation]: https://www.jhipster.tech
|
|
||||||
[jhipster 5.8.2 archive]: https://www.jhipster.tech/documentation-archive/v5.8.2
|
|
||||||
[using jhipster in development]: https://www.jhipster.tech/documentation-archive/v5.8.2/development/
|
|
||||||
[using docker and docker-compose]: https://www.jhipster.tech/documentation-archive/v5.8.2/docker-compose
|
|
||||||
[using jhipster in production]: https://www.jhipster.tech/documentation-archive/v5.8.2/production/
|
|
||||||
[running tests page]: https://www.jhipster.tech/documentation-archive/v5.8.2/running-tests/
|
|
||||||
[code quality page]: https://www.jhipster.tech/documentation-archive/v5.8.2/code-quality/
|
|
||||||
[setting up continuous integration]: https://www.jhipster.tech/documentation-archive/v5.8.2/setting-up-ci/
|
|
||||||
[node.js]: https://nodejs.org/
|
|
||||||
[yarn]: https://yarnpkg.org/
|
|
||||||
[webpack]: https://webpack.github.io/
|
|
||||||
[angular cli]: https://cli.angular.io/
|
|
||||||
[browsersync]: http://www.browsersync.io/
|
|
||||||
[jest]: https://facebook.github.io/jest/
|
|
||||||
[jasmine]: http://jasmine.github.io/2.0/introduction.html
|
|
||||||
[protractor]: https://angular.github.io/protractor/
|
|
||||||
[leaflet]: http://leafletjs.com/
|
|
||||||
[definitelytyped]: http://definitelytyped.org/
|
|
||||||
[openapi-generator]: https://openapi-generator.tech
|
|
||||||
[swagger-editor]: http://editor.swagger.io
|
|
||||||
[doing api-first development]: https://www.jhipster.tech/documentation-archive/v5.8.2/doing-api-first-development/
|
|
103
Jenkinsfile
vendored
103
Jenkinsfile
vendored
@ -1,47 +1,84 @@
|
|||||||
#!/usr/bin/env groovy
|
pipeline {
|
||||||
|
agent {
|
||||||
|
dockerfile {
|
||||||
|
filename 'etc/jenkinsAgent.Dockerfile'
|
||||||
|
// additionalBuildArgs ...
|
||||||
|
args '--network=bridge --user root -v $PWD:$PWD -v /var/run/docker.sock:/var/run/docker.sock --group-add 984'
|
||||||
|
reuseNode true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
node {
|
environment {
|
||||||
withEnv(["PATH=$HOME/bin:$PATH"]) {
|
DOCKER_HOST = 'unix:///var/run/docker.sock'
|
||||||
stage('checkout') {
|
HSADMINNG_POSTGRES_ADMIN_USERNAME = 'admin'
|
||||||
|
HSADMINNG_POSTGRES_RESTRICTED_USERNAME = 'restricted'
|
||||||
|
HSADMINNG_MIGRATION_DATA_PATH = 'migration'
|
||||||
|
}
|
||||||
|
|
||||||
|
triggers {
|
||||||
|
pollSCM('H/1 * * * *')
|
||||||
|
}
|
||||||
|
|
||||||
|
stages {
|
||||||
|
stage('Checkout') {
|
||||||
|
steps {
|
||||||
checkout scm
|
checkout scm
|
||||||
}
|
}
|
||||||
|
|
||||||
stage('check java') {
|
|
||||||
sh "java -version"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
stage('clean') {
|
stage ('Compile') {
|
||||||
sh "chmod +x gradlew"
|
steps {
|
||||||
sh "./gradlew clean --no-daemon"
|
sh './gradlew clean processSpring compileJava compileTestJava --no-daemon'
|
||||||
}
|
|
||||||
|
|
||||||
stage('npm install') {
|
|
||||||
sh "./gradlew npm_install -PnodeInstall --no-daemon"
|
|
||||||
}
|
|
||||||
|
|
||||||
stage('backend tests') {
|
|
||||||
try {
|
|
||||||
sh "./gradlew test -PnodeInstall --no-daemon"
|
|
||||||
} catch (err) {
|
|
||||||
throw err
|
|
||||||
} finally {
|
|
||||||
junit '**/build/**/TEST-*.xml'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stage('frontend tests') {
|
stage ('Tests') {
|
||||||
try {
|
parallel {
|
||||||
sh "./gradlew npm_run_test -PnodeInstall --no-daemon"
|
stage('Unit-/Integration/Acceptance-Tests') {
|
||||||
} catch (err) {
|
steps {
|
||||||
throw err
|
sh './gradlew check --no-daemon -x pitest -x dependencyCheckAnalyze -x importOfficeData -x importHostingAssets'
|
||||||
} finally {
|
}
|
||||||
junit '**/build/test-results/TESTS-*.xml'
|
}
|
||||||
|
stage('Import-Tests') {
|
||||||
|
steps {
|
||||||
|
sh './gradlew importOfficeData importHostingAssets --no-daemon'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stage ('Scenario-Tests') {
|
||||||
|
steps {
|
||||||
|
sh './gradlew scenarioTests --no-daemon'
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stage('packaging') {
|
stage ('Check') {
|
||||||
sh "./gradlew bootWar -x test -Pprod -PnodeInstall --no-daemon"
|
steps {
|
||||||
archiveArtifacts artifacts: '**/build/libs/*.war', fingerprint: true
|
sh './gradlew check -x pitest -x dependencyCheckAnalyze --no-daemon'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
always {
|
||||||
|
// archive test results
|
||||||
|
junit 'build/test-results/test/*.xml'
|
||||||
|
|
||||||
|
// archive the JaCoCo coverage report in XML and HTML format
|
||||||
|
jacoco(
|
||||||
|
execPattern: 'build/jacoco/*.exec',
|
||||||
|
classPattern: 'build/classes/java/main',
|
||||||
|
sourcePattern: 'src/main/java'
|
||||||
|
)
|
||||||
|
|
||||||
|
// archive scenario-test reports in HTML format
|
||||||
|
sh '''
|
||||||
|
./gradlew convertMarkdownToHtml
|
||||||
|
'''
|
||||||
|
archiveArtifacts artifacts: 'doc/scenarios/*.html', allowEmptyArchive: true
|
||||||
|
|
||||||
|
// cleanup workspace
|
||||||
|
cleanWs()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
26
LICENSE.md
Normal file
26
LICENSE.md
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
=====================
|
||||||
|
|
||||||
|
Copyright ©2022 Michael Hönnig
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
796
README.md
796
README.md
@ -1,21 +1,797 @@
|
|||||||
= hsadminNg Development
|
# hsadminNg Development
|
||||||
|
|
||||||
== Setting up the Development Environment
|
This documents gives an overview of the development environment and tools.
|
||||||
|
For architecture consider the files in the `doc` and `adr` folder.
|
||||||
|
|
||||||
You'll often need to execute `./gradlew`, therefore we suggest to define this alias:
|
<!-- generated TOC begin: -->
|
||||||
|
- [Setting up the Development Environment](#setting-up-the-development-environment)
|
||||||
|
- [PostgreSQL Server](#postgresql-server)
|
||||||
|
- [Markdown](#markdown)
|
||||||
|
- [Render Markdown embedded PlantUML](#render-markdown-embedded-plantuml)
|
||||||
|
- [Render Markdown Embedded Mermaid Diagrams](#render-markdown-embedded-mermaid-diagrams)
|
||||||
|
- [IDE Specific Settings](#ide-specific-settings)
|
||||||
|
- [IntelliJ IDEA](#intellij-idea)
|
||||||
|
- [Other Tools](#other-tools)
|
||||||
|
- [Running the SQL files](#running-the-sql-files)
|
||||||
|
- [For RBAC](#for-rbac)
|
||||||
|
- [For Historization](#for-historization)
|
||||||
|
- [Coding Guidelines](#coding-guidelines)
|
||||||
|
- [Directory and Package Structure](#directory-and-package-structure)
|
||||||
|
- [General Directory Structure](#general-directory-structure)
|
||||||
|
- [Source Code Package Structure](#source-code-package-structure)
|
||||||
|
- [Run Tests from Command Line](#run-tests-from-command-line)
|
||||||
|
- [Spotless Code Formatting](#spotless-code-formatting)
|
||||||
|
- [JaCoCo Test Code Coverage Check](#jacoco-test-code-coverage-check)
|
||||||
|
- [PiTest Mutation Testing](#pitest-mutation-testing)
|
||||||
|
- [Remark](#remark)
|
||||||
|
- [OWASP Security Vulnerability Check](#owasp-security-vulnerability-check)
|
||||||
|
- [Dependency-License-Compatibility](#dependency-license-compatibility)
|
||||||
|
- [Dependency Version Upgrade](#dependency-version-upgrade)
|
||||||
|
- [How To ...](#how-to-...)
|
||||||
|
- [How to Configure .pgpass for the Default PostgreSQL Database?](#how-to-configure-.pgpass-for-the-default-postgresql-database?)
|
||||||
|
- [How to Run the Tests Against a Local User-Space Podman Daemon?](#how-to-run-the-tests-against-a-local-user-space-podman-daemon?)
|
||||||
|
- [Install and Run Podman](#install-and-run-podman)
|
||||||
|
- [Use the Command Line to Run the Tests Against the Podman Daemon ](#use-the-command-line-to-run-the-tests-against-the-podman-daemon-)
|
||||||
|
- [Use IntelliJ IDEA Run the Tests Against the Podman Daemon](#use-intellij-idea-run-the-tests-against-the-podman-daemon)
|
||||||
|
- [~/.testcontainers.properties](#~/.testcontainers.properties)
|
||||||
|
- [How to Run the Tests Against a Remote Podman or Docker Daemon?](#how-to-run-the-tests-against-a-remote-podman-or-docker-daemon?)
|
||||||
|
- [How to Run the Application on a Different Port?](#how-to-run-the-application-on-a-different-port?)
|
||||||
|
- [How to Use a Persistent Database for Integration Tests?](#how-to-use-a-persistent-database-for-integration-tests?)
|
||||||
|
- [How to Amend Liquibase SQL Changesets?](#how-to-amend-liquibase-sql-changesets?)
|
||||||
|
- [How to Re-Generate Spring-Controller-Interfaces from OpenAPI specs?](#how-to-re-generate-spring-controller-interfaces-from-openapi-specs?)
|
||||||
|
- [How to Generate Database Table Diagrams?](#how-to-generate-database-table-diagrams?)
|
||||||
|
- [Further Documentation](#further-documentation)
|
||||||
|
<!-- generated TOC end. -->
|
||||||
|
|
||||||
alias gw='./gradlew'
|
## Setting up the Development Environment
|
||||||
|
|
||||||
== Building the Application with Test Execution
|
All instructions assume that you're using a current _Linux_ or _MacOS_ operating system.
|
||||||
|
Everything is tested on _Ubuntu Linux 22.04_ and _MacOS Monterey (12.4)_.
|
||||||
|
|
||||||
gw build
|
To be able to build and run the Java Spring Boot application, you need the following tools:
|
||||||
|
|
||||||
== Starting the Application
|
- Docker 20.x (on MacOS you also need *Docker Desktop* or similar) or Podman
|
||||||
|
- optionally: PostgreSQL Server 15.5-bookworm
|
||||||
|
(see instructions below to install and run in Docker)
|
||||||
|
- The matching Java JDK at will be automatically installed by Gradle toolchain support to `~/.gradle/jdks/`.
|
||||||
|
- You also might need an IDE (e.g. *IntelliJ IDEA* or *Eclipse* or *VS Code* with *[STS](https://spring.io/tools)* and a GUI Frontend for *PostgreSQL* like *Postbird*.
|
||||||
|
|
||||||
Either simply:
|
If you have at least Docker and the Java JDK installed in appropriate versions and in your `PATH`, then you can start like this:
|
||||||
|
|
||||||
|
cd your-hsadmin-ng-directory
|
||||||
|
|
||||||
|
source .aliases # creates some comfortable bash aliases, e.g. 'gw'='./gradlew'
|
||||||
|
gw # initially downloads the configured Gradle version into the project
|
||||||
|
|
||||||
|
gw test # compiles and runs unit- and integration-tests
|
||||||
|
|
||||||
|
# if the container has not been built yet, run this:
|
||||||
|
pg-sql-run # downloads + runs PostgreSQL in a Docker container on localhost:5432
|
||||||
|
# if the container has been built already, run this:
|
||||||
|
pg-sql-start
|
||||||
|
|
||||||
|
gw bootRun # compiles and runs the application on localhost:8080
|
||||||
|
|
||||||
|
# the following command should reply with "pong":
|
||||||
|
curl http://localhost:8080/api/ping
|
||||||
|
|
||||||
|
# the following command should return a JSON array with just all customers:
|
||||||
|
curl \
|
||||||
|
-H 'current-subject: superuser-alex@hostsharing.net' \
|
||||||
|
http://localhost:8080/api/test/customers
|
||||||
|
|
||||||
|
# the following command should return a JSON array with just all packages visible for the admin of the customer yyy:
|
||||||
|
curl \
|
||||||
|
-H 'current-subject: superuser-alex@hostsharing.net' -H 'assumed-roles: rbactest.customer#yyy:ADMIN' \
|
||||||
|
http://localhost:8080/api/test/packages
|
||||||
|
|
||||||
|
# add a new customer
|
||||||
|
curl \
|
||||||
|
-H 'current-subject: superuser-alex@hostsharing.net' -H "Content-Type: application/json" \
|
||||||
|
-d '{ "prefix":"ttt", "reference":80001, "adminUserName":"admin@ttt.example.com" }' \
|
||||||
|
-X POST http://localhost:8080/api/test/customers
|
||||||
|
|
||||||
|
If you wonder who 'superuser-alex@hostsharing.net' and 'superuser-fran@hostsharing.net' are and where the data comes from:
|
||||||
|
Mike and Sven are just example global admin accounts as part of the example data which is automatically inserted in Testcontainers and Development environments.
|
||||||
|
Also try for example 'admin@xxx.example.com' or 'unknown@example.org'.
|
||||||
|
|
||||||
|
If you want a formatted JSON output, you can pipe the result to `jq` or similar.
|
||||||
|
|
||||||
|
And to see the full, currently implemented, API, open http://localhost:8080/swagger-ui/index.html.
|
||||||
|
|
||||||
|
If you still need to install some of these tools, find some hints in the next chapters.
|
||||||
|
|
||||||
|
|
||||||
|
### PostgreSQL Server
|
||||||
|
|
||||||
|
You could use any PostgreSQL Server (version 15) installed on your machine.
|
||||||
|
You might amend the port and user settings in `src/main/resources/application.yml`, though.
|
||||||
|
|
||||||
|
But the easiest way to run PostgreSQL is via Docker.
|
||||||
|
|
||||||
|
Initially, pull an image compatible to current PostgreSQL version of Hostsharing:
|
||||||
|
|
||||||
|
docker pull postgres:15.5-bookworm
|
||||||
|
|
||||||
|
<big>**⚠**</big>
|
||||||
|
If we switch the version, please also amend the documentation as well as the aliases file. Thanks!
|
||||||
|
|
||||||
|
Create and run a container with the given PostgreSQL version:
|
||||||
|
|
||||||
|
docker run --name hsadmin-ng-postgres -e POSTGRES_PASSWORD=password -p 5432:5432 -d postgres:15.5-bookworm
|
||||||
|
|
||||||
|
# or via alias:
|
||||||
|
pg-sql-run
|
||||||
|
|
||||||
|
To check if the PostgreSQL container is running, the following command should list a container with the name "hsadmin-ng-postgres":
|
||||||
|
|
||||||
|
docker container ls
|
||||||
|
|
||||||
|
Stop the PostgreSQL container:
|
||||||
|
|
||||||
|
docker stop hsadmin-ng-postgres
|
||||||
|
# or via alias: pg-sql-stop
|
||||||
|
|
||||||
|
Start the PostgreSQL container again:
|
||||||
|
|
||||||
|
docker container start hsadmin-ng-postgres
|
||||||
|
# or via alias: pg-sql-start
|
||||||
|
|
||||||
|
Remove the PostgreSQL container:
|
||||||
|
|
||||||
|
docker rm hsadmin-ng-postgres
|
||||||
|
|
||||||
|
# or via alias:
|
||||||
|
pg-sql-remove
|
||||||
|
|
||||||
|
To reset to a clean database, use:
|
||||||
|
|
||||||
|
pg-sql-stop; pg-sql-remove; pg-sql-run
|
||||||
|
|
||||||
|
# or via alias:
|
||||||
|
pg-sql-reset
|
||||||
|
|
||||||
|
After the PostgreSQL container is removed, you need to create it again as shown in "Create and run ..." above.
|
||||||
|
|
||||||
|
Given the container is running, to create a backup in ~/backup, run:
|
||||||
|
|
||||||
|
docker exec -i hsadmin-ng-postgres /usr/bin/pg_dump --clean --create -U postgres postgres | gzip -9 > ~/backup/hsadmin-ng-postgres.sql.gz
|
||||||
|
|
||||||
|
# or via alias:
|
||||||
|
pg-sql-backup >~/backup/hsadmin-ng-postgres.sql.gz
|
||||||
|
|
||||||
|
|
||||||
|
Again, given the container is running, to restore the backup from ~/backup, run:
|
||||||
|
|
||||||
|
gunzip --stdout --keep ~/backup/hsadmin-ng-postgres.sql.gz | docker exec -i hsadmin-ng-postgres psql -U postgres -d postgres
|
||||||
|
|
||||||
|
# or via alias:
|
||||||
|
pg-sql-restore <~/backup/hsadmin-ng-postgres.sql.gz
|
||||||
|
|
||||||
|
|
||||||
|
### Markdown
|
||||||
|
|
||||||
|
To generate the TOC (Table of Contents), a little bash script from a
|
||||||
|
[Blog Article](https://medium.com/@acrodriguez/one-liner-to-generate-a-markdown-toc-f5292112fd14) was used.
|
||||||
|
|
||||||
|
Given this is in PATH as `md-toc`, use:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
md-toc <README.md 2 4 | cut -c5-'
|
||||||
|
```
|
||||||
|
|
||||||
|
To render the Markdown files, especially to watch embedded PlantUML diagrams, you can use one of the following methods:
|
||||||
|
|
||||||
|
#### Render Markdown embedded PlantUML
|
||||||
|
|
||||||
|
Can you see the following diagram right in your IDE?
|
||||||
|
I mean a real graphic diagram, not just some markup code.
|
||||||
|
|
||||||
|
```plantuml
|
||||||
|
@startuml
|
||||||
|
me -> you: Can you see this diagram?
|
||||||
|
you -> me: Sorry, I don't :-(
|
||||||
|
me -> you: Install some tooling!
|
||||||
|
@enduml
|
||||||
|
```
|
||||||
|
|
||||||
|
If not, you need to install some tooling.
|
||||||
|
|
||||||
|
##### for IntelliJ IDEA (or derived products)
|
||||||
|
|
||||||
|
You just need the bundled Markdown plugin enabled and install and activate the PlantUML plugin in its [settings](jetbrains://idea/settings?name=Languages+%26+Frameworks--Markdown).
|
||||||
|
|
||||||
|
You might also need to install Graphviz on your operating system.
|
||||||
|
For Debian-based Linux systems this might work:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo apt install graphviz
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
##### Ubuntu Linux command line
|
||||||
|
|
||||||
|
1. Install Pandoc with some extra libraries:
|
||||||
|
```shell
|
||||||
|
sudo apt-get install pandoc texlive-latex-base texlive-fonts-recommended texlive-extra-utils texlive-latex-extra pandoc-plantuml-filter
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Install mermaid-filter, e.g. this way:
|
||||||
|
```shell
|
||||||
|
npm install -g mermaid-filter
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Run Pandoc to generate a PDF from a Markdown file with PlantUML and Mermaid diagrams:
|
||||||
|
```shell
|
||||||
|
pandoc --filter mermaid-filter --filter pandoc-plantuml rbac.md -o rbac.pdf
|
||||||
|
```
|
||||||
|
|
||||||
|
##### for other IDEs / operating systems
|
||||||
|
|
||||||
|
If you have figured out how it works, please add instructions above this section.
|
||||||
|
|
||||||
|
#### Render Markdown Embedded Mermaid Diagrams
|
||||||
|
|
||||||
|
The source of RBAC role diagrams are much easier to read with Mermaid than with PlantUML or GraphViz, that's also the main reason Mermaid is used.
|
||||||
|
|
||||||
|
Can you see the following diagram right in your IDE?
|
||||||
|
I mean a real graphic diagram, not just some markup code.
|
||||||
|
@startuml
|
||||||
|
me -> you: Can you see this diagram?
|
||||||
|
you -> me: Sorry, I don't :-(
|
||||||
|
me -> you: Install some tooling!
|
||||||
|
@enduml
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD;
|
||||||
|
A[Can you see this diagram?];
|
||||||
|
A --> yes;
|
||||||
|
A --> no;
|
||||||
|
no --> F[Follow the instructions below!]
|
||||||
|
F --> yes
|
||||||
|
yes --> E[Then everything is fine.]
|
||||||
|
```
|
||||||
|
|
||||||
|
If not, you need to install some tooling.
|
||||||
|
|
||||||
|
##### for IntelliJ IDEA (or derived products)
|
||||||
|
|
||||||
|
1. Activate the bundled Jebrains Markdown PlantUML Extension via
|
||||||
|
[File | Settings | Languages & Frameworks | Markdown](jetbrains://idea/settings?name=Languages+%26+Frameworks--Markdown)
|
||||||
|
2. Install the Jetbrains Mermaid plugin: https://plugins.jetbrains.com/plugin/20146-mermaid, it also works embedded in Markdown files.
|
||||||
|
|
||||||
|
Now the above diagram should be rendered.
|
||||||
|
|
||||||
|
##### for other IDEs / command-line / operating systems
|
||||||
|
|
||||||
|
If you have figured out how it works, please add instructions above this section.
|
||||||
|
|
||||||
|
### IDE Specific Settings
|
||||||
|
|
||||||
|
#### IntelliJ IDEA
|
||||||
|
|
||||||
|
##### Build Settings
|
||||||
|
|
||||||
|
Go to [Gradle Settings}(jetbrains://idea/settings?name=Build%2C+Execution%2C+Deployment--Build+Tools--Gradle) and select "Build and run using" and "Run tests using" both to "gradle".
|
||||||
|
Otherwise, settings from `build.gradle`, like compiler arguments, are not applied when compiling through *IntelliJ IDEA*.
|
||||||
|
|
||||||
|
##### Annotation Processor
|
||||||
|
|
||||||
|
Go to [Annotations Processors](jetbrains://idea/settings?name=Build%2C+Execution%2C+Deployment--Compiler--Annotation+Processors) and activate annotation processing.
|
||||||
|
Otherwise, *IntelliJ IDEA* can't see *Lombok* generated classes
|
||||||
|
and will show false errors (missing identifiers).
|
||||||
|
|
||||||
|
|
||||||
|
##### Suggested Plugins
|
||||||
|
|
||||||
|
- [Jetbrains Mermaid Integration](https://plugins.jetbrains.com/plugin/20146-mermaid)
|
||||||
|
- [Vojtěch Krása PlantUML Integration](https://plugins.jetbrains.com/plugin/7017-plantuml-integration)
|
||||||
|
|
||||||
|
### Other Tools
|
||||||
|
|
||||||
|
**jq**: a JSON formatter.
|
||||||
|
On _Debian_'oid systems you can install it with `sudo apt-get install jq`.
|
||||||
|
On _MacOS_ you can install it with `brew install jq`, given you have _brew_ installed.
|
||||||
|
|
||||||
|
## Running the SQL files
|
||||||
|
|
||||||
|
### For RBAC
|
||||||
|
|
||||||
|
The Schema is automatically created via *Liquibase*, a database migration library.
|
||||||
|
Currently, also some test data is automatically created.
|
||||||
|
|
||||||
|
To increase the amount of test data, increase the number of generated customers in `2022-07-28-051-hs-customer.sql` and run that
|
||||||
|
|
||||||
|
If you already have data, e.g. for customers 0..999 (thus with reference numbers 10000..10999) and want to add another 1000 customers, amend the for loop to 1000...1999 and also uncomment and amend the `CONTINUE WHEN` or `WHERE` conditions in the other test data generators, using the first new customer reference number (in the example that's 11000).
|
||||||
|
|
||||||
|
### For Historization
|
||||||
|
|
||||||
|
The historization is not yet integrated into the *Liquibase*-scripts.
|
||||||
|
You can explore the prototype as follows:
|
||||||
|
|
||||||
|
- start with an empty database
|
||||||
|
(the example tables are currently not compatible with RBAC),
|
||||||
|
- then run `historization.sql` in the database,
|
||||||
|
- finally run `examples.sql` in the database.
|
||||||
|
|
||||||
|
## Coding Guidelines
|
||||||
|
|
||||||
|
### Directory and Package Structure
|
||||||
|
|
||||||
|
#### General Directory Structure
|
||||||
|
|
||||||
|
`.aliases`
|
||||||
|
Shell-aliases for common tasks.
|
||||||
|
|
||||||
|
`build/`
|
||||||
|
Output directory for gradle build results. Ignored by git.
|
||||||
|
|
||||||
|
`build.gradle`
|
||||||
|
Gradle build-file. Contains dependencies and build configurations.
|
||||||
|
|
||||||
|
`doc/`
|
||||||
|
Contains project documentation.
|
||||||
|
|
||||||
|
`.editorconfig`
|
||||||
|
Rules for indentation etc. considered by many code editors.
|
||||||
|
|
||||||
|
`etc/`
|
||||||
|
Miscellaneous configurations, as long as these don't need to be in the rood directory.
|
||||||
|
|
||||||
|
`.git/`
|
||||||
|
Git repository. Do not temper with this!
|
||||||
|
|
||||||
|
`.gitattributes`
|
||||||
|
Git configurations regarding text file format conversion between operating systems.
|
||||||
|
|
||||||
|
`.gitignore`
|
||||||
|
Git configuration regarding which files and directories should be ignored (not checked in).
|
||||||
|
|
||||||
|
`.gradle/`
|
||||||
|
Config files created by `gradle wrapper`. Ignored by git.
|
||||||
|
|
||||||
|
`gradle/`
|
||||||
|
The gradle distribution downloaded by `gradle wrapper`. Ignored by git.
|
||||||
|
|
||||||
|
`gradlew` and `gradlew.bat` use these batches to run gradle for builds etc.
|
||||||
|
|
||||||
|
`.idea/` (optional)
|
||||||
|
Config and cache files created by *IntelliJ IDEA*. Ignore by git.
|
||||||
|
|
||||||
|
`LICENSE.md`
|
||||||
|
Contains the license used for this software.
|
||||||
|
|
||||||
|
`out/` (optional)
|
||||||
|
Build output created by *IntelliJ IDEA". Ignored by git.
|
||||||
|
|
||||||
|
`README.md`
|
||||||
|
Contains an overview about how to build the project and the used tools.
|
||||||
|
|
||||||
|
`.run/` (optional)
|
||||||
|
Created by *IntelliJ IDEA* to contain run and debug configurations.
|
||||||
|
|
||||||
|
`settings.gradle`
|
||||||
|
Configuration file for gradle.
|
||||||
|
|
||||||
|
`sql/`
|
||||||
|
Contains SQL scripts for experiments and useful tasks.
|
||||||
|
Most of this will sooner or later be moved to Liquibase-scripts.
|
||||||
|
|
||||||
|
`src/`
|
||||||
|
The actual source-code, see [Source Code Package Structure](#source-code-package-structure) for details.
|
||||||
|
|
||||||
|
`tools/`
|
||||||
|
Some shell-scripts to useful tasks.
|
||||||
|
|
||||||
|
|
||||||
|
#### Source Code Package Structure
|
||||||
|
|
||||||
|
For the source code itself, the general standard Java directory structure is used, where productive and test code are separated like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
src
|
||||||
|
main/
|
||||||
|
java/
|
||||||
|
net.hostsharing.hasadminng/
|
||||||
|
resources/
|
||||||
|
|
||||||
|
test/
|
||||||
|
java/
|
||||||
|
net.hostsharing.hasadminng/
|
||||||
|
resources/
|
||||||
|
```
|
||||||
|
|
||||||
|
The Java package structure below contains:
|
||||||
|
|
||||||
|
- config and global (utility) packages,
|
||||||
|
these should not access any other packages within the project
|
||||||
|
- rbac, containing all packages related to the RBAC subsystem
|
||||||
|
- hs, containing Hostsharing business object related packages
|
||||||
|
|
||||||
|
Underneath of rbac and hs, the structure is business oriented, NOT technical / layer -oriented.
|
||||||
|
|
||||||
|
Some of these rules are checked with *ArchUnit* unit tests.
|
||||||
|
|
||||||
|
|
||||||
|
### Run Tests from Command Line
|
||||||
|
|
||||||
|
Run all tests which have not yet been passed with the current source code:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
gw test
|
||||||
|
```
|
||||||
|
|
||||||
|
Force running all tests:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
gw cleanTest test
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Spotless Code Formatting
|
||||||
|
|
||||||
|
Code formatting for Java is checked via *spotless*.
|
||||||
|
The formatting style can be checked with this command:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
gw spotlessCheck
|
||||||
|
```
|
||||||
|
|
||||||
|
This task is also included in `gw build` and `gw check`.
|
||||||
|
|
||||||
|
To apply formatting rules, use:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
gw spotlessApply
|
||||||
|
```
|
||||||
|
|
||||||
|
### JaCoCo Test Code Coverage Check
|
||||||
|
|
||||||
|
This project uses the JaCoCo test code coverage report with limit checks.
|
||||||
|
It can be executed with:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
gw jacocoTestReport
|
||||||
|
```
|
||||||
|
|
||||||
|
This task is also automatically run after `gw test`.
|
||||||
|
It is configured in [build.gradle](build.gradle).
|
||||||
|
|
||||||
|
A report is generated under [build/reports/jacoco/tests/test/index.html](./build/reports/jacoco/test/html/index.html).
|
||||||
|
|
||||||
|
Additionally, quality limits are checked via:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
gw jacocoTestCoverageVerification
|
||||||
|
```
|
||||||
|
|
||||||
|
This task is also executed as part of `gw check`.
|
||||||
|
|
||||||
|
|
||||||
|
### PiTest Mutation Testing
|
||||||
|
|
||||||
|
PiTest mutation testing is configured for unit tests.
|
||||||
|
It can be executed with:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
gw pitest
|
||||||
|
```
|
||||||
|
|
||||||
|
Classes to be scanned, tests to be executed and thresholds are configured in [build.gradle](build.gradle).
|
||||||
|
|
||||||
|
A report is generated under [build/reports/pitest/index.html](./build/reports/pitest/index.html).
|
||||||
|
A link to the report is also printed after the `pitest` run.
|
||||||
|
|
||||||
|
This task is also executed as part of `gw check`.
|
||||||
|
|
||||||
|
#### Remark
|
||||||
|
|
||||||
|
In this project, there is little business logic in *Java* code;
|
||||||
|
most business code is in *plsql*
|
||||||
|
and *Java* ist mostly used for mapping REST calls to database queries.
|
||||||
|
This mapping ist mostly done through *Spring* annotations and other implicit code.
|
||||||
|
|
||||||
|
Therefore, there are only few unit tests and thus mutation testing has limited value.
|
||||||
|
We'll see if this changes when the project progresses and more validations are added.
|
||||||
|
|
||||||
|
|
||||||
|
### OWASP Security Vulnerability Check
|
||||||
|
|
||||||
|
An OWASP security vulnerability is configured, but you need an API key.
|
||||||
|
Fetch it from https://nvd.nist.gov/developers/request-an-api-key.
|
||||||
|
|
||||||
|
Then add it to your `~/.gradle/gradle.properties` file:
|
||||||
|
|
||||||
|
```
|
||||||
|
OWASP_API_KEY=........-....-....-....-............
|
||||||
|
```
|
||||||
|
|
||||||
|
Now you can run the dependency vulnerability check:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
gw dependencyCheckUpdate
|
||||||
|
gw dependencyCheckAnalyze
|
||||||
|
```
|
||||||
|
|
||||||
|
This task is also included in `gw build` and `gw check`.
|
||||||
|
It is configured in [build.gradle](build.gradle).
|
||||||
|
|
||||||
|
Often vulnerability reports don't apply to our use cases.
|
||||||
|
Therefore, reports can be [suppressed](./etc/owasp-dependency-check-suppression.xml).
|
||||||
|
In case of suppression, a note must be added to explain why it does not apply to us.
|
||||||
|
|
||||||
|
See also: https://jeremylong.github.io/DependencyCheck/dependency-check-gradle/index.html.
|
||||||
|
|
||||||
|
### Dependency-License-Compatibility
|
||||||
|
|
||||||
|
The `gw check` phase depends on a dependency-license-compatibility check.
|
||||||
|
If any dependency violates the configured [list of allowed licenses](etc/allowed-licenses.json), the build will fail.
|
||||||
|
New licenses can be added to that list after a legal investigation.
|
||||||
|
|
||||||
|
<big>**⚠**</big>
|
||||||
|
*GPL* (*GNU General Public License*) is only allowed with classpath exception.
|
||||||
|
Do <u>not</u> use any dependencies under *GPL* without this exception,
|
||||||
|
except if these offer an alternative license which is allowed.
|
||||||
|
*LGPL* (*GNU <u>Library</u> General Public License*) is also allowed.
|
||||||
|
|
||||||
|
To run just the dependency-license-compatibility check, use:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
gw checkLicense
|
||||||
|
```
|
||||||
|
|
||||||
|
If the check fails, a report can be found here: The generated license can be found under [dependencies-without-allowed-license.json](/build/reports/dependency-license/dependencies-without-allowed-license.json).
|
||||||
|
|
||||||
|
And to generate a report, use:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
gw generateLicenseReport
|
||||||
|
```
|
||||||
|
|
||||||
|
The generated license can be found here: [index.html](build/reports/dependency-license/index.html).
|
||||||
|
|
||||||
|
More information can be found on the [project's website](https://github.com/jk1/Gradle-License-Report).
|
||||||
|
|
||||||
|
### Dependency Version Upgrade
|
||||||
|
|
||||||
|
Dependency versions can be automatically upgraded to the latest available version:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
gw useLatestVersions
|
||||||
|
```
|
||||||
|
|
||||||
|
Afterward, `gw check` is automatically started.
|
||||||
|
Please only commit+push to master if the check run shows no errors.
|
||||||
|
|
||||||
|
More infos, e.g. on blacklists see on the [project's website](https://github.com/patrikerdes/gradle-use-latest-versions-plugin).
|
||||||
|
|
||||||
|
|
||||||
|
## Biggest Flaws in our Architecture
|
||||||
|
|
||||||
|
### The RBAC System is too Complicated
|
||||||
|
|
||||||
|
Now, where we have a better experience with what we really need from the RBAC system, we have learned
|
||||||
|
that and creates too many (grant- and role-) rows and too even tables which could be avoided completely.
|
||||||
|
|
||||||
|
The basic idea is always to always have a fixed set of ordered role-types which apply for all DB-tables under RBAC,
|
||||||
|
e.g. OWNER>ADMIN>AGENT\[>PROXY?\]>TENENT>REFERRER.
|
||||||
|
Grants between these for the same DB-row would be implicit by order comparision.
|
||||||
|
This way we would get rid of all explicit grants within the same DB-row
|
||||||
|
and would not need the `rbac.role` table anymore.
|
||||||
|
We would also reduce the depth of the expensive recursive CTE-query.
|
||||||
|
|
||||||
|
This has to be explored further.
|
||||||
|
For now, we just keep it in mind and
|
||||||
|
|
||||||
|
### The Mapper is Error-Prone
|
||||||
|
|
||||||
|
Where `org.modelmapper.ModelMapper` reduces bloat-code a lot and has some nice features about recursive data-structure mappings,
|
||||||
|
it often causes strange errors which are hard to fix.
|
||||||
|
E.g. the uuid of the target main object is often taken from an uuid of a sub-subject.
|
||||||
|
(For now, use `StrictMapper` to avoid this, for the case it happens.)
|
||||||
|
|
||||||
|
|
||||||
|
## How To ...
|
||||||
|
|
||||||
|
### How to Configure .pgpass for the Default PostgreSQL Database?
|
||||||
|
|
||||||
|
To access the default database schema as used during development, add this line to your `.pgpass` file in your users home directory:
|
||||||
|
|
||||||
|
```
|
||||||
|
localhost:5432:postgres:postgres:password
|
||||||
|
```
|
||||||
|
|
||||||
|
Amend host and port if necessary.
|
||||||
|
|
||||||
|
|
||||||
|
### How to Run the Tests Against a Local User-Space Podman Daemon?
|
||||||
|
|
||||||
|
Using a normal Docker daemon running as root has some security issues.
|
||||||
|
As an alternative, this chapter shows how you can run a Podman daemon in user-space.
|
||||||
|
|
||||||
|
#### Install and Run Podman
|
||||||
|
|
||||||
|
You can find directions in [this project on Github](https://stackoverflow.com/questions/71549856/testcontainers-with-podman-in-java-tests)
|
||||||
|
|
||||||
|
Summary for Debian-based Linux systems:
|
||||||
|
|
||||||
|
1. Install Podman, e.g. like this:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
sudo apt-get -y install podman
|
||||||
|
```
|
||||||
|
|
||||||
|
It is possible to move the storage directory to /tmp, e.g. to increase performance or to avoid issues with NFS mounted home directories:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cat .config/containers/storage.conf
|
||||||
|
[storage]
|
||||||
|
driver = "vfs"
|
||||||
|
graphRoot = "/tmp/containers/storage"
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Then start it like this:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
systemctl --user enable --now podman.socket
|
||||||
|
systemctl --user status podman.socket
|
||||||
|
ls -la /run/user/$UID/podman/podman.sock
|
||||||
|
```
|
||||||
|
|
||||||
|
These commands are also available in `.aliases` as `podman-start`.
|
||||||
|
|
||||||
|
|
||||||
|
#### Use the Command Line to Run the Tests Against the Podman Daemon
|
||||||
|
|
||||||
|
1. In a local shell. in which you want to run the tests, set some environment variables:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
export DOCKER_HOST="unix:///run/user/$UID/podman/podman.sock"
|
||||||
|
export TESTCONTAINERS_RYUK_DISABLED=true
|
||||||
|
```
|
||||||
|
|
||||||
|
These commands are also available in `.aliases` as `podman-use`.
|
||||||
|
|
||||||
|
Disabling RYUK is necessary, because it's not supported by Podman.
|
||||||
|
Supposedly this means that containers are not properly cleaned up after test runs,
|
||||||
|
but I could not see any remaining containers after test runs.
|
||||||
|
If we are running into problems with stale containers,
|
||||||
|
we need to register a shutdown-hook in the test source code.
|
||||||
|
|
||||||
|
2. Now You Can Run the Tests
|
||||||
|
|
||||||
|
```shell
|
||||||
|
gw test # gw is from the .aliases file
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Use IntelliJ IDEA Run the Tests Against the Podman Daemon
|
||||||
|
|
||||||
|
To run the tests against a Podman Daemon in IntelliJ IDEA too, you also need to set the environment variables `DOCKER_HOST` and `TESTCONTAINERS_RYUK_DISABLED` as show above.
|
||||||
|
This can either be done in the environment from which IDEA is started.
|
||||||
|
Or you can use the run config template for gradle to set these variables:
|
||||||
|
|
||||||
|
![IntelliJ IDEA Gradle Run Template](./doc/.images/intellij-idea-gradle-run-template.png)
|
||||||
|
|
||||||
|
If you already have Gradle run configs, you need to delete them, so they get re-created from the template.
|
||||||
|
Alternatively you need to add the environment varibles here too:
|
||||||
|
|
||||||
|
![IntelliJ IDEA Gradle Run Config Example](./doc/.images/intellij-idea-gradle-run-config.png)
|
||||||
|
|
||||||
|
Find more information [here](https://www.jetbrains.com/help/idea/run-debug-configuration.html).
|
||||||
|
|
||||||
|
|
||||||
|
#### ~/.testcontainers.properties
|
||||||
|
|
||||||
|
It should be possible to set these environment variables in `~/.testcontainers.properties`,
|
||||||
|
but it did not work so far.
|
||||||
|
Maybe a problem with quoting.
|
||||||
|
|
||||||
|
If you manage to make it work, please amend this documentation, thanks.
|
||||||
|
|
||||||
|
|
||||||
|
### How to Run the Tests Against a Remote Podman or Docker Daemon?
|
||||||
|
|
||||||
|
1. On the remote host, you need to have a Podman or Docker daemon running on a port accessible from the Internet.
|
||||||
|
Probably, you want to protect it with a VPN, but that's not part of this documentation.
|
||||||
|
|
||||||
|
e.g. to make Podman listen to a port, run this:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
podman system service -t 0 tcp:HOST:PORT # please replace HOST+PORT
|
||||||
|
```
|
||||||
|
|
||||||
|
2. In a local shell. in which you want to run the tests, set some environment variables:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
export DOCKER_HOST=tcp://HOST:PORT # please replace HOST+PORT again
|
||||||
|
export TESTCONTAINERS_RYUK_DISABLED=true # only for Podman
|
||||||
|
```
|
||||||
|
|
||||||
|
Regarding RYUK, see also in the directions for a locally running Podman, above.
|
||||||
|
|
||||||
|
3. Now you can run the tests:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
gw clean test # gw is from the .aliases file
|
||||||
|
```
|
||||||
|
|
||||||
|
For information about how to run the tests in IntelliJ IDEA against a remote Podman daemon, see also in the chapter above just with the HOST:PORT-based DOCKER_HOST.
|
||||||
|
|
||||||
|
### How to Run the Application on a Different Port?
|
||||||
|
|
||||||
|
By default, `gw bootRun` starts the application on port 8080.
|
||||||
|
|
||||||
|
This port can be changed in
|
||||||
|
[src/main/resources/application.yml](src/main/resources/application.yml) through the property `server.port`.
|
||||||
|
|
||||||
|
### How to Use a Persistent Database for Integration Tests?
|
||||||
|
|
||||||
|
Usually, the `DataJpaTest` integration tests run against a database in a temporary docker container.
|
||||||
|
As soon as the test ends, the database is gone; this might make debugging difficult.
|
||||||
|
|
||||||
|
Alternatively, a persistent database could be used by amending the
|
||||||
|
[resources/application.yml](src/main/resources/application.yml) through the property `spring.datasource.url` in [src/test/resources/application.yml](src/test/resources/application.yml) , e.g. to the JDBC-URL from [src/main/resources/application.yml](src/main/resources/application.yml).
|
||||||
|
|
||||||
|
If the persistent database and the temporary database show different results, one of these reasons could be the cause:
|
||||||
|
|
||||||
|
1. You might have some changesets only running in either context,
|
||||||
|
check the `context: ...` in the changeset control lines.
|
||||||
|
2. You might have changes in the database which interfere with the tests,
|
||||||
|
e.g. from a previous run of tests or manually applied.
|
||||||
|
It's best to run `pg-sql-reset && gw bootRun` before each test run, to have a clean database.
|
||||||
|
|
||||||
|
### How to Amend Liquibase SQL Changesets?
|
||||||
|
|
||||||
|
Liquibase changesets are meant to be immutable and based on each other.
|
||||||
|
That means, once a changeset is written, it never changes, not even a whitespace or comment.
|
||||||
|
Liquibase is a *database migration tool*, not a *database initialization tool*.
|
||||||
|
|
||||||
|
This, if you need to add change a table, stored procedure or whatever,
|
||||||
|
create a new changeset and apply `ALTER`, `DROP`, `CREATE OR REPLACE` or whatever SQL commands to perform your changes.
|
||||||
|
These changes will be automatically applied once the application starts up again.
|
||||||
|
This way, any staging or production database will always match the application code.
|
||||||
|
|
||||||
|
But, during initial development that can be a big hassle because the database structure changes a lot in that stage.
|
||||||
|
Also, the actual structure of the database won't be easily recognized anymore through lots of migration changesets.
|
||||||
|
|
||||||
|
Therefore, during initial development, it's good approach just to amend the existing changesets and delete the database:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
pg-sql-reset
|
||||||
gw bootRun
|
gw bootRun
|
||||||
|
```
|
||||||
|
|
||||||
or with a specific port:
|
<big>**⚠**</big>
|
||||||
|
Just don't forget switching to the migration mode, once there is a production database!
|
||||||
|
|
||||||
SERVER_PORT=8081 ./gradlew bootRun
|
### How to Re-Generate Spring-Controller-Interfaces from OpenAPI specs?
|
||||||
|
|
||||||
|
The API is described as OpenAPI specifications in `src/main/resources/api-definition/`.
|
||||||
|
|
||||||
|
Once generated, the interfaces for the Spring-Controllers can be found in `build/generated/sources/openapi`.
|
||||||
|
|
||||||
|
These interfaces have to be implemented by subclasses named `*Controller`.
|
||||||
|
|
||||||
|
All gradle tasks which need the generated interfaces depend on the Gradle task `openApiGenerate` which controls the code generation.
|
||||||
|
It can also be executed directly:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
gw openApiGenerate
|
||||||
|
```
|
||||||
|
|
||||||
|
### How to Generate Database Table Diagrams?
|
||||||
|
|
||||||
|
Some overview documentation about the database can be generated via [postgresql_autodoc](https://github.com/cbbrowne/autodoc").
|
||||||
|
To make it easier, the command line is included in the `.aliases`, just call:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
postgres-autodoc
|
||||||
|
```
|
||||||
|
|
||||||
|
The output will list the generated files.
|
||||||
|
|
||||||
|
|
||||||
|
## Further Documentation
|
||||||
|
|
||||||
|
- the `doc` directory contains architecture concepts and a glossary
|
||||||
|
- the `ideas` directory contains unstructured ideas for future development or documentation
|
||||||
|
39
angular.json
39
angular.json
@ -1,39 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
|
||||||
"version": 1,
|
|
||||||
"newProjectRoot": "projects",
|
|
||||||
"projects": {
|
|
||||||
"hsadmin-ng": {
|
|
||||||
"root": "",
|
|
||||||
"sourceRoot": "src/main/webapp",
|
|
||||||
"projectType": "application",
|
|
||||||
"architect": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"defaultProject": "hsadmin-ng",
|
|
||||||
"cli": {
|
|
||||||
"packageManager": "npm"
|
|
||||||
},
|
|
||||||
"schematics": {
|
|
||||||
"@schematics/angular:component": {
|
|
||||||
"inlineStyle": true,
|
|
||||||
"inlineTemplate": false,
|
|
||||||
"spec": false,
|
|
||||||
"prefix": "jhi",
|
|
||||||
"styleExt": "css"
|
|
||||||
},
|
|
||||||
"@schematics/angular:directive": {
|
|
||||||
"spec": false,
|
|
||||||
"prefix": "jhi"
|
|
||||||
},
|
|
||||||
"@schematics/angular:guard": {
|
|
||||||
"spec": false
|
|
||||||
},
|
|
||||||
"@schematics/angular:pipe": {
|
|
||||||
"spec": false
|
|
||||||
},
|
|
||||||
"@schematics/angular:service": {
|
|
||||||
"spec": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
38
bin/git-pull-and-if-origin-changed-run-tests
Executable file
38
bin/git-pull-and-if-origin-changed-run-tests
Executable file
@ -0,0 +1,38 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# waits for commits on any branch on origin, checks it out and builds it
|
||||||
|
|
||||||
|
. .aliases
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
git fetch origin >/dev/null
|
||||||
|
branch_with_new_commits=`git fetch origin >/dev/null; git for-each-ref --format='%(refname:short) %(upstream:track)' refs/heads | grep '\[behind' | cut -d' ' -f1 | head -n1`
|
||||||
|
|
||||||
|
if [ -n "$branch_with_new_commits" ]; then
|
||||||
|
echo "checking out branch: $branch_with_new_commits"
|
||||||
|
if git show-ref --quiet --heads "$branch_with_new_commits"; then
|
||||||
|
echo "Branch $branch_with_new_commits already exists. Checking it out and pulling latest changes."
|
||||||
|
git checkout "$branch_with_new_commits"
|
||||||
|
git pull origin "$branch_with_new_commits"
|
||||||
|
else
|
||||||
|
echo "Creating and checking out new branch: $branch_with_new_commits"
|
||||||
|
git checkout -b "$branch_with_new_commits" "origin/$branch_with_new_commits"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "building ..."
|
||||||
|
./gradlew gw clean test check -x pitest
|
||||||
|
fi
|
||||||
|
|
||||||
|
# wait 10s with a little animation
|
||||||
|
echo -e -n "\r\033[K waiting for changes (/) ..."
|
||||||
|
sleep 2
|
||||||
|
echo -e -n "\r\033[K waiting for changes (-) ..."
|
||||||
|
sleep 2
|
||||||
|
echo -e -n "\r\033[K waiting for changes (\) ..."
|
||||||
|
sleep 2
|
||||||
|
echo -e -n "\r\033[K waiting for changes (|) ..."
|
||||||
|
sleep 2
|
||||||
|
echo -e -n "\r\033[K waiting for changes ( ) ... "
|
||||||
|
sleep 2
|
||||||
|
echo -e -n "\r\033[K checking for changes"
|
||||||
|
done
|
||||||
|
|
633
build.gradle
633
build.gradle
@ -1,291 +1,446 @@
|
|||||||
import org.gradle.internal.os.OperatingSystem
|
|
||||||
|
|
||||||
buildscript {
|
|
||||||
repositories {
|
|
||||||
mavenLocal()
|
|
||||||
mavenCentral()
|
|
||||||
maven { url "http://repo.spring.io/plugins-release" }
|
|
||||||
maven { url "https://plugins.gradle.org/m2/" }
|
|
||||||
}
|
|
||||||
dependencies {
|
|
||||||
classpath "org.springframework.boot:spring-boot-gradle-plugin:${spring_boot_version}"
|
|
||||||
classpath "io.spring.gradle:propdeps-plugin:0.0.10.RELEASE"
|
|
||||||
classpath "org.openapitools:openapi-generator-gradle-plugin:3.3.0"
|
|
||||||
classpath "gradle.plugin.com.gorylenko.gradle-git-properties:gradle-git-properties:1.5.2"
|
|
||||||
//jhipster-needle-gradle-buildscript-dependency - JHipster will add additional gradle build script plugins here
|
|
||||||
}
|
|
||||||
dependencies {
|
|
||||||
classpath 'org.owasp:dependency-check-gradle:4.0.2'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id "org.sonarqube" version "2.6.2"
|
id 'java'
|
||||||
id "net.ltgt.apt-eclipse" version "0.19"
|
id 'org.springframework.boot' version '3.3.4'
|
||||||
id "net.ltgt.apt-idea" version "0.19"
|
id 'io.spring.dependency-management' version '1.1.6'
|
||||||
id "net.ltgt.apt" version "0.19"
|
id 'io.openapiprocessor.openapi-processor' version '2023.2'
|
||||||
id "io.spring.dependency-management" version "1.0.6.RELEASE"
|
id 'com.github.jk1.dependency-license-report' version '2.9'
|
||||||
id "com.moowork.node" version "1.2.0"
|
id "org.owasp.dependencycheck" version "10.0.4"
|
||||||
id 'org.liquibase.gradle' version '2.0.1'
|
id "com.diffplug.spotless" version "6.25.0"
|
||||||
//jhipster-needle-gradle-plugins - JHipster will add additional gradle plugins here
|
id 'jacoco'
|
||||||
|
id 'info.solidsoft.pitest' version '1.15.0'
|
||||||
|
id 'se.patrikerdes.use-latest-versions' version '0.2.18'
|
||||||
|
id 'com.github.ben-manes.versions' version '0.51.0'
|
||||||
}
|
}
|
||||||
|
|
||||||
apply plugin: 'java'
|
group = 'net.hostsharing'
|
||||||
apply plugin: 'org.owasp.dependencycheck'
|
version = '0.0.1-SNAPSHOT'
|
||||||
sourceCompatibility=1.8
|
|
||||||
targetCompatibility=1.8
|
|
||||||
// Until JHipster supports JDK 9
|
|
||||||
assert System.properties['java.specification.version'] == '1.8'
|
|
||||||
|
|
||||||
apply plugin: 'maven'
|
wrapper {
|
||||||
apply plugin: 'org.springframework.boot'
|
distributionType = Wrapper.DistributionType.BIN
|
||||||
apply plugin: 'war'
|
gradleVersion = '8.5'
|
||||||
apply plugin: 'propdeps'
|
|
||||||
apply plugin: 'com.moowork.node'
|
|
||||||
apply plugin: 'io.spring.dependency-management'
|
|
||||||
apply plugin: 'idea'
|
|
||||||
|
|
||||||
idea {
|
|
||||||
module {
|
|
||||||
excludeDirs += files('node_modules')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
configurations {
|
||||||
|
compileOnly {
|
||||||
|
extendsFrom annotationProcessor
|
||||||
|
}
|
||||||
|
testCompile {
|
||||||
|
extendsFrom testAnnotationProcessor
|
||||||
|
|
||||||
|
// Only JUNit 5 (Jupiter) should be used at compile time.
|
||||||
|
// For runtime it's still needed by testcontainers, though.
|
||||||
|
exclude group: 'junit', module: 'junit'
|
||||||
|
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
maven { url 'https://repo.spring.io/milestone' }
|
||||||
|
maven { url 'https://repo.spring.io/snapshot' }
|
||||||
|
}
|
||||||
|
|
||||||
|
java {
|
||||||
|
toolchain {
|
||||||
|
languageVersion = JavaLanguageVersion.of(21)
|
||||||
|
vendor = JvmVendorSpec.ADOPTIUM
|
||||||
|
implementation = JvmImplementation.VENDOR_SPECIFIC
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ext {
|
||||||
|
set('testcontainersVersion', "1.17.3")
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
|
||||||
|
implementation 'org.springframework.boot:spring-boot-starter-data-rest'
|
||||||
|
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
|
||||||
|
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||||
|
implementation 'org.springframework.boot:spring-boot-starter-validation'
|
||||||
|
implementation 'com.github.gavlyukovskiy:datasource-proxy-spring-boot-starter:1.9.2'
|
||||||
|
implementation 'org.springdoc:springdoc-openapi:2.6.0'
|
||||||
|
implementation 'org.postgresql:postgresql:42.7.4'
|
||||||
|
implementation 'org.liquibase:liquibase-core:4.29.2'
|
||||||
|
implementation 'io.hypersistence:hypersistence-utils-hibernate-63:3.8.3'
|
||||||
|
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.0'
|
||||||
|
implementation 'org.openapitools:jackson-databind-nullable:0.2.6'
|
||||||
|
implementation 'org.apache.commons:commons-text:1.12.0'
|
||||||
|
implementation 'net.java.dev.jna:jna:5.15.0'
|
||||||
|
implementation 'org.modelmapper:modelmapper:3.2.1'
|
||||||
|
implementation 'org.iban4j:iban4j:3.2.10-RELEASE'
|
||||||
|
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0'
|
||||||
|
implementation 'org.webjars:swagger-ui:5.17.14'
|
||||||
|
implementation 'org.reflections:reflections:0.10.2'
|
||||||
|
|
||||||
|
compileOnly 'org.projectlombok:lombok'
|
||||||
|
testCompileOnly 'org.projectlombok:lombok'
|
||||||
|
|
||||||
|
developmentOnly 'org.springframework.boot:spring-boot-devtools'
|
||||||
|
|
||||||
|
annotationProcessor 'org.projectlombok:lombok'
|
||||||
|
testAnnotationProcessor 'org.projectlombok:lombok'
|
||||||
|
|
||||||
|
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||||
|
testImplementation 'org.testcontainers:testcontainers'
|
||||||
|
testImplementation 'org.testcontainers:junit-jupiter'
|
||||||
|
testImplementation 'org.junit.jupiter:junit-jupiter'
|
||||||
|
testImplementation 'org.testcontainers:postgresql'
|
||||||
|
testImplementation 'com.tngtech.archunit:archunit-junit5:1.3.0'
|
||||||
|
testImplementation 'io.rest-assured:spring-mock-mvc'
|
||||||
|
testImplementation 'org.hamcrest:hamcrest-core:3.0'
|
||||||
|
testImplementation 'org.pitest:pitest-junit5-plugin:1.2.1'
|
||||||
|
testImplementation 'org.junit.jupiter:junit-jupiter-api'
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencyManagement {
|
dependencyManagement {
|
||||||
imports {
|
imports {
|
||||||
mavenBom 'io.github.jhipster:jhipster-dependencies:' + jhipster_dependencies_version
|
mavenBom "org.testcontainers:testcontainers-bom:${testcontainersVersion}"
|
||||||
//jhipster-needle-gradle-dependency-management - JHipster will add additional dependencies management here
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultTasks 'bootRun'
|
// Java Compiler Options
|
||||||
|
tasks.withType(JavaCompile) {
|
||||||
group = 'org.hostsharing.hsadminng'
|
options.compilerArgs += [
|
||||||
version = '0.0.1-SNAPSHOT'
|
"-parameters" // keep parameter names => no need for @Param for SpringData
|
||||||
|
]
|
||||||
description = ''
|
|
||||||
|
|
||||||
bootWar {
|
|
||||||
mainClassName = 'org.hostsharing.hsadminng.HsadminNgApp'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
war {
|
// Configure tests
|
||||||
webAppDirName = 'build/www/'
|
tasks.named('test') {
|
||||||
enabled = true
|
useJUnitPlatform()
|
||||||
extension = 'war.original'
|
jvmArgs '-Duser.language=en'
|
||||||
|
jvmArgs '-Duser.country=US'
|
||||||
}
|
}
|
||||||
|
|
||||||
springBoot {
|
// OpenAPI Source Code Generation
|
||||||
mainClassName = 'org.hostsharing.hsadminng.HsadminNgApp'
|
openapiProcessor {
|
||||||
|
springRoot {
|
||||||
|
processorName 'spring'
|
||||||
|
processor 'io.openapiprocessor:openapi-processor-spring:2022.5'
|
||||||
|
apiPath "$projectDir/src/main/resources/api-definition.yaml"
|
||||||
|
mapping "$projectDir/src/main/resources/api-mappings.yaml"
|
||||||
|
targetDir "$buildDir/generated/sources/openapi-javax"
|
||||||
|
showWarnings true
|
||||||
|
openApiNullable true
|
||||||
}
|
}
|
||||||
|
springRbac {
|
||||||
|
processorName 'spring'
|
||||||
|
processor 'io.openapiprocessor:openapi-processor-spring:2022.5'
|
||||||
|
apiPath "$projectDir/src/main/resources/api-definition/rbac/rbac.yaml"
|
||||||
|
mapping "$projectDir/src/main/resources/api-definition/rbac/api-mappings.yaml"
|
||||||
|
targetDir "$buildDir/generated/sources/openapi-javax"
|
||||||
|
showWarnings true
|
||||||
|
openApiNullable true
|
||||||
|
}
|
||||||
|
springTest {
|
||||||
|
processorName 'spring'
|
||||||
|
processor 'io.openapiprocessor:openapi-processor-spring:2022.5'
|
||||||
|
apiPath "$projectDir/src/main/resources/api-definition/test/test.yaml"
|
||||||
|
mapping "$projectDir/src/main/resources/api-definition/test/api-mappings.yaml"
|
||||||
|
targetDir "$buildDir/generated/sources/openapi-javax"
|
||||||
|
showWarnings true
|
||||||
|
openApiNullable true
|
||||||
|
}
|
||||||
|
springHsOffice {
|
||||||
|
processorName 'spring'
|
||||||
|
processor 'io.openapiprocessor:openapi-processor-spring:2022.5'
|
||||||
|
apiPath "$projectDir/src/main/resources/api-definition/hs-office/hs-office.yaml"
|
||||||
|
mapping "$projectDir/src/main/resources/api-definition/hs-office/api-mappings.yaml"
|
||||||
|
targetDir "$buildDir/generated/sources/openapi-javax"
|
||||||
|
showWarnings true
|
||||||
|
openApiNullable true
|
||||||
|
}
|
||||||
|
springHsBooking {
|
||||||
|
processorName 'spring'
|
||||||
|
processor 'io.openapiprocessor:openapi-processor-spring:2022.5'
|
||||||
|
apiPath "$projectDir/src/main/resources/api-definition/hs-booking/hs-booking.yaml"
|
||||||
|
mapping "$projectDir/src/main/resources/api-definition/hs-booking/api-mappings.yaml"
|
||||||
|
targetDir "$buildDir/generated/sources/openapi-javax"
|
||||||
|
showWarnings true
|
||||||
|
openApiNullable true
|
||||||
|
}
|
||||||
|
springHsHosting {
|
||||||
|
processorName 'spring'
|
||||||
|
processor 'io.openapiprocessor:openapi-processor-spring:2022.5'
|
||||||
|
apiPath "$projectDir/src/main/resources/api-definition/hs-hosting/hs-hosting.yaml"
|
||||||
|
mapping "$projectDir/src/main/resources/api-definition/hs-hosting/api-mappings.yaml"
|
||||||
|
targetDir "$buildDir/generated/sources/openapi-javax"
|
||||||
|
showWarnings true
|
||||||
|
openApiNullable true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sourceSets.main.java.srcDir 'build/generated/sources/openapi'
|
||||||
|
abstract class ProcessSpring extends DefaultTask {}
|
||||||
|
tasks.register('processSpring', ProcessSpring)
|
||||||
|
['processSpringRoot',
|
||||||
|
'processSpringRbac',
|
||||||
|
'processSpringTest',
|
||||||
|
'processSpringHsOffice',
|
||||||
|
'processSpringHsBooking',
|
||||||
|
'processSpringHsHosting'
|
||||||
|
].each {
|
||||||
|
project.tasks.processSpring.dependsOn it
|
||||||
|
}
|
||||||
|
project.tasks.processResources.dependsOn processSpring
|
||||||
|
project.tasks.compileJava.dependsOn processSpring
|
||||||
|
|
||||||
if (OperatingSystem.current().isWindows()) {
|
// Rename javax to jakarta in OpenApi generated java files because
|
||||||
// https://stackoverflow.com/questions/40037487/the-filename-or-extension-is-too-long-error-using-gradle
|
// io.openapiprocessor.openapi-processor 2022.5 does not yet support the openapiprocessor useSpringBoot3 config option.
|
||||||
task classpathJar(type: Jar) {
|
// TODO.impl: Upgrade to io.openapiprocessor.openapi-processor >= 2024.2
|
||||||
dependsOn configurations.runtime
|
// and use either `bean-validation: true` in api-mapping.yaml or `useSpringBoot3 true` (not sure where exactly).
|
||||||
appendix = 'classpath'
|
task openApiGenerate(type: Copy) {
|
||||||
|
from "$buildDir/generated/sources/openapi-javax"
|
||||||
|
into "$buildDir/generated/sources/openapi"
|
||||||
|
filter { line -> line.replaceAll('javax', 'jakarta') }
|
||||||
|
}
|
||||||
|
compileJava.source "$buildDir/generated/sources/openapi"
|
||||||
|
compileJava.dependsOn openApiGenerate
|
||||||
|
openApiGenerate.dependsOn processSpring
|
||||||
|
|
||||||
doFirst {
|
// Spotless Code Formatting
|
||||||
manifest {
|
spotless {
|
||||||
attributes 'Class-Path': configurations.runtime.files.collect {
|
java {
|
||||||
it.toURI().toURL().toString().replaceFirst(/file:\/+/, '/').replaceAll(' ', '%20')
|
removeUnusedImports()
|
||||||
}.join(' ')
|
indentWithSpaces(4)
|
||||||
|
endWithNewline()
|
||||||
|
toggleOffOn()
|
||||||
|
|
||||||
|
target fileTree(rootDir) {
|
||||||
|
include '**/*.java'
|
||||||
|
exclude '**/generated/**/*.java'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
project.tasks.check.dependsOn(spotlessCheck)
|
||||||
|
// HACK: no idea why spotless uses the output of these tasks, but we get warnings without those
|
||||||
|
project.tasks.spotlessJava.dependsOn(
|
||||||
|
tasks.generateLicenseReport,
|
||||||
|
tasks.pitest,
|
||||||
|
tasks.jacocoTestReport,
|
||||||
|
tasks.processResources,
|
||||||
|
tasks.processTestResources)
|
||||||
|
|
||||||
bootRun {
|
// OWASP Dependency Security Test
|
||||||
dependsOn classpathJar
|
dependencyCheck {
|
||||||
doFirst {
|
nvd {
|
||||||
classpath = files("$buildDir/classes/java/main", "$buildDir/resources/main", classpathJar.archivePath)
|
apiKey = project.properties['OWASP_API_KEY'] // set it in ~/.gradle/gradle.properties
|
||||||
|
delay = 16000
|
||||||
}
|
}
|
||||||
|
format = 'ALL'
|
||||||
|
suppressionFile = 'etc/owasp-dependency-check-suppression.xml'
|
||||||
|
failOnError = true
|
||||||
|
failBuildOnCVSS = 5
|
||||||
}
|
}
|
||||||
|
project.tasks.check.dependsOn(dependencyCheckAnalyze)
|
||||||
|
project.tasks.dependencyCheckAnalyze.doFirst { // Why not doLast? See README.md!
|
||||||
|
println "OWASP Dependency Security Report: file:///${project.rootDir}/build/reports/dependency-check-report.html"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// License Check
|
||||||
|
licenseReport {
|
||||||
|
excludeBoms = true
|
||||||
|
allowedLicensesFile = new File("$projectDir/etc/allowed-licenses.json")
|
||||||
|
}
|
||||||
|
project.tasks.check.dependsOn(checkLicense)
|
||||||
|
|
||||||
|
// JaCoCo Test Code Coverage
|
||||||
|
jacoco {
|
||||||
|
toolVersion = "0.8.10"
|
||||||
|
}
|
||||||
test {
|
test {
|
||||||
exclude '**/CucumberTest*'
|
finalizedBy jacocoTestReport // generate report after tests
|
||||||
|
excludes = [
|
||||||
// uncomment if the tests reports are not generated
|
'net.hostsharing.hsadminng.**.generated.**',
|
||||||
// see https://github.com/jhipster/generator-jhipster/pull/2771 and https://github.com/jhipster/generator-jhipster/pull/4484
|
]
|
||||||
// ignoreFailures true
|
useJUnitPlatform {
|
||||||
reports.html.enabled = false
|
excludeTags 'importOfficeData', 'importHostingData', 'scenarioTest'
|
||||||
}
|
}
|
||||||
|
|
||||||
task cucumberTest(type: Test) {
|
|
||||||
description = "Execute cucumber BDD tests."
|
|
||||||
group = "verification"
|
|
||||||
include '**/CucumberTest*'
|
|
||||||
|
|
||||||
// uncomment if the tests reports are not generated
|
|
||||||
// see https://github.com/jhipster/generator-jhipster/pull/2771 and https://github.com/jhipster/generator-jhipster/pull/4484
|
|
||||||
// ignoreFailures true
|
|
||||||
reports.html.enabled = false
|
|
||||||
}
|
}
|
||||||
|
jacocoTestReport {
|
||||||
check.dependsOn cucumberTest
|
dependsOn test
|
||||||
task testReport(type: TestReport) {
|
afterEvaluate {
|
||||||
destinationDir = file("$buildDir/reports/tests")
|
classDirectories.setFrom(files(classDirectories.files.collect {
|
||||||
reportOn test
|
fileTree(dir: it, exclude: [
|
||||||
|
"net/hostsharing/hsadminng/**/generated/**/*.class",
|
||||||
|
"net/hostsharing/hsadminng/hs/HsadminNgApplication.class"
|
||||||
|
])
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
doFirst { // Why not doLast? See README.md!
|
||||||
task cucumberTestReport(type: TestReport) {
|
println "HTML Jacoco Test Code Coverage Report: file://${reports.html.outputLocation.get()}/index.html"
|
||||||
destinationDir = file("$buildDir/reports/tests")
|
|
||||||
reportOn cucumberTest
|
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: 'gradle/docker.gradle'
|
|
||||||
apply from: 'gradle/sonar.gradle'
|
|
||||||
apply from: 'gradle/swagger.gradle'
|
|
||||||
//jhipster-needle-gradle-apply-from - JHipster will add additional gradle scripts to be applied here
|
|
||||||
|
|
||||||
if (project.hasProperty('prod')) {
|
|
||||||
apply from: 'gradle/profile_prod.gradle'
|
|
||||||
} else {
|
|
||||||
apply from: 'gradle/profile_dev.gradle'
|
|
||||||
}
|
}
|
||||||
|
project.tasks.check.dependsOn(jacocoTestCoverageVerification)
|
||||||
|
jacocoTestCoverageVerification {
|
||||||
if (!project.hasProperty('runList')) {
|
violationRules {
|
||||||
project.ext.runList = 'main'
|
rule {
|
||||||
}
|
limit {
|
||||||
|
minimum = 0.80 // TODO.test: improve instruction coverage
|
||||||
project.ext.diffChangelogFile = 'src/main/resources/config/liquibase/changelog/' + new Date().format('yyyyMMddHHmmss') + '_changelog.xml'
|
|
||||||
|
|
||||||
liquibase {
|
|
||||||
activities {
|
|
||||||
main {
|
|
||||||
driver ''
|
|
||||||
url ''
|
|
||||||
username 'hsadminNg'
|
|
||||||
password ''
|
|
||||||
changeLogFile 'src/main/resources/config/liquibase/master.xml'
|
|
||||||
defaultSchemaName ''
|
|
||||||
logLevel 'debug'
|
|
||||||
classpath 'src/main/resources/'
|
|
||||||
}
|
|
||||||
diffLog {
|
|
||||||
driver ''
|
|
||||||
url ''
|
|
||||||
username 'hsadminNg'
|
|
||||||
password ''
|
|
||||||
changeLogFile project.ext.diffChangelogFile
|
|
||||||
referenceUrl 'hibernate:spring:org.hostsharing.hsadminng.domain?dialect=&hibernate.physical_naming_strategy=org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy&hibernate.implicit_naming_strategy=org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy'
|
|
||||||
defaultSchemaName ''
|
|
||||||
logLevel 'debug'
|
|
||||||
classpath "$buildDir/classes/java/main"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
runList = project.ext.runList
|
// element: PACKAGE, BUNDLE, CLASS, SOURCEFILE or METHOD
|
||||||
|
// counter: INSTRUCTION, BRANCH, LINE, COMPLEXITY, METHOD, or CLASS
|
||||||
|
// value: TOTALCOUNT, COVEREDCOUNT, MISSEDCOUNT, COVEREDRATIO or MISSEDRATIO
|
||||||
|
|
||||||
|
rule {
|
||||||
|
element = 'CLASS'
|
||||||
|
excludes = [
|
||||||
|
'net.hostsharing.hsadminng.**.generated.**',
|
||||||
|
'net.hostsharing.hsadminng.rbac.test.dom.TestDomainEntity',
|
||||||
|
'net.hostsharing.hsadminng.HsadminNgApplication',
|
||||||
|
'net.hostsharing.hsadminng.ping.PingController',
|
||||||
|
'net.hostsharing.hsadminng.rbac.generator.*',
|
||||||
|
'net.hostsharing.hsadminng.rbac.grant.RbacGrantsDiagramService',
|
||||||
|
'net.hostsharing.hsadminng.rbac.grant.RbacGrantsDiagramService.Node',
|
||||||
|
'net.hostsharing.hsadminng.**.*Repository',
|
||||||
|
'net.hostsharing.hsadminng.mapper.Mapper'
|
||||||
|
]
|
||||||
|
|
||||||
|
limit {
|
||||||
|
counter = 'LINE'
|
||||||
|
value = 'COVEREDRATIO'
|
||||||
|
minimum = 0.75 // TODO.test: improve line coverage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rule {
|
||||||
|
element = 'METHOD'
|
||||||
|
excludes = [
|
||||||
|
'net.hostsharing.hsadminng.**.generated.**',
|
||||||
|
'net.hostsharing.hsadminng.HsadminNgApplication.main',
|
||||||
|
'net.hostsharing.hsadminng.ping.PingController.*'
|
||||||
|
]
|
||||||
|
|
||||||
|
limit {
|
||||||
|
counter = 'BRANCH'
|
||||||
|
value = 'COVEREDRATIO'
|
||||||
|
minimum = 0.00 // TODO.test: improve branch coverage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
configurations {
|
tasks.register('importOfficeData', Test) {
|
||||||
providedRuntime
|
useJUnitPlatform {
|
||||||
compile.exclude module: "spring-boot-starter-tomcat"
|
includeTags 'importOfficeData'
|
||||||
}
|
}
|
||||||
|
|
||||||
repositories {
|
group 'verification'
|
||||||
mavenLocal()
|
description 'run the import jobs as tests'
|
||||||
mavenCentral()
|
|
||||||
jcenter()
|
mustRunAfter spotlessJava
|
||||||
//jhipster-needle-gradle-repositories - JHipster will add additional repositories
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
tasks.register('importHostingAssets', Test) {
|
||||||
// Use ", version: jhipster_dependencies_version, changing: true" if you want
|
useJUnitPlatform {
|
||||||
// to use a SNAPSHOT release instead of a stable release
|
includeTags 'importHostingAssets'
|
||||||
compile group: "io.github.jhipster", name: "jhipster-framework"
|
|
||||||
compile "org.springframework.boot:spring-boot-starter-cache"
|
|
||||||
compile "io.dropwizard.metrics:metrics-core"
|
|
||||||
compile 'io.micrometer:micrometer-registry-prometheus'
|
|
||||||
compile "net.logstash.logback:logstash-logback-encoder"
|
|
||||||
compile "com.fasterxml.jackson.datatype:jackson-datatype-hppc"
|
|
||||||
compile "com.fasterxml.jackson.datatype:jackson-datatype-jsr310"
|
|
||||||
compile "com.fasterxml.jackson.datatype:jackson-datatype-hibernate5"
|
|
||||||
compile "com.fasterxml.jackson.core:jackson-annotations"
|
|
||||||
compile "com.fasterxml.jackson.core:jackson-databind"
|
|
||||||
compile "com.fasterxml.jackson.module:jackson-module-afterburner"
|
|
||||||
compile "javax.cache:cache-api"
|
|
||||||
compile "org.hibernate:hibernate-core"
|
|
||||||
compile "com.zaxxer:HikariCP"
|
|
||||||
compile "org.apache.commons:commons-lang3"
|
|
||||||
compile "commons-io:commons-io"
|
|
||||||
compile "javax.transaction:javax.transaction-api"
|
|
||||||
compile "org.ehcache:ehcache"
|
|
||||||
compile "org.hibernate:hibernate-entitymanager"
|
|
||||||
compile "org.hibernate:hibernate-envers"
|
|
||||||
compile "org.hibernate.validator:hibernate-validator"
|
|
||||||
compile "org.liquibase:liquibase-core"
|
|
||||||
compile "com.mattbertolini:liquibase-slf4j"
|
|
||||||
liquibaseRuntime "org.liquibase:liquibase-core"
|
|
||||||
liquibaseRuntime "org.liquibase.ext:liquibase-hibernate5:${liquibase_hibernate5_version}"
|
|
||||||
liquibaseRuntime sourceSets.main.compileClasspath
|
|
||||||
compile "org.springframework.boot:spring-boot-loader-tools"
|
|
||||||
compile "org.springframework.boot:spring-boot-starter-mail"
|
|
||||||
compile "org.springframework.boot:spring-boot-starter-logging"
|
|
||||||
compile "org.springframework.boot:spring-boot-starter-actuator"
|
|
||||||
compile "org.springframework.boot:spring-boot-starter-aop"
|
|
||||||
compile "org.springframework.boot:spring-boot-starter-data-jpa"
|
|
||||||
compile "org.springframework.boot:spring-boot-starter-security"
|
|
||||||
compile ("org.springframework.boot:spring-boot-starter-web") {
|
|
||||||
exclude module: 'spring-boot-starter-tomcat'
|
|
||||||
}
|
|
||||||
compile "org.springframework.boot:spring-boot-starter-undertow"
|
|
||||||
compile "org.springframework.boot:spring-boot-starter-thymeleaf"
|
|
||||||
compile "org.zalando:problem-spring-web:0.24.0-RC.0"
|
|
||||||
compile "org.springframework.boot:spring-boot-starter-cloud-connectors"
|
|
||||||
compile "org.springframework.security:spring-security-config"
|
|
||||||
compile "org.springframework.security:spring-security-data"
|
|
||||||
compile "org.springframework.security:spring-security-web"
|
|
||||||
compile "io.jsonwebtoken:jjwt-api"
|
|
||||||
runtime "io.jsonwebtoken:jjwt-impl"
|
|
||||||
runtime "io.jsonwebtoken:jjwt-jackson"
|
|
||||||
compile ("io.springfox:springfox-swagger2") {
|
|
||||||
exclude module: 'mapstruct'
|
|
||||||
}
|
|
||||||
compile "io.springfox:springfox-bean-validators"
|
|
||||||
compile "org.postgresql:postgresql"
|
|
||||||
liquibaseRuntime "org.postgresql:postgresql"
|
|
||||||
compile "org.mapstruct:mapstruct-jdk8:${mapstruct_version}"
|
|
||||||
annotationProcessor "org.mapstruct:mapstruct-processor:${mapstruct_version}"
|
|
||||||
annotationProcessor "org.hibernate:hibernate-jpamodelgen"
|
|
||||||
annotationProcessor ("org.springframework.boot:spring-boot-configuration-processor") {
|
|
||||||
exclude group: 'com.vaadin.external.google', module: 'android-json'
|
|
||||||
}
|
|
||||||
testCompile "com.jayway.jsonpath:json-path"
|
|
||||||
testCompile "io.cucumber:cucumber-junit"
|
|
||||||
testCompile "io.cucumber:cucumber-spring"
|
|
||||||
testCompile ("org.springframework.boot:spring-boot-starter-test") {
|
|
||||||
exclude group: 'com.vaadin.external.google', module: 'android-json'
|
|
||||||
}
|
|
||||||
testCompile "org.springframework.security:spring-security-test"
|
|
||||||
testCompile "org.springframework.boot:spring-boot-test"
|
|
||||||
testCompile "org.assertj:assertj-core"
|
|
||||||
testCompile "junit:junit"
|
|
||||||
testCompile "org.mockito:mockito-core"
|
|
||||||
testCompile "com.mattbertolini:liquibase-slf4j"
|
|
||||||
testCompile "org.hamcrest:hamcrest-library"
|
|
||||||
testCompile "com.h2database:h2"
|
|
||||||
liquibaseRuntime "com.h2database:h2"
|
|
||||||
//jhipster-needle-gradle-dependency - JHipster will add additional dependencies here
|
|
||||||
}
|
}
|
||||||
|
|
||||||
task cleanResources(type: Delete) {
|
group 'verification'
|
||||||
delete 'build/resources'
|
description 'run the import jobs as tests'
|
||||||
|
|
||||||
|
mustRunAfter spotlessJava
|
||||||
}
|
}
|
||||||
|
|
||||||
wrapper {
|
tasks.register('scenarioTests', Test) {
|
||||||
gradleVersion = '4.10.2'
|
useJUnitPlatform {
|
||||||
|
includeTags 'scenarioTest'
|
||||||
}
|
}
|
||||||
|
|
||||||
task stage(dependsOn: 'bootWar') {
|
group 'verification'
|
||||||
|
description 'run the import jobs as tests'
|
||||||
|
|
||||||
|
mustRunAfter spotlessJava
|
||||||
}
|
}
|
||||||
|
|
||||||
if (project.hasProperty('nodeInstall')) {
|
// pitest mutation testing
|
||||||
node {
|
pitest {
|
||||||
version = "${node_version}"
|
targetClasses = ['net.hostsharing.hsadminng.**']
|
||||||
npmVersion = "${npm_version}"
|
excludedClasses = [
|
||||||
yarnVersion = "${yarn_version}"
|
'net.hostsharing.hsadminng.config.**',
|
||||||
download = false
|
// 'net.hostsharing.hsadminng.**.*Controller',
|
||||||
|
'net.hostsharing.hsadminng.**.generated.**'
|
||||||
|
]
|
||||||
|
|
||||||
|
targetTests = ['net.hostsharing.hsadminng.**.*UnitTest', 'net.hostsharing.hsadminng.**.*RestTest']
|
||||||
|
excludedTestClasses = ['**AcceptanceTest*', '**IntegrationTest*']
|
||||||
|
|
||||||
|
pitestVersion = '1.17.0'
|
||||||
|
junit5PluginVersion = '1.1.0'
|
||||||
|
|
||||||
|
threads = 4
|
||||||
|
|
||||||
|
// As Java unit tests are pretty pointless in our case, this maybe makes not much sense.
|
||||||
|
mutationThreshold = 71
|
||||||
|
coverageThreshold = 57
|
||||||
|
testStrengthThreshold = 87
|
||||||
|
|
||||||
|
outputFormats = ['XML', 'HTML']
|
||||||
|
timestampedReports = false
|
||||||
|
}
|
||||||
|
project.tasks.check.dependsOn(project.tasks.pitest)
|
||||||
|
project.tasks.pitest.doFirst { // Why not doLast? See README.md!
|
||||||
|
println "PiTest Mutation Report: file:///${project.rootDir}/build/reports/pitest/index.html"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Dependency Versions Upgrade
|
||||||
|
useLatestVersions {
|
||||||
|
finalizedBy check
|
||||||
|
}
|
||||||
|
|
||||||
|
def isNonStable = { String version ->
|
||||||
|
def stableKeyword = ['RELEASE', 'FINAL', 'GA'].any { it -> version.toUpperCase().contains(it) }
|
||||||
|
def regex = /^[0-9,.v-]+(-r)?$/
|
||||||
|
return !stableKeyword && !(version ==~ regex)
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.named("dependencyUpdates").configure {
|
||||||
|
rejectVersionIf {
|
||||||
|
isNonStable(it.candidate.version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Generate HTML from Markdown scenario-test-reports using Pandoc:
|
||||||
|
tasks.register('convertMarkdownToHtml') {
|
||||||
|
description = 'Generates HTML from Markdown scenario-test-reports using Pandoc.'
|
||||||
|
group = 'Conversion'
|
||||||
|
|
||||||
|
// Define the template file and input directory
|
||||||
|
def templateFile = file('doc/scenarios/template.html')
|
||||||
|
|
||||||
|
// Task configuration and execution
|
||||||
|
doFirst {
|
||||||
|
// Check if pandoc is installed
|
||||||
|
try {
|
||||||
|
exec {
|
||||||
|
commandLine 'pandoc', '--version'
|
||||||
|
}
|
||||||
|
} catch (Exception) {
|
||||||
|
throw new GradleException("Pandoc is not installed or not found in the system path.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the template file exists
|
||||||
|
if (!templateFile.exists()) {
|
||||||
|
throw new GradleException("Template file 'doc/scenarios/template.html' not found.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
doLast {
|
||||||
|
// Gather all Markdown files in the current directory
|
||||||
|
fileTree(dir: '.', include: 'doc/scenarios/*.md').each { file ->
|
||||||
|
// Corrected way to create the output file path
|
||||||
|
def outputFile = new File(file.parent, file.name.replaceAll(/\.md$/, '.html'))
|
||||||
|
|
||||||
|
// Execute pandoc for each markdown file
|
||||||
|
exec {
|
||||||
|
commandLine 'pandoc', file.absolutePath, '--template', templateFile.absolutePath, '-o', outputFile.absolutePath
|
||||||
|
}
|
||||||
|
|
||||||
|
println "Converted ${file.name} to ${outputFile.name}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
BIN
doc/.images/intellij-idea-gradle-run-config.png
Normal file
BIN
doc/.images/intellij-idea-gradle-run-config.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 74 KiB |
BIN
doc/.images/intellij-idea-gradle-run-template.png
Normal file
BIN
doc/.images/intellij-idea-gradle-run-template.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 97 KiB |
15
doc/adr-concept.md
Normal file
15
doc/adr-concept.md
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
## ADR-Concept
|
||||||
|
|
||||||
|
This project uses ADRs (Architecture Decision Records), see also https://adr.github.io/.
|
||||||
|
|
||||||
|
There is a template available under [0000-00-00.adr-tempate.md](./0000-00-00.adr-tempate.md).
|
||||||
|
|
||||||
|
It's suggested to write an ADR if any of these is true:
|
||||||
|
|
||||||
|
- an architectural decision is hard to change,
|
||||||
|
- there is a dispute about an architectural decision,
|
||||||
|
- some unusual architectural decision was made (e.g. unusual library),
|
||||||
|
- some deeper investigation was necessary before the decision.
|
||||||
|
|
||||||
|
ADRs should not be written for minor decisions with limited impact.
|
||||||
|
|
39
doc/adr/0000-00-00.adr-template.md
Normal file
39
doc/adr/0000-00-00.adr-template.md
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
# TITLE
|
||||||
|
|
||||||
|
**Status:**
|
||||||
|
- [ ] proposed by (Proposer)
|
||||||
|
- [ ] accepted by (Participants)
|
||||||
|
- [ ] rejected by (Participants)
|
||||||
|
- [ ] superseded by (superseding ADR)
|
||||||
|
|
||||||
|
## Context and Problem Statement
|
||||||
|
|
||||||
|
A short description, why and under which circumstances this decision had to be made.
|
||||||
|
|
||||||
|
### Technical Background
|
||||||
|
|
||||||
|
Some details about the technical challenge.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
* OPTION-1
|
||||||
|
* OPTION-...
|
||||||
|
|
||||||
|
### OPTION-n
|
||||||
|
|
||||||
|
A short overview about the option.
|
||||||
|
|
||||||
|
#### Advantages
|
||||||
|
|
||||||
|
A list of advantages.
|
||||||
|
|
||||||
|
|
||||||
|
#### Disadvantages
|
||||||
|
|
||||||
|
A list of disadvantages.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Decision Outcome
|
||||||
|
|
||||||
|
Which option was chose and why.
|
160
doc/adr/2022-07-18.row-level-security-mechanism.md
Normal file
160
doc/adr/2022-07-18.row-level-security-mechanism.md
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
# Use VIEWs with JOIN into Permission-Assignments for Row-Level-Security
|
||||||
|
|
||||||
|
**Status:**
|
||||||
|
- [x] proposed by Michael Hönnig
|
||||||
|
- [ ] accepted by (Participants)
|
||||||
|
- [ ] rejected by (Participants)
|
||||||
|
- [ ] superseded by (superseding ADR)
|
||||||
|
|
||||||
|
## Context and Problem Statement
|
||||||
|
|
||||||
|
We need to decide how to apply the access rules defined in our RBAC system to the visibility of table rows for the accessing user.
|
||||||
|
|
||||||
|
The core problem here is, that in our RBAC system, determining the permissions of the accessing user has to consider a hierarchy of roles.
|
||||||
|
|
||||||
|
### Technical Background
|
||||||
|
|
||||||
|
The session variable `hsadminng.currentSubject` contains the accessing (domain-level) user, which is unrelated to the PostgreSQL user).
|
||||||
|
|
||||||
|
Given is a stored function `isPermissionGrantedToSubject` which detects if the accessing subject has a given permission (e.g. 'view').
|
||||||
|
|
||||||
|
Given is also a stored function `queryAllPermissionsOfSubjectId` which returns the flattened view to all permissions assigned to the given accessing user.
|
||||||
|
|
||||||
|
In the following code snippets `customer` is just an example domain table.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
* Perform Visibility-Checks programmatically in the Backend
|
||||||
|
* Add Visibility-Checks in the Backend
|
||||||
|
* POLICY with ENABLE ROW LEVEL SECURITY
|
||||||
|
* VIEW-RULE with ON SELECT DO INSTEAD
|
||||||
|
* VIEW with JOIN into Flattened Permissions
|
||||||
|
|
||||||
|
### Perform Visibility-Checks programmatically in the Backend
|
||||||
|
|
||||||
|
In this solution, the database ignores row level visibility and returns all rows which match a given query. Afterwards, the result is filtered programmatically with Java-code in the backend.
|
||||||
|
|
||||||
|
#### Advantages
|
||||||
|
|
||||||
|
Very flexible access, programmatic, rules could be implemented.
|
||||||
|
|
||||||
|
The role-hierarchy and permissions for current subjects (e.g. logged-in users) could be cached in the backend.
|
||||||
|
|
||||||
|
The access logic can be tested in pure Java unit tests.
|
||||||
|
|
||||||
|
At least regarding this aspect, an in-memory database could be used for integration testing; though the recursive Role-evaluation uses PostgreSQL features anyway.
|
||||||
|
|
||||||
|
#### Disadvantages
|
||||||
|
|
||||||
|
It's inefficient when initial query is not very restrictive, e.g. as on overview pages in a frontend, which often show all accessible objects, large parts or even whole database tables need to be transferred from the database to the backend.
|
||||||
|
|
||||||
|
It's error-prone and security leaks can happen too easily, because after every query the access rights for all participating joins have to be considered.
|
||||||
|
|
||||||
|
### Add Visibility-Checks in the Backend
|
||||||
|
|
||||||
|
In this solution again, the database ignores row level visibility and returns all rows which match a given query. And the backend adds filter conditions to each query sent to the database.
|
||||||
|
|
||||||
|
#### Advantages
|
||||||
|
|
||||||
|
At least regarding this aspect, an in-memory database could be used for integration testing.
|
||||||
|
|
||||||
|
#### Disadvantages
|
||||||
|
|
||||||
|
It's error-prone and security leaks can happen too easily, because for every query the access rights for all participating joins have to be considered.
|
||||||
|
|
||||||
|
### POLICY with ENABLE ROW LEVEL SECURITY
|
||||||
|
|
||||||
|
For restricted DB-users, which are used by the backend, access to rows is filtered using a policy:
|
||||||
|
|
||||||
|
SET SESSION AUTHORIZATION DEFAULT;
|
||||||
|
CREATE ROLE restricted;
|
||||||
|
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO restricted;
|
||||||
|
ALTER TABLE customer ENABLE ROW LEVEL SECURITY;
|
||||||
|
CREATE POLICY customer_policy ON customer
|
||||||
|
FOR SELECT
|
||||||
|
TO restricted
|
||||||
|
USING (
|
||||||
|
rbac.isPermissionGrantedToSubject(rbac.findEffectivePermissionId('customer', id, 'view'), currentSubjectUuid())
|
||||||
|
);
|
||||||
|
|
||||||
|
SET SESSION AUTHORIZATION restricted;
|
||||||
|
SET hsadminng.currentSubject TO 'alex@example.com';
|
||||||
|
SELECT * from customer; -- will only return visible rows
|
||||||
|
|
||||||
|
#### Advantages
|
||||||
|
|
||||||
|
Using POLICY together with ENABLE ROW LEVEL SECURITY is the PostgreSQL native mechanism to control access to data on the role level. Therefore, it looked like an obvious and elegant solution.
|
||||||
|
|
||||||
|
Every access at from the backend is under access control at the database level.
|
||||||
|
|
||||||
|
### Disadvantages
|
||||||
|
|
||||||
|
Unfortunately security mechanisms in PostgreSQL prevent the query optimizer to work well beyond ownership barriers (session user vs. table owner) and a SELECT from a table with 1 million objects needed over 30 seconds with our hierarchical RBAC policy.
|
||||||
|
|
||||||
|
We are bound to PostgreSQL, including integration tests and testing the RBAC system itself.
|
||||||
|
|
||||||
|
### VIEW-RULE with ON SELECT DO INSTEAD
|
||||||
|
|
||||||
|
SET SESSION SESSION AUTHORIZATION DEFAULT;
|
||||||
|
CREATE VIEW cust_view AS
|
||||||
|
SELECT * FROM customer;
|
||||||
|
CREATE OR REPLACE RULE "_RETURN" AS
|
||||||
|
ON SELECT TO cust_view
|
||||||
|
DO INSTEAD
|
||||||
|
SELECT * FROM customer WHERE rbac.isPermissionGrantedToSubject(rbac.findEffectivePermissionId('customer', id, 'view'), currentSubjectUuid());
|
||||||
|
|
||||||
|
SET SESSION AUTHORIZATION restricted;
|
||||||
|
SET hsadminng.currentSubject TO 'alex@example.com';
|
||||||
|
SELECT * from customer; -- will only return visible rows
|
||||||
|
|
||||||
|
#### Advantages
|
||||||
|
|
||||||
|
Every access at from the backend is under access control at the database level.
|
||||||
|
|
||||||
|
Also using ON UPDATE etc., original tables could be completely hidden from the backend, and thus improved security.
|
||||||
|
|
||||||
|
### Disadvantages
|
||||||
|
|
||||||
|
Unfortunately security mechanisms in PostgreSQL prevent the query optimizer to work well beyond ownership barriers (session user vs. table owner) and a SELECT from a table with 1 million objects needed over 30 seconds with our hierarchical RBAC policy.
|
||||||
|
|
||||||
|
We are bound to PostgreSQL, including integration tests and testing the RBAC system itself.
|
||||||
|
|
||||||
|
An extra view needed for every table.
|
||||||
|
|
||||||
|
|
||||||
|
### VIEW with JOIN into flattened permissions
|
||||||
|
|
||||||
|
We do not access the tables directly from the backend, but via views which join the flattened permissions
|
||||||
|
|
||||||
|
SET SESSION SESSION AUTHORIZATION DEFAULT;
|
||||||
|
CREATE OR REPLACE VIEW cust_view AS
|
||||||
|
SELECT c.id, c.reference, c.prefix
|
||||||
|
FROM customer AS c
|
||||||
|
JOIN queryAllPermissionsOfSubjectId(currentSubjectUuid()) AS p
|
||||||
|
ON p.tableName='customer' AND p.rowId=c.id AND p.op='view';
|
||||||
|
GRANT ALL PRIVILEGES ON cust_view TO restricted;
|
||||||
|
|
||||||
|
SET SESSION SESSION AUTHORIZATION restricted;
|
||||||
|
SET hsadminng.currentSubject TO 'alex@example.com';
|
||||||
|
SELECT * from cust_view; -- will only return visible rows
|
||||||
|
|
||||||
|
Alternatively the JOIN could also be applied in a "ON SELECT DO INSTEAD"-RULE, if there is any advantage for later features.
|
||||||
|
|
||||||
|
#### Advantages
|
||||||
|
|
||||||
|
Every access at from the backend is under access control at the database level.
|
||||||
|
|
||||||
|
No special PostgreSQL features needed; though the recursive Role-evaluation uses PostgreSQL features anyway.
|
||||||
|
|
||||||
|
Very fast, on my laptop a SELECT * FROM a table with 1 million rows just took about 50ms.
|
||||||
|
|
||||||
|
Also using ON UPDATE etc., original tables could be completely hidden from the backend, and thus improved security.
|
||||||
|
|
||||||
|
### Disadvantages
|
||||||
|
|
||||||
|
An extra view needed for every table.
|
||||||
|
|
||||||
|
|
||||||
|
## Decision Outcome
|
||||||
|
|
||||||
|
We chose the option **"VIEW with JOIN into flattened permissions"** because it supports the best combination of performance and security with almost no disadvantge.
|
108
doc/adr/2022-08-08.object-mapping.md
Normal file
108
doc/adr/2022-08-08.object-mapping.md
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
# Object Mapping
|
||||||
|
|
||||||
|
**Status:**
|
||||||
|
- [x] proposed by Michael Hönnig
|
||||||
|
- [ ] accepted by (Participants)
|
||||||
|
- [ ] rejected by (Participants)
|
||||||
|
- [ ] superseded by (superseding ADR)
|
||||||
|
|
||||||
|
## Context and Problem Statement
|
||||||
|
|
||||||
|
Since we are using the *API first*-approach,
|
||||||
|
thus generating Java interfaces and model classes from an OpenAPI specification,
|
||||||
|
we cannot use the JPA-entities anymore at the API level,
|
||||||
|
not even if the data fields are 100% identical.
|
||||||
|
|
||||||
|
Therefore, we need some kind of mapping strategy.
|
||||||
|
|
||||||
|
|
||||||
|
### Technical Background
|
||||||
|
|
||||||
|
Java does not support duck-typing and therefore, objects of different classes have to be converted to each other, even if all data fields are identical.
|
||||||
|
|
||||||
|
In our case, the database query is usually the slowest part of handling a request.
|
||||||
|
Therefore, for the mapper, ease of use is more important than performance,
|
||||||
|
at least as long as the mapping part does not take more than 10% of the total request.
|
||||||
|
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
* specific programmatic conversion
|
||||||
|
* using the *MapStruct* library
|
||||||
|
* using the *ModelMapper* library
|
||||||
|
* Dozer, last update from 2014 + vulnerabilities => skipped
|
||||||
|
* Orika, last update from 2019 + vulnerabilities => skipped
|
||||||
|
* JMapper
|
||||||
|
|
||||||
|
### specific programmatic conversion
|
||||||
|
|
||||||
|
In this solution, we would write own code to convert the objects.
|
||||||
|
This usually means 3 converters for each entity/resource pair:
|
||||||
|
|
||||||
|
- entity -> resource
|
||||||
|
- resource -> entity
|
||||||
|
- list of entities -> list of resources
|
||||||
|
|
||||||
|
#### Advantages
|
||||||
|
|
||||||
|
Very flexible and fast.
|
||||||
|
|
||||||
|
#### Disadvantages
|
||||||
|
|
||||||
|
Huge amounts of bloat code.
|
||||||
|
|
||||||
|
|
||||||
|
### using the *MapStruct* library
|
||||||
|
|
||||||
|
See https://mapstruct.org/.
|
||||||
|
|
||||||
|
#### Advantages
|
||||||
|
|
||||||
|
- Most popular mapping library in the Java-world.
|
||||||
|
- Actively maintained, last release 1.5.2 from Jun 18, 2022.
|
||||||
|
- very fast (see [^1])
|
||||||
|
|
||||||
|
|
||||||
|
#### Disadvantages
|
||||||
|
|
||||||
|
- Needs interface declarations with annotations.
|
||||||
|
- Looks like it causes still too much bloat code for our purposes.
|
||||||
|
|
||||||
|
|
||||||
|
### using the *ModelMapper* library
|
||||||
|
|
||||||
|
See http://modelmapper.org/.
|
||||||
|
|
||||||
|
#### Advantages
|
||||||
|
|
||||||
|
- 1:1 mappings just need a simple method call without any bloat-code.
|
||||||
|
- Actively maintained, last release 3.1.0 from Mar 08, 2022.
|
||||||
|
|
||||||
|
#### Disadvantages
|
||||||
|
|
||||||
|
- could not find any, will give it a try
|
||||||
|
|
||||||
|
### using the *JMapper* library
|
||||||
|
|
||||||
|
See https://jmapper-framework.github.io/jmapper-core/.
|
||||||
|
|
||||||
|
#### Advantages
|
||||||
|
|
||||||
|
- Supports annotation-based and programmatic mapping exceptions.
|
||||||
|
- Actively maintained, last release 1.6.3 from May 27, 2022.
|
||||||
|
- very fast (see [^1])
|
||||||
|
|
||||||
|
|
||||||
|
#### Disadvantages
|
||||||
|
|
||||||
|
- needs a separate mapper instance for each mapping pair
|
||||||
|
- cannot map collections (needs `stream().map(...).collect(toList())` or similar)
|
||||||
|
|
||||||
|
|
||||||
|
## Decision Outcome
|
||||||
|
|
||||||
|
We chose the option **"using the *ModelMapper* library"** because it has an acceptable performance without any bloat code.
|
||||||
|
|
||||||
|
If it turns out to be too slow after all, "using the *JMapper* library" seems to be a good alternative.
|
||||||
|
|
||||||
|
[^1]: https://www.baeldung.com/java-performance-mapping-frameworks
|
@ -0,0 +1,119 @@
|
|||||||
|
# Handling Automatic Creation of Hosting Assets for New Booking Items
|
||||||
|
|
||||||
|
**Status:**
|
||||||
|
- [x] proposed by (Michael Hönnig)
|
||||||
|
- [ ] accepted by (Participants)
|
||||||
|
- [ ] rejected by (Participants)
|
||||||
|
- [ ] superseded by (superseding ADR)
|
||||||
|
|
||||||
|
|
||||||
|
## Context and Problem Statement
|
||||||
|
|
||||||
|
When a customer creates a new booking item (e.g., `MANAGED_WEBSPACE`), the system must automatically create the related hosting asset.
|
||||||
|
This process can sometimes fail or require additional data from the user, e.g. installing a DNS verification key, or a hostmaster, e.g. the target server to use.
|
||||||
|
|
||||||
|
The challenge is how to handle this automatic creation process while dealing with missing data, asynchronicity and failures while ensuring system consistency and proper user notification.
|
||||||
|
|
||||||
|
|
||||||
|
### Technical Background
|
||||||
|
|
||||||
|
The creation of hosting assets can occur synchronously (in simple cases) or asynchronously (when additional steps like manual verification are needed).
|
||||||
|
For example, a `DOMAIN_SETUP` hosting asset may require DNS verification from the user, and until this is provided, the related domain cannot be fully set up.
|
||||||
|
|
||||||
|
Additionally, not all data needed for creating the hosting asset is stored in the booking item.
|
||||||
|
It's part of the HTTP request and later stored in the hosting asset, but we also need to store it before the hosting asset can be created asynchronously.
|
||||||
|
|
||||||
|
Current system behavior involves returning HTTP 201 upon booking item creation, but the automatic hosting asset creation might fail due to missing information.
|
||||||
|
The system needs to manage the creation process in a way that ensures valid hosting assets are created and informs the user of any actions required while still returning a 201 HTTP code, not an error code.
|
||||||
|
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
For storing the data needed for the hosting-asset creation:
|
||||||
|
|
||||||
|
* STORAGE-1: Store temporary asset data in the `BookingItemEntity`, e.g. a JSON column.
|
||||||
|
And delete the value of that column, once the hosting assets got successfully created.
|
||||||
|
* STORAGE-2: Create hosting assets immediately, even if invalid, but mark them as "inactive" until completed and fully validated.
|
||||||
|
* STORAGE-3: Store the asset data in a kind of event- or job-queue, which get deleted once the hosting-asset got successfully created.
|
||||||
|
|
||||||
|
For the user-notification status:
|
||||||
|
|
||||||
|
* STATUS-1: Introduce a status field in the booking-items.
|
||||||
|
* STATUS-2: Store the status in the event-/job-queue entries.
|
||||||
|
|
||||||
|
### STORAGE-1: Temporary Data Storage in `BookingItemEntity`
|
||||||
|
|
||||||
|
Store asset-related data (e.g., domain name) in a temporary column or JSON field in the `BookingItemEntity` until the hosting assets are successfully created.
|
||||||
|
Once assets are created, the temporary data is deleted to avoid inconsistencies.
|
||||||
|
|
||||||
|
#### Advantages
|
||||||
|
- Easy to implement.
|
||||||
|
|
||||||
|
#### Disadvantages
|
||||||
|
- Needs either a separate map of properties in the booking-item.
|
||||||
|
- Or, if stored as a JSON field in the booking-item-resources, these are misused.
|
||||||
|
- Requires additional cleanup logic to remove stale data.
|
||||||
|
|
||||||
|
### STORAGE-2: Inactive Hosting Assets Until Validation
|
||||||
|
|
||||||
|
Create the hosting assets immediately upon booking item creation but mark them as "inactive" until all required information (e.g., verification code) is provided and validation is complete.
|
||||||
|
|
||||||
|
#### Advantages
|
||||||
|
- Avoids temporary external data storage for the hosting-assets.
|
||||||
|
|
||||||
|
#### Disadvantages
|
||||||
|
- Validation becomes more complex as some properties need to be validated, others not.
|
||||||
|
And some properties even need special treatment for new entities, which then becomes vague.
|
||||||
|
- Inactive assets have to be filtered from operational assets.
|
||||||
|
- Potential risk of incomplete or inconsistent assets being created, which may require correction.
|
||||||
|
- Difficult to write tests for all possible combinations of validations.
|
||||||
|
|
||||||
|
### STORAGE-3: Event-Based Approach
|
||||||
|
|
||||||
|
The hosting asset data required for creation us passed to the API and stored in a `BookingItemCreatedEvent`.
|
||||||
|
If hosting asset creation cannot happen synchronously, the event is stored and processed asynchronously in batches, retrying failed asset creation as needed.
|
||||||
|
|
||||||
|
#### Advantages
|
||||||
|
- Clean-data-structure (separation of concerns).
|
||||||
|
- Clear separation between booking item creation and hosting asset creation.
|
||||||
|
- Only valid assets in the database.
|
||||||
|
- Can handle complex asynchronous processes (like waiting for external verification) in a clean and structured manner.
|
||||||
|
- Easier to manage retries and failures in asset creation without complicating the booking item structure.
|
||||||
|
|
||||||
|
#### Disadvantages
|
||||||
|
- At the Spring controller level, the whole JSON is already converted into Java objects,
|
||||||
|
but for storing the asset data in the even, we need JSON again.
|
||||||
|
This could is not just a performance-overhead but could also lead to inconsistencies.
|
||||||
|
|
||||||
|
### STATUS-1: Store hosting-asset-creation-status in the `BookingItemEntity`
|
||||||
|
|
||||||
|
A status field would be added to booking-items to track the creation state of related hosting assets.
|
||||||
|
The users could check their booking-items for the status of the hosting-asset creation, error messages and further instructions.
|
||||||
|
|
||||||
|
#### Advantages
|
||||||
|
- Easy to implement.
|
||||||
|
|
||||||
|
#### Disadvantages
|
||||||
|
- Adds a field to the booking-item which is makes no sense anymore once the related hosting asset is created.
|
||||||
|
|
||||||
|
|
||||||
|
### Status-2: Store hosting-asset-creation-status in the `BookingItemCreateEvent`
|
||||||
|
|
||||||
|
A status field would be added to the booking-item-created event and get updated with the latest messages any time we try to create the hosting-asset.
|
||||||
|
|
||||||
|
#### Advantages
|
||||||
|
- Clean-data-structure (separation of concerns)
|
||||||
|
|
||||||
|
#### Disadvantages
|
||||||
|
- Accessing the status requires querying the event queue.
|
||||||
|
|
||||||
|
|
||||||
|
## Decision Outcome
|
||||||
|
|
||||||
|
**Chosen Option: STORAGE-3 with STATUS-2 (Event-Based Approach with `BookingItemCreatedEvent`)**
|
||||||
|
|
||||||
|
The event-based approach was selected as the best solution for handling automatic hosting asset creation. This option provides a clear separation between booking item creation and hosting asset creation, ensuring that no invalid or incomplete assets are created. The asynchronous nature of the event system allows for retries and external validation steps (such as user-entered verification codes) without disrupting the overall flow.
|
||||||
|
|
||||||
|
By using `BookingItemCreatedEvent` to store the hosting-asset data and the status,
|
||||||
|
we don't need to misuse other data structures for temporary data
|
||||||
|
and therefore hava a clean separation of concerns.
|
74
doc/glossary.md
Normal file
74
doc/glossary.md
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
### hsadminNg Glossary
|
||||||
|
|
||||||
|
This is a collection of terms used in this project, which either might not be generally known or unclear in meaning.
|
||||||
|
If you miss something, please add it with a `TODO` marker.
|
||||||
|
|
||||||
|
#### Blackbox-Test
|
||||||
|
|
||||||
|
A blackbox-test does not know and not consider such internals of an implementation, it just tests externally observable behaviour.
|
||||||
|
|
||||||
|
|
||||||
|
#### Business Object
|
||||||
|
|
||||||
|
Used in the RBAC-system to refer to an object from the business realm.
|
||||||
|
The usual term is *domain object* but in our context, the term *domain* could be too easily confused with a DNS *Internet domain*.
|
||||||
|
|
||||||
|
|
||||||
|
#### Dummy
|
||||||
|
|
||||||
|
A *dummy* is a kind of *Test-Double* which replaces a real dependency which is not really needed in the test case.
|
||||||
|
|
||||||
|
|
||||||
|
#### Fake
|
||||||
|
|
||||||
|
A *fake* is a kind of *Test-Double* without using any library, but rather a manual fake implementation of a dependency.
|
||||||
|
|
||||||
|
|
||||||
|
#### Mock
|
||||||
|
|
||||||
|
A *mock* is a kind of *Test-Double* which can be configured to behaviours as needed by a test-case.
|
||||||
|
|
||||||
|
Often the term "mock" is used in a generic way, because typical mocking libraries like *Mockito* can also be used as dummies or spies and can replace fakes.
|
||||||
|
|
||||||
|
|
||||||
|
#### RBAC
|
||||||
|
|
||||||
|
Abbreviation for *Role Based Access Control*.
|
||||||
|
A system to control access to business objects by defining users, roles, and permissions.
|
||||||
|
See also [The INCITS 359-2012 Standard](https://www.techstreet.com/standards/incits-359-2012?product_id=1837530).
|
||||||
|
|
||||||
|
In our case we are implementing a hierarchical RBAC for a hierarchical and dynamic business object structure.
|
||||||
|
More information can be found in our [RBAC Architecture Document](rbac.md).
|
||||||
|
|
||||||
|
|
||||||
|
#### Tenant
|
||||||
|
|
||||||
|
*Tenant* is one of the standard roles of Hostsharing's RBAC system.
|
||||||
|
It is assigned as a sub-role to those who have rights on sub-objects of a business object.
|
||||||
|
Usually, tenants can only view the contents.
|
||||||
|
|
||||||
|
Generally, tenant roles only apply for the mere existence, id and name of a business object,
|
||||||
|
not for internal details.
|
||||||
|
E.g. a tenant of a customer could be the administrator of a hosting package of that customer.
|
||||||
|
They can view some identifying information of that customer, but not view their billing and banking information.
|
||||||
|
|
||||||
|
|
||||||
|
#### Whitebox-Test
|
||||||
|
|
||||||
|
A whitebox-test knows and considers the internals of an implementation, e.g. it knows which dependencies it needs and can test special, implementation-dependent cases.
|
||||||
|
|
||||||
|
|
||||||
|
#### Test-Double
|
||||||
|
|
||||||
|
A "double" is a general term for something which replaces a real implementation of a dependency of the unit under test.
|
||||||
|
This can be a "dummy", a "fake", a "mock", a "spy" or a "stub".
|
||||||
|
|
||||||
|
|
||||||
|
#### Test-Fixture
|
||||||
|
|
||||||
|
Generally a test-fixture refers to all code within a test
|
||||||
|
which is needed to setup the test environment and extract results,
|
||||||
|
but which is not part of the test-cases.
|
||||||
|
|
||||||
|
In other words: The code which is needed to bind test-cases to the actual unit under test,
|
||||||
|
is called test-fixture.
|
218
doc/hs-hosting-asset-type-structure.md
Normal file
218
doc/hs-hosting-asset-type-structure.md
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
## HostingAsset Type Structure
|
||||||
|
|
||||||
|
|
||||||
|
### Server+Webspace
|
||||||
|
|
||||||
|
```plantuml
|
||||||
|
@startuml
|
||||||
|
left to right direction
|
||||||
|
|
||||||
|
package Booking #feb28c {
|
||||||
|
entity BI_PRIVATE_CLOUD
|
||||||
|
entity BI_CLOUD_SERVER
|
||||||
|
entity BI_MANAGED_SERVER
|
||||||
|
entity BI_MANAGED_WEBSPACE
|
||||||
|
entity BI_DOMAIN_SETUP
|
||||||
|
}
|
||||||
|
|
||||||
|
package Hosting #feb28c{
|
||||||
|
package Server #99bcdb {
|
||||||
|
entity HA_CLOUD_SERVER
|
||||||
|
entity HA_MANAGED_SERVER
|
||||||
|
entity HA_IPV4_NUMBER
|
||||||
|
entity HA_IPV6_NUMBER
|
||||||
|
}
|
||||||
|
|
||||||
|
package Webspace #99bcdb {
|
||||||
|
entity HA_MANAGED_WEBSPACE
|
||||||
|
entity HA_UNIX_USER
|
||||||
|
entity HA_EMAIL_ALIAS
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
BI_CLOUD_SERVER *--> BI_PRIVATE_CLOUD
|
||||||
|
BI_MANAGED_SERVER *--> BI_PRIVATE_CLOUD
|
||||||
|
BI_MANAGED_WEBSPACE *--> BI_MANAGED_SERVER
|
||||||
|
|
||||||
|
HA_CLOUD_SERVER *==> BI_CLOUD_SERVER
|
||||||
|
HA_MANAGED_SERVER *==> BI_MANAGED_SERVER
|
||||||
|
HA_MANAGED_WEBSPACE *==> BI_MANAGED_WEBSPACE
|
||||||
|
HA_MANAGED_WEBSPACE o..> HA_MANAGED_SERVER
|
||||||
|
HA_UNIX_USER *==> HA_MANAGED_WEBSPACE
|
||||||
|
HA_EMAIL_ALIAS *==> HA_MANAGED_WEBSPACE
|
||||||
|
HA_IPV4_NUMBER o..> HA_CLOUD_SERVER
|
||||||
|
HA_IPV4_NUMBER o..> HA_MANAGED_SERVER
|
||||||
|
HA_IPV4_NUMBER o..> HA_MANAGED_WEBSPACE
|
||||||
|
HA_IPV6_NUMBER o..> HA_CLOUD_SERVER
|
||||||
|
HA_IPV6_NUMBER o..> HA_MANAGED_SERVER
|
||||||
|
HA_IPV6_NUMBER o..> HA_MANAGED_WEBSPACE
|
||||||
|
|
||||||
|
package Legend #white {
|
||||||
|
SUB_ENTITY1 *--> REQUIRED_PARENT_ENTITY
|
||||||
|
SUB_ENTITY2 *..> OPTIONAL_PARENT_ENTITY
|
||||||
|
ASSIGNED_ENTITY1 o--> REQUIRED_ASSIGNED_TO_ENTITY1
|
||||||
|
ASSIGNED_ENTITY2 o..> OPTIONAL_ASSIGNED_TO_ENTITY2
|
||||||
|
}
|
||||||
|
Booking -down[hidden]->Legend
|
||||||
|
```
|
||||||
|
|
||||||
|
### Domain
|
||||||
|
|
||||||
|
```plantuml
|
||||||
|
@startuml
|
||||||
|
left to right direction
|
||||||
|
|
||||||
|
package Booking #feb28c {
|
||||||
|
entity BI_PRIVATE_CLOUD
|
||||||
|
entity BI_CLOUD_SERVER
|
||||||
|
entity BI_MANAGED_SERVER
|
||||||
|
entity BI_MANAGED_WEBSPACE
|
||||||
|
entity BI_DOMAIN_SETUP
|
||||||
|
}
|
||||||
|
|
||||||
|
package Hosting #feb28c{
|
||||||
|
package Domain #99bcdb {
|
||||||
|
entity HA_DOMAIN_SETUP
|
||||||
|
entity HA_DOMAIN_DNS_SETUP
|
||||||
|
entity HA_DOMAIN_HTTP_SETUP
|
||||||
|
entity HA_DOMAIN_SMTP_SETUP
|
||||||
|
entity HA_DOMAIN_MBOX_SETUP
|
||||||
|
entity HA_EMAIL_ADDRESS
|
||||||
|
}
|
||||||
|
|
||||||
|
package Webspace #99bcdb {
|
||||||
|
entity HA_MANAGED_WEBSPACE
|
||||||
|
entity HA_UNIX_USER
|
||||||
|
entity HA_EMAIL_ALIAS
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
BI_CLOUD_SERVER *--> BI_PRIVATE_CLOUD
|
||||||
|
BI_MANAGED_SERVER *--> BI_PRIVATE_CLOUD
|
||||||
|
BI_MANAGED_WEBSPACE *--> BI_MANAGED_SERVER
|
||||||
|
|
||||||
|
HA_MANAGED_WEBSPACE *==> BI_MANAGED_WEBSPACE
|
||||||
|
HA_UNIX_USER *==> HA_MANAGED_WEBSPACE
|
||||||
|
HA_EMAIL_ALIAS *==> HA_MANAGED_WEBSPACE
|
||||||
|
HA_DOMAIN_SETUP *==> BI_DOMAIN_SETUP
|
||||||
|
HA_DOMAIN_SETUP o..> HA_DOMAIN_SETUP
|
||||||
|
HA_DOMAIN_DNS_SETUP *==> HA_DOMAIN_SETUP
|
||||||
|
HA_DOMAIN_DNS_SETUP o--> HA_MANAGED_WEBSPACE
|
||||||
|
HA_DOMAIN_HTTP_SETUP *==> HA_DOMAIN_SETUP
|
||||||
|
HA_DOMAIN_HTTP_SETUP o--> HA_UNIX_USER
|
||||||
|
HA_DOMAIN_SMTP_SETUP *==> HA_DOMAIN_SETUP
|
||||||
|
HA_DOMAIN_SMTP_SETUP o--> HA_MANAGED_WEBSPACE
|
||||||
|
HA_DOMAIN_MBOX_SETUP *==> HA_DOMAIN_SETUP
|
||||||
|
HA_DOMAIN_MBOX_SETUP o--> HA_MANAGED_WEBSPACE
|
||||||
|
HA_EMAIL_ADDRESS *==> HA_DOMAIN_MBOX_SETUP
|
||||||
|
|
||||||
|
package Legend #white {
|
||||||
|
SUB_ENTITY1 *--> REQUIRED_PARENT_ENTITY
|
||||||
|
SUB_ENTITY2 *..> OPTIONAL_PARENT_ENTITY
|
||||||
|
ASSIGNED_ENTITY1 o--> REQUIRED_ASSIGNED_TO_ENTITY1
|
||||||
|
ASSIGNED_ENTITY2 o..> OPTIONAL_ASSIGNED_TO_ENTITY2
|
||||||
|
}
|
||||||
|
Booking -down[hidden]->Legend
|
||||||
|
```
|
||||||
|
|
||||||
|
### MariaDB
|
||||||
|
|
||||||
|
```plantuml
|
||||||
|
@startuml
|
||||||
|
left to right direction
|
||||||
|
|
||||||
|
package Booking #feb28c {
|
||||||
|
entity BI_PRIVATE_CLOUD
|
||||||
|
entity BI_CLOUD_SERVER
|
||||||
|
entity BI_MANAGED_SERVER
|
||||||
|
entity BI_MANAGED_WEBSPACE
|
||||||
|
entity BI_DOMAIN_SETUP
|
||||||
|
}
|
||||||
|
|
||||||
|
package Hosting #feb28c{
|
||||||
|
package MariaDB #99bcdb {
|
||||||
|
entity HA_MARIADB_INSTANCE
|
||||||
|
entity HA_MARIADB_USER
|
||||||
|
entity HA_MARIADB_DATABASE
|
||||||
|
}
|
||||||
|
|
||||||
|
package Webspace #99bcdb {
|
||||||
|
entity HA_MANAGED_WEBSPACE
|
||||||
|
entity HA_UNIX_USER
|
||||||
|
entity HA_EMAIL_ALIAS
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
BI_CLOUD_SERVER *--> BI_PRIVATE_CLOUD
|
||||||
|
BI_MANAGED_SERVER *--> BI_PRIVATE_CLOUD
|
||||||
|
BI_MANAGED_WEBSPACE *--> BI_MANAGED_SERVER
|
||||||
|
|
||||||
|
HA_MANAGED_WEBSPACE *==> BI_MANAGED_WEBSPACE
|
||||||
|
HA_UNIX_USER *==> HA_MANAGED_WEBSPACE
|
||||||
|
HA_EMAIL_ALIAS *==> HA_MANAGED_WEBSPACE
|
||||||
|
HA_MARIADB_USER *==> HA_MANAGED_WEBSPACE
|
||||||
|
HA_MARIADB_USER o--> HA_MARIADB_INSTANCE
|
||||||
|
HA_MARIADB_DATABASE *==> HA_MARIADB_USER
|
||||||
|
|
||||||
|
package Legend #white {
|
||||||
|
SUB_ENTITY1 *--> REQUIRED_PARENT_ENTITY
|
||||||
|
SUB_ENTITY2 *..> OPTIONAL_PARENT_ENTITY
|
||||||
|
ASSIGNED_ENTITY1 o--> REQUIRED_ASSIGNED_TO_ENTITY1
|
||||||
|
ASSIGNED_ENTITY2 o..> OPTIONAL_ASSIGNED_TO_ENTITY2
|
||||||
|
}
|
||||||
|
Booking -down[hidden]->Legend
|
||||||
|
```
|
||||||
|
|
||||||
|
### PostgreSQL
|
||||||
|
|
||||||
|
```plantuml
|
||||||
|
@startuml
|
||||||
|
left to right direction
|
||||||
|
|
||||||
|
package Booking #feb28c {
|
||||||
|
entity BI_PRIVATE_CLOUD
|
||||||
|
entity BI_CLOUD_SERVER
|
||||||
|
entity BI_MANAGED_SERVER
|
||||||
|
entity BI_MANAGED_WEBSPACE
|
||||||
|
entity BI_DOMAIN_SETUP
|
||||||
|
}
|
||||||
|
|
||||||
|
package Hosting #feb28c{
|
||||||
|
package PostgreSQL #99bcdb {
|
||||||
|
entity HA_PGSQL_INSTANCE
|
||||||
|
entity HA_PGSQL_USER
|
||||||
|
entity HA_PGSQL_DATABASE
|
||||||
|
}
|
||||||
|
|
||||||
|
package Webspace #99bcdb {
|
||||||
|
entity HA_MANAGED_WEBSPACE
|
||||||
|
entity HA_UNIX_USER
|
||||||
|
entity HA_EMAIL_ALIAS
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
BI_CLOUD_SERVER *--> BI_PRIVATE_CLOUD
|
||||||
|
BI_MANAGED_SERVER *--> BI_PRIVATE_CLOUD
|
||||||
|
BI_MANAGED_WEBSPACE *--> BI_MANAGED_SERVER
|
||||||
|
|
||||||
|
HA_MANAGED_WEBSPACE *==> BI_MANAGED_WEBSPACE
|
||||||
|
HA_UNIX_USER *==> HA_MANAGED_WEBSPACE
|
||||||
|
HA_EMAIL_ALIAS *==> HA_MANAGED_WEBSPACE
|
||||||
|
HA_PGSQL_USER *==> HA_MANAGED_WEBSPACE
|
||||||
|
HA_PGSQL_USER o--> HA_PGSQL_INSTANCE
|
||||||
|
HA_PGSQL_DATABASE *==> HA_PGSQL_USER
|
||||||
|
|
||||||
|
package Legend #white {
|
||||||
|
SUB_ENTITY1 *--> REQUIRED_PARENT_ENTITY
|
||||||
|
SUB_ENTITY2 *..> OPTIONAL_PARENT_ENTITY
|
||||||
|
ASSIGNED_ENTITY1 o--> REQUIRED_ASSIGNED_TO_ENTITY1
|
||||||
|
ASSIGNED_ENTITY2 o..> OPTIONAL_ASSIGNED_TO_ENTITY2
|
||||||
|
}
|
||||||
|
Booking -down[hidden]->Legend
|
||||||
|
```
|
||||||
|
|
||||||
|
This code generated was by HsHostingAssetType.main, do not amend manually.
|
187
doc/hs-office-data-structure.md
Normal file
187
doc/hs-office-data-structure.md
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
# Beispiel: juristische Person (GmbH)
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
classDiagram
|
||||||
|
direction TD
|
||||||
|
|
||||||
|
namespace Hostsharing {
|
||||||
|
class person-HostsharingEG
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Partner {
|
||||||
|
class partner-MeierGmbH
|
||||||
|
class rel-MeierGmbH
|
||||||
|
class personDetails-MeierGmbH
|
||||||
|
class contactData-MeierGmbH
|
||||||
|
class person-MeierGmbH
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Representatives {
|
||||||
|
class person-FrankMeier
|
||||||
|
class contactData-FrankMeier
|
||||||
|
class rel-MeierGmbH-FrankMeier
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Debitors {
|
||||||
|
class debitor-MeierGmbH
|
||||||
|
class contactData-MeierGmbH-Buha
|
||||||
|
class rel-MeierGmbH-Buha
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Operations {
|
||||||
|
class person-SabineMeier
|
||||||
|
class contactData-SabineMeier
|
||||||
|
class rel-MeierGmbH-SabineMeier
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Enums {
|
||||||
|
|
||||||
|
class RelationType {
|
||||||
|
<<enumeration>>
|
||||||
|
UNKNOWN
|
||||||
|
PARTNER
|
||||||
|
DEBITOR
|
||||||
|
REPRESENTATIVE
|
||||||
|
OPERATIONS
|
||||||
|
}
|
||||||
|
|
||||||
|
class PersonType {
|
||||||
|
<<enumeration>>
|
||||||
|
UNKNOWN: nur für Import
|
||||||
|
NATURAL_PERSON: natürliche Person
|
||||||
|
LEGAL_PERSON: z.B. GmbH, e.K., eG, e.V.
|
||||||
|
INCORORATED_FIRM: z.B. OHG, Partnerschaftsgesellschaft
|
||||||
|
UNINCORPORATED_FIRM: z.B. GbR, ARGE, Erbengemeinschaft
|
||||||
|
PUBLIC_INSTITUTION: KdöR, AöR [ohne Registergericht/Registernummer]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class person-HostsharingEG {
|
||||||
|
+personType: LEGAL
|
||||||
|
+tradeName: Hostsahring eG
|
||||||
|
+familyName
|
||||||
|
+givenName
|
||||||
|
}
|
||||||
|
|
||||||
|
class partner-MeierGmbH {
|
||||||
|
+Numeric partnerNumber: 12345
|
||||||
|
+Relation partnerRel
|
||||||
|
}
|
||||||
|
partner-MeierGmbH *-- rel-MeierGmbH
|
||||||
|
|
||||||
|
class person-MeierGmbH {
|
||||||
|
+personType: LEGAL
|
||||||
|
+tradeName: Meier GmbH
|
||||||
|
+familyName
|
||||||
|
+givenName
|
||||||
|
}
|
||||||
|
person-MeierGmbH *-- personDetails-MeierGmbH
|
||||||
|
|
||||||
|
class personDetails-MeierGmbH {
|
||||||
|
+registrationOffice: AG Hamburg
|
||||||
|
+registrationNumber: ABC123434
|
||||||
|
+birthName
|
||||||
|
+birthPlace
|
||||||
|
+dateOfDeath
|
||||||
|
}
|
||||||
|
|
||||||
|
class contactData-MeierGmbH {
|
||||||
|
+postalAddress: Hauptstraße 5, 22345 Hamburg
|
||||||
|
+phoneNumbers: +49 40 12345-00
|
||||||
|
+emailAddresses: office@meier-gmbh.de
|
||||||
|
}
|
||||||
|
|
||||||
|
class rel-MeierGmbH {
|
||||||
|
+RelationType type PARTNER
|
||||||
|
+Person anchor
|
||||||
|
+Person holder
|
||||||
|
+Contact contact
|
||||||
|
}
|
||||||
|
rel-MeierGmbH o-- person-HostsharingEG : anchor
|
||||||
|
rel-MeierGmbH o-- person-MeierGmbH : holder
|
||||||
|
rel-MeierGmbH o-- contactData-MeierGmbH
|
||||||
|
|
||||||
|
%% --- Debitors ---
|
||||||
|
|
||||||
|
class debitor-MeierGmbH {
|
||||||
|
+Partner partner
|
||||||
|
+Numeric[2] debitorNumberSuffix: 00
|
||||||
|
+Relation debitorRel
|
||||||
|
+boolean billable: true
|
||||||
|
+String vatId: ID123456789
|
||||||
|
+String vatCountryCode: DE
|
||||||
|
+boolean vatBusiness: true
|
||||||
|
+boolean vatReverseCharge: false
|
||||||
|
+BankAccount refundBankAccount
|
||||||
|
+String defaultPrefix: mei
|
||||||
|
}
|
||||||
|
debitor-MeierGmbH o-- partner-MeierGmbH
|
||||||
|
debitor-MeierGmbH *-- rel-MeierGmbH-Buha
|
||||||
|
|
||||||
|
class contactData-MeierGmbH-Buha {
|
||||||
|
+postalAddress: Hauptstraße 5, 22345 Hamburg
|
||||||
|
+phoneNumbers: +49 40 12345-05
|
||||||
|
+emailAddresses: buha@meier-gmbh.de
|
||||||
|
}
|
||||||
|
|
||||||
|
class rel-MeierGmbH-Buha {
|
||||||
|
+RelationType type DEBITOR
|
||||||
|
+Person anchor
|
||||||
|
+Person holder
|
||||||
|
+Contact contact
|
||||||
|
}
|
||||||
|
rel-MeierGmbH-Buha o-- person-MeierGmbH : anchor
|
||||||
|
rel-MeierGmbH-Buha o-- person-MeierGmbH : holder
|
||||||
|
rel-MeierGmbH-Buha o-- contactData-MeierGmbH-Buha
|
||||||
|
|
||||||
|
%% --- Representatives ---
|
||||||
|
|
||||||
|
class person-FrankMeier {
|
||||||
|
+ personType: NATURAL
|
||||||
|
+ tradeName
|
||||||
|
+ familyName: Meier
|
||||||
|
+ givenName: Frank
|
||||||
|
}
|
||||||
|
|
||||||
|
class contactData-FrankMeier {
|
||||||
|
+postalAddress
|
||||||
|
+phoneNumbers: +49 40 12345-22
|
||||||
|
+emailAddresses: frank.meier@meier-gmbh.de
|
||||||
|
}
|
||||||
|
|
||||||
|
class rel-MeierGmbH-FrankMeier {
|
||||||
|
+RelationType type REPRESENTATIVE
|
||||||
|
+Person anchor
|
||||||
|
+Person holder
|
||||||
|
+Contact contact
|
||||||
|
}
|
||||||
|
rel-MeierGmbH-FrankMeier o-- person-MeierGmbH : anchor
|
||||||
|
rel-MeierGmbH-FrankMeier o-- person-FrankMeier : holder
|
||||||
|
rel-MeierGmbH-FrankMeier o-- contactData-FrankMeier
|
||||||
|
|
||||||
|
%% --- Operations ---
|
||||||
|
|
||||||
|
class person-SabineMeier {
|
||||||
|
+personType: NATURAL
|
||||||
|
+tradeName
|
||||||
|
+familyName: Meier
|
||||||
|
+givenName: Sabine
|
||||||
|
}
|
||||||
|
|
||||||
|
class contactData-SabineMeier {
|
||||||
|
+postalAddress
|
||||||
|
+phoneNumbers: +49 40 12345-22
|
||||||
|
+emailAddresses: sabine.meier@meier-gmbh.de
|
||||||
|
}
|
||||||
|
|
||||||
|
class rel-MeierGmbH-SabineMeier {
|
||||||
|
+RelationType type OPERATIONAL
|
||||||
|
+Person anchor
|
||||||
|
+Person holder
|
||||||
|
+Contact contact
|
||||||
|
}
|
||||||
|
rel-MeierGmbH-SabineMeier o-- person-MeierGmbH : anchor
|
||||||
|
rel-MeierGmbH-SabineMeier o-- person-SabineMeier : holder
|
||||||
|
rel-MeierGmbH-SabineMeier o-- contactData-SabineMeier
|
||||||
|
|
||||||
|
```
|
83
doc/ideas/rbac-schema-f.md
Normal file
83
doc/ideas/rbac-schema-f.md
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
*(this is just a scribbled draft, that's why it's still in German)*
|
||||||
|
|
||||||
|
### *Schema-F* für Permissions, Rollen und Grants
|
||||||
|
|
||||||
|
Permissions, Rollen und Grants werden in den INSERT/UPDATE/DELETE-Triggern von Geschäftsobjekten erzeugt und gelöscht. Das Löschen erfolgt meistens automatisch über das zugehörige RbacObject, die INSERT- und UPDATE-Trigger müssen jedoch in *pl/pgsql* ausprogrammiert werden.
|
||||||
|
|
||||||
|
Das folgende Schema soll dabei unterstützen, die richtigen Permissions, Rollen und Grants festzulegen.
|
||||||
|
|
||||||
|
An einigen Stellen ist vom *Initiator* die Rede. Als *Initiator* gilt derjenige User, der die Operation (INSERT oder UPDATE) durchführt bzw. eine explizit anzugebende Rolle des Users.
|
||||||
|
Wird keine solche explizite Rolle angegeben, gilt die granted Rolle als diejenige, als der das Grant erfolgt.
|
||||||
|
|
||||||
|
#### Typ Root: Objekte, welche nur eine Spezialisierung bzw. Zusatzdaten für andere Objekte bereitstellen (z.B. Partner für Relations vom Typ Partner oder Partner Details für Partner)
|
||||||
|
|
||||||
|
Objektorientiert gedacht, enthalten solche Objekte die Zusatzdaten einer Subklasse; die Daten im Partner erweitern also eine Relation vom Typ `partner`.
|
||||||
|
|
||||||
|
- Dann muss dieses Objekt zeitlich nach dem Objekt erzeugt werden, auf dass es sich bezieht, also z.B. zeitlich nach der Relation.
|
||||||
|
- Es werden Delete (\*), Edit und View Permissions für dieses Objekt erzeugt.
|
||||||
|
- Es werden **keine** Rollen für dieses Objekt erzeugt.
|
||||||
|
- Statt eigener Rollen werden die o.g. Permissions passenden Rollen des Hauptobjekts zugewiesen (granted) bzw. aus denen entfernt (revoked).
|
||||||
|
- Handelt es sich um Zusatzdaten zum Zwecke der Spezialisierung, dann z.B. so:
|
||||||
|
- Delete (\*) <-- Owner des Hauptobjektes
|
||||||
|
- Edit <-- **Admin** des Hauptobjektes
|
||||||
|
- View <-- Agent des Hauptobjektes
|
||||||
|
- Handelt es sich um Zusatzdaten, für die sich Edit-Rechte delegieren lassen sollen (wie im Falle der Partner-Details eines Partners), dann z.B. so:
|
||||||
|
- Delete (\*) <-- Owner des Hauptobjektes
|
||||||
|
- Edit <-- **Agent** des Hauptobjektes
|
||||||
|
- View <-- Agent des Hauptobjektes
|
||||||
|
- Für die Rollenzuordnung zwischen referenzierten Objekten gilt:
|
||||||
|
- Für Objekte vom Typ Root werden die Rollen des zugehörigen Aggregator-Objektes verwendet.
|
||||||
|
- Gibt es Referenzen auf hierarchisch verbundene Objekte (z.B. Debitor.refundBankAccount) gilt folgende Faustregel:
|
||||||
|
***Nach oben absteigen, nach unten halten oder aufsteigen.*** An einem fachlich übergeordneten Objekt wird also eine niedrigere Rolle (z.B. Debitor.ADMIN -> Partner.AGENT), einem fachlich untergeordneten Objekt eine gleichwertige Rolle (z.B. Partner.ADMIN -> Debitor.ADMIN) zugewiesen oder sogar aufgestiegen (Debitor.ADMIN -> Package.TENANT).
|
||||||
|
- Für Referenzen zwischen Objekten, die nicht hierarchisch zueinander stehen (z.B. Debitor und Bankverbindung), wird auf beiden seiten abgestiegen (also Debitor.ADMIN -> BankAccount.REFERRER und BankAccount.ADMIN -> Debitor.TENANT).
|
||||||
|
|
||||||
|
Anmerkung: Der Typ-Begriff *Root* bezieht sich auf die Rolle im fachlichen Datenmodell. Im Bezug auf den Teilgraphen eines fachlichen Kontexts ist dies auch eine Wurzel im Sinne der Graphentheorie. Aber in anderen fachlichen Kontexten können auch diese Objekte von anderen Teilgraphen referenziert werden und werden dann zum inneren Knoten.
|
||||||
|
|
||||||
|
|
||||||
|
#### Typ Aggregator: Objekte, welche weitere Objekte zusammenfassen (z.B. Relation fasst zwei Persons und einen Contact zusammen)
|
||||||
|
|
||||||
|
Solche Objekte verweisen üblicherweise auf Objekte vom Typ Leaf und werden oft von Objekten des Typs Root referenziert.
|
||||||
|
|
||||||
|
- Es werden i.d.R. folgende Rollen für diese Objekte erzeugt:
|
||||||
|
- Owner, Admin, Agent, Tenent(, Guest?)
|
||||||
|
- Es werden Delete (\*), Edit und View Permissions für dieses Objekt erzeugt.
|
||||||
|
- Die Permissions werden den Rollen sinnvoll zugewiesen, z.B.:
|
||||||
|
- Owner -> Delete (\*)
|
||||||
|
- Admin --> Edit
|
||||||
|
- Tenant (oder ggf. Guest) --> View
|
||||||
|
- Außerdem werden folgende Grants erstellt bzw. entzogen:
|
||||||
|
- Initiator --> Owner
|
||||||
|
- Owner --> Admin
|
||||||
|
- Admin --> Referrer
|
||||||
|
- Admins der referenzierten Objekte werden Agent des Aggregators
|
||||||
|
- Tenants des Aggregators werden Referrer der referenzierten Objekte
|
||||||
|
|
||||||
|
### Typ Leaf: Handelt es sich um ein Objekt, welches (außer zur Modellierung separater Permissions) keine Unterobjekte enthält (z.B. Person, Customer)?
|
||||||
|
|
||||||
|
Solche Objekte werden üblicherweise von Objekten des Typs Aggregator, manchmal auch von Objekten des Typs Root, referenziert.
|
||||||
|
|
||||||
|
- Es werden i.d.R. folgende Rollen für diese Objekte erzeugt:
|
||||||
|
- Owner, Admin, Referrer
|
||||||
|
- Es werden Delete (\*), Edit und View Permissions für dieses Objekt erzeugt.
|
||||||
|
- Die Permissions werden den Rollen sinnvoll zugewiesen, z.B.:
|
||||||
|
- Delete (\*) <-- Owner
|
||||||
|
- Edit <-- Admin
|
||||||
|
- View <-- Referrer
|
||||||
|
- Außerdem werden folgende Grants erstellt bzw. entzogen:
|
||||||
|
- Owner --> Admin
|
||||||
|
- Admin --> Referrer
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
|
||||||
|
subgraph partnerDetails
|
||||||
|
direction TB
|
||||||
|
style partnerDetails fill:#eee
|
||||||
|
|
||||||
|
perm:partnerDetails.*{{partnerDetails.*}}
|
||||||
|
role:partnerDetails.edit{{partnerDetails.edit}}
|
||||||
|
role:partnerDetails.view{{partnerDetails.view}}
|
||||||
|
|
||||||
|
|
||||||
|
end
|
||||||
|
```
|
29
doc/ideas/simplified-grant-structure.md
Normal file
29
doc/ideas/simplified-grant-structure.md
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
(this is just a scribbled idea, that's why it's still in German)
|
||||||
|
|
||||||
|
Ich habe mal wieder vom RBAC-System geträumt 🙈 Ok, im Halbschlaf darüber nachgedacht trifft es wohl besser. Und jetzt frage ich mich, ob wir viel zu kompliziert gedacht haben.
|
||||||
|
|
||||||
|
Bislang gingen wir ja davon aus, dass, wenn komplexe Entitäten (z.B. Partner) erzeugt werden, wir wir über den INSERT-Trigger den Rollen der verknüpften Entitäten (z.B. den Rollen der Personendaten des Partners) auch Rechte an den komplexeren Entitäten und umgekehrt geben müssen.
|
||||||
|
|
||||||
|
Da die komplexen Entitäten nur mit gewissen verbundenen Entitäten überhaupt sinnvoll nutzbar sind und diese daher über INNSER JOINs mitladen, könnte sonst auch nur jemand diese Entitäten, der auch die SELECT-Permission an den verküpften Entitäten hat.
|
||||||
|
|
||||||
|
Vor einigen Wochen hatten wir schon einmal darüber geredet, ob wir dieses Geflecht wirklich komplett durchplanen müssen, also über mehrere Stufen hinweg, oder ob sehr warscheinlich eh dieselben Leuten an den weiter entfernten Entitäten die nötien Rechte haben, weil dahinter dieselben User stehen. Also z.B. dass gewährleistet ist, dass jemand mit ADMIN-Recht an den Personendaten des Partners auch bis in die SEPA-Mandate eines Debitors hineinsehen kann.
|
||||||
|
|
||||||
|
Und nun gehe ich noch einen Schritt weiter: Könnte es nicht auch andersherum sein? Also wenn jemand z.B. SELECT-Recht am Partner hat, dass wir davon ausgehen können, dass derjenige auch die Partner-Personen- und Kontaktdaten sehen darf, und zwar implizit durch seine Partner-SELECT-Permission und ohne dass er explizit Rollen für diese Partner-Personen oder Kontaktdaten inne hat?
|
||||||
|
|
||||||
|
Im Halbschlaf kam mir nur die Idee, warum wir nicht einfach die komplexen JPA-Entitäten zwar auf die restricted View setzen, wie bisher, aber für die verknüpften Entitäten auf die direkten (bisher "Raw..." genannt) Entitäten gehen. Dann könnte jemand mit einer Rolle, welche die SELECT-Permission auf die komplexe JPA-Entität (z.B.) Partner inne hat, auch die dazugehörige Relation(ship) ["Relation" wurde vor kurzem auf kurz "Relation" umbenannt] und die wiederum dazu gehörigen Personen- und Kontaktdaten lesen, ohne dass in einem INSERT- und UPDATE-Trigger der Partner-Entität die ganzen Grants mit den verknüpften Entäten aufgebaut und aktualisiert werden müssen.
|
||||||
|
|
||||||
|
Beim Debitor ist das nämlich selbst mit Generator die Hölle, zumal eben auch Querverbindungen gegranted werden müssen, z.B. von der Debitor-Person zum Sema-Mandat - jedenfalls wenn man nicht Gefahr laufen wollte, dass jemand mit Admin-Rechten an der Partner-Person (also z.B. ein Repräsentant des Partners) die Sepa-Mandate der Debitoren gar nicht mehr sehen kann. Natürlich bräuchte man immer noch die Agent-Rolle am Partner und Debitor (evtl. repräsentiert durch die jeweils zugehörigen Relation - falls dieser Trick überhaupt noch nötig wäre), sowie ein Grant vom Partner-Agent auf den Debitor-Agent und vom Debitor-Agent auf die Sepa-Mandate-Admins, aber eben ohne filigran die ganzen Neben-Entäten (Personen- und Kontaktdaten von Partner und Debitor sowie Bank-Account) in jedem Trigger berücksichtigen zu müssen. Beim Refund-Bank-Account sogar besonders ätzend, weil der optional ist und dadurch zig "if ...refundBankAccountUuid is not null then ..." im Code enstehen (wenn der auch generiert ist).
|
||||||
|
|
||||||
|
Mit anderen Worten, um als Repräsentant eines Geschäftspartners auf den Bank-Account der Sepa-Mandate sehen zu dürfen, wird derzeut folgende Grant-Kette durchlaufen (bzw. eben noch nicht, weil es noch nicht funktioniert):
|
||||||
|
|
||||||
|
User -> Partner-Holder-Person:ADMIN -> Partner-Relation:AGENT -> Debitor-Relation:AGENT -> Sepa-Mandat:ADMIN -> BankAccount:ADMIN -> BankAccount:SELECT
|
||||||
|
|
||||||
|
Daraus würde:
|
||||||
|
|
||||||
|
User -> Partner-Relation:AGENT -> Debitor-Relation:AGENT -> Sepa-Mandat:ADMIN -> Sepa-Mandat:SELECT*
|
||||||
|
|
||||||
|
(*mit JOIN auf RawBankAccount, also implizitem Leserecht)
|
||||||
|
|
||||||
|
Das klingt zunächst nach nur einer marginalen Vereinfachung, die eigentlich Vereinfachung liegt aber im Erzeugen der Grants in den Triggern, denn da sind zudem noch Partner-Anchor-Person, Debitor-Holder- und Anchor-Person, Partner- und Debitor-Contact sowie der RefundBankAccount zu berücksichtigen. Und genau diese Grants würden großteils wegfallen, und durch implizite Persmissions über die JOINs auf die Raw-Tables ersetzt werden. Den refundBankAccound müssten wir dann, analog zu den Sepa-Mandataten, umgedreht modellieren, da den sonst
|
||||||
|
|
||||||
|
Man könnte das Ganze auch als "Entwicklung der Rechtestruktur für Hosting-Entitäten auf der obersten Ebene" (Manged Webspace, Managed Server, Cloud Server etc.) sehen, denn die hängen alle unter dem Mega-komplexen Debitor.
|
288
doc/projects-booking-items-and-hosting-entities.md
Normal file
288
doc/projects-booking-items-and-hosting-entities.md
Normal file
@ -0,0 +1,288 @@
|
|||||||
|
## HSAdmin-NG
|
||||||
|
### Project/BookingItems/HostingEntities
|
||||||
|
|
||||||
|
__ATTENTION__: The notation uses UML clas diagram elements, but partly with different meanings. See Agenda.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
classDiagram
|
||||||
|
direction TD
|
||||||
|
|
||||||
|
Partner o-- "0..n" Membership
|
||||||
|
Partner *-- "1..n" Debitor
|
||||||
|
Debitor *-- "1..n" Project
|
||||||
|
|
||||||
|
Project o-- "0..n" PrivateCloudBI
|
||||||
|
Project o-- "0..n" CloudServerBI
|
||||||
|
Project o-- "0..n" ManagedServerBI
|
||||||
|
Project o-- "0..n" ManagedWebspaceBI
|
||||||
|
|
||||||
|
PrivateCloudBI o-- "0..n" ManagedServerBI
|
||||||
|
PrivateCloudBI o-- "0..n" CloudServerBI
|
||||||
|
|
||||||
|
CloudServerBI *-- CloudServerHE
|
||||||
|
|
||||||
|
ManagedServerBI *-- ManagedServerHE
|
||||||
|
ManagedServerBI o-- "0..n" ManagedWebspaceBI
|
||||||
|
ManagedWebspaceBI *-- ManagedWebspaceHE
|
||||||
|
|
||||||
|
ManagedWebspaceHE *-- "1..n" UnixUserHE
|
||||||
|
ManagedWebspaceHE o-- "0..n" DomainDNSSetupHE
|
||||||
|
ManagedWebspaceHE o-- "0..n" DomainHttpSetupHE
|
||||||
|
ManagedWebspaceHE o-- "0..n" DomainEMailSetupHE
|
||||||
|
ManagedWebspaceHE o-- "0..n" EMailAliasHE
|
||||||
|
DomainEMailSetupHE o-- "0..n" EMailAddressHE
|
||||||
|
ManagedWebspaceHE o-- "0..n" MariaDBUserHE
|
||||||
|
MariaDBUserHE o-- "0..n" MariaDBHE
|
||||||
|
ManagedWebspaceHE o-- "0..n" PostgresDBUserHE
|
||||||
|
PostgresDBUserHE o-- "0..n" PostgresDBHE
|
||||||
|
|
||||||
|
DomainHttpSetupHE --|> UnixUserHE : assignedToAsset
|
||||||
|
|
||||||
|
ManagedWebspaceHE --|> ManagedServerHE
|
||||||
|
|
||||||
|
namespace Office {
|
||||||
|
class Partner {
|
||||||
|
}
|
||||||
|
|
||||||
|
class Membership {
|
||||||
|
}
|
||||||
|
|
||||||
|
class Debitor {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Booking {
|
||||||
|
class Project {
|
||||||
|
+caption
|
||||||
|
+create()
|
||||||
|
}
|
||||||
|
class PrivateCloudBI {
|
||||||
|
+caption
|
||||||
|
~resources = [
|
||||||
|
⠀⠀+CPUs
|
||||||
|
⠀⠀+RAM
|
||||||
|
⠀⠀+SSD
|
||||||
|
⠀⠀+HDD
|
||||||
|
⠀⠀+Traffic
|
||||||
|
]
|
||||||
|
|
||||||
|
+book()
|
||||||
|
}
|
||||||
|
class CloudServerBI {
|
||||||
|
+caption
|
||||||
|
~resources = [
|
||||||
|
⠀⠀+CPUs
|
||||||
|
⠀⠀+RAM
|
||||||
|
⠀⠀+SSD
|
||||||
|
⠀⠀+HDD
|
||||||
|
⠀⠀+Traffic
|
||||||
|
]
|
||||||
|
|
||||||
|
+book()
|
||||||
|
}
|
||||||
|
class ManagedServerBI {
|
||||||
|
+caption
|
||||||
|
~respources = [
|
||||||
|
⠀⠀+CPUs
|
||||||
|
⠀⠀+RAM
|
||||||
|
⠀⠀+SSD
|
||||||
|
⠀⠀+HDD
|
||||||
|
⠀⠀+Traffic
|
||||||
|
]
|
||||||
|
|
||||||
|
+book()
|
||||||
|
}
|
||||||
|
class ManagedWebspaceBI {
|
||||||
|
+caption
|
||||||
|
~resources = [
|
||||||
|
⠀⠀+SSD
|
||||||
|
⠀⠀+HDD
|
||||||
|
⠀⠀+Traffic
|
||||||
|
⠀⠀+MultiOptions
|
||||||
|
⠀⠀+Daemons
|
||||||
|
]
|
||||||
|
|
||||||
|
+book()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
style Project stroke:blue,stroke-width:4px
|
||||||
|
style PrivateCloudBI stroke:blue,stroke-width:4px
|
||||||
|
style CloudServerBI stroke:blue,stroke-width:4px
|
||||||
|
style ManagedServerBI stroke:blue,stroke-width:4px
|
||||||
|
style ManagedWebspaceBI stroke:blue,stroke-width:4px
|
||||||
|
|
||||||
|
%% ---------------------------------------------------------
|
||||||
|
|
||||||
|
namespace HostingServers {
|
||||||
|
%% separate (pseudo-) namespace just for better rendering
|
||||||
|
|
||||||
|
class CloudServerHE {
|
||||||
|
-identifier, e.g. "vm1234"
|
||||||
|
-caption := bi.caption?
|
||||||
|
-parentAsset := parentHost
|
||||||
|
-identifier := serverName
|
||||||
|
-create()
|
||||||
|
}
|
||||||
|
class ManagedServerHE {
|
||||||
|
-identifier, e.g. "vm1234"
|
||||||
|
-caption := bi.caption?
|
||||||
|
-parentAsset := parentHost
|
||||||
|
-identifier := serverName
|
||||||
|
~config = [
|
||||||
|
⠀⠀+installed Software
|
||||||
|
]
|
||||||
|
-create()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Hosting {
|
||||||
|
class ManagedWebspaceHE {
|
||||||
|
-parentAsset := parentManagedServer
|
||||||
|
-identifier : webspaceName
|
||||||
|
+caption
|
||||||
|
|
||||||
|
-create()
|
||||||
|
}
|
||||||
|
|
||||||
|
class UnixUserHE {
|
||||||
|
+identifier ["xyz00-..."]
|
||||||
|
+caption
|
||||||
|
~config = [
|
||||||
|
⠀⠀+SSD Soft Quota
|
||||||
|
⠀⠀+SSD Hard Quota
|
||||||
|
⠀⠀+HDD Soft Quota
|
||||||
|
⠀⠀+HDD Hard Quota
|
||||||
|
⠀⠀#shell
|
||||||
|
⠀⠀#password
|
||||||
|
]
|
||||||
|
|
||||||
|
+create()
|
||||||
|
}
|
||||||
|
class DomainDNSSetupHE {
|
||||||
|
+identifier, e.g. "example.com"
|
||||||
|
+caption
|
||||||
|
|
||||||
|
+create()
|
||||||
|
}
|
||||||
|
class DomainHttpSetupHE {
|
||||||
|
+identifier, e.g. "example.com"
|
||||||
|
+caption
|
||||||
|
|
||||||
|
+create()
|
||||||
|
}
|
||||||
|
class DomainEMailSetupHE {
|
||||||
|
+identifier, e.g. "example.com"
|
||||||
|
+caption
|
||||||
|
|
||||||
|
+create()
|
||||||
|
}
|
||||||
|
class EMailAliasHE {
|
||||||
|
+identifier, e.g "xyz00-..."
|
||||||
|
+caption
|
||||||
|
|
||||||
|
~config = [
|
||||||
|
⠀⠀+target[]
|
||||||
|
]
|
||||||
|
|
||||||
|
+create()
|
||||||
|
}
|
||||||
|
class EMailAddressHE {
|
||||||
|
+identifier, e.g. "test@example.org"
|
||||||
|
+caption
|
||||||
|
~config = [
|
||||||
|
⠀⠀+sub-domain
|
||||||
|
⠀⠀+local-part
|
||||||
|
⠀⠀+target
|
||||||
|
]
|
||||||
|
|
||||||
|
+create()
|
||||||
|
}
|
||||||
|
class MariaDBUserHE {
|
||||||
|
+identifier, e.g. "xyz00_mydb"
|
||||||
|
+caption
|
||||||
|
config = [
|
||||||
|
⠀⠀#password
|
||||||
|
]
|
||||||
|
|
||||||
|
+create()
|
||||||
|
}
|
||||||
|
class MariaDBHE {
|
||||||
|
+identifier, e.g. "xyz00_mydb"
|
||||||
|
+caption
|
||||||
|
~config = [
|
||||||
|
⠀⠀+encoding
|
||||||
|
]
|
||||||
|
|
||||||
|
+create()
|
||||||
|
}
|
||||||
|
class PostgresDBUserHE {
|
||||||
|
+identifier, e.g. "xyz00_mydb"
|
||||||
|
+caption
|
||||||
|
~config = [
|
||||||
|
⠀⠀#password
|
||||||
|
]
|
||||||
|
|
||||||
|
+create()
|
||||||
|
}
|
||||||
|
class PostgresDBHE {
|
||||||
|
+identifier, e.g. "xyz00_mydb"
|
||||||
|
+caption
|
||||||
|
|
||||||
|
~config = [
|
||||||
|
⠀⠀+encoding
|
||||||
|
⠀⠀+extensions
|
||||||
|
]
|
||||||
|
+create()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
style CloudServerHE stroke:orange,stroke-width:4px
|
||||||
|
style ManagedServerHE stroke:orange,stroke-width:4px
|
||||||
|
style ManagedWebspaceHE stroke:orange,stroke-width:4px
|
||||||
|
style UnixUserHE stroke:blue,stroke-width:4px
|
||||||
|
style DomainDNSSetupHE stroke:blue,stroke-width:4px
|
||||||
|
style DomainHttpSetupHE stroke:blue,stroke-width:4px
|
||||||
|
style DomainEMailSetupHE stroke:blue,stroke-width:4px
|
||||||
|
style EMailAliasHE stroke:blue,stroke-width:4px
|
||||||
|
style EMailAddressHE stroke:blue,stroke-width:4px
|
||||||
|
style MariaDBUserHE stroke:blue,stroke-width:4px
|
||||||
|
style MariaDBHE stroke:blue,stroke-width:4px
|
||||||
|
style PostgresDBUserHE stroke:blue,stroke-width:4px
|
||||||
|
style PostgresDBHE stroke:blue,stroke-width:4px
|
||||||
|
|
||||||
|
%% --------------------------------------
|
||||||
|
|
||||||
|
ParentA o-- ChildA : can contain
|
||||||
|
ParentB *-- ChildB : contains
|
||||||
|
|
||||||
|
namespace Agenda {
|
||||||
|
class ParentA {
|
||||||
|
}
|
||||||
|
class ChildA {
|
||||||
|
}
|
||||||
|
class ParentB {
|
||||||
|
}
|
||||||
|
class ChildB {
|
||||||
|
}
|
||||||
|
class CreatedByClient {
|
||||||
|
}
|
||||||
|
class CreatedAutomatically {
|
||||||
|
}
|
||||||
|
class SomeEntity {
|
||||||
|
~patchable = [
|
||||||
|
%% the following indentations uses two U+2800 to have effect in the rendered diagram
|
||||||
|
⠀⠀+first
|
||||||
|
⠀⠀+second
|
||||||
|
]
|
||||||
|
-readOnly for client accounts
|
||||||
|
+readWrite for client accounts
|
||||||
|
#writeOnly
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
style CreatedByClient stroke:blue,stroke-width:4px
|
||||||
|
style CreatedAutomatically stroke:orange,stroke-width:4px
|
||||||
|
end
|
||||||
|
```
|
468
doc/rbac-performance-analysis.md
Normal file
468
doc/rbac-performance-analysis.md
Normal file
@ -0,0 +1,468 @@
|
|||||||
|
# RBAC Performance Analysis
|
||||||
|
|
||||||
|
This describes the analysis of the legacy-data-import which took way too long, which turned out to be a problem in the RBAC-access-rights-check as well as `EntityManager.persist` creating too many SQL queries.
|
||||||
|
|
||||||
|
|
||||||
|
## Our Performance-Problem
|
||||||
|
|
||||||
|
During the legacy data import for hosting assets we noticed massive performance problems. The import of about 2200 hosting-assets (IP-numbers, managed-webspaces, managed- and cloud-servers) as well as the creation of booking-items and booking-projects as well as necessary office-data entities (persons, contacts, partners, debitors, relations) **took 25 minutes**.
|
||||||
|
|
||||||
|
Importing hosting assets up to UnixUsers and EmailAddresses even **took about 100 minutes**.
|
||||||
|
|
||||||
|
(The office data import sometimes, but rarely, took only 10min.
|
||||||
|
We could not find a pattern, why that was the case. The impression that it had to do with too many other parallel processes, e.g. browser with BBB or IntelliJ IDEA was proved wrong, but stopping all unnecessary processes and performing the import again.)
|
||||||
|
|
||||||
|
|
||||||
|
## Preparation
|
||||||
|
|
||||||
|
### Configuring PostgreSQL
|
||||||
|
|
||||||
|
The pg_stat_statements PostgreSQL-Extension can be used to measure how long queries take and how often they are called.
|
||||||
|
|
||||||
|
The module auto_explain can be used to automatically run EXPLAIN on long-running queries.
|
||||||
|
|
||||||
|
To use this extension and module, we extended the PostgreSQL-Docker-image:
|
||||||
|
|
||||||
|
```Dockerfile
|
||||||
|
FROM postgres:15.5-bookworm
|
||||||
|
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y postgresql-contrib && \
|
||||||
|
apt-get clean
|
||||||
|
|
||||||
|
COPY etc/postgresql-log-slow-queries.conf /etc/postgresql/postgresql.conf
|
||||||
|
```
|
||||||
|
|
||||||
|
And create an image from it:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker build -t postgres-with-contrib:15.5-bookworm .
|
||||||
|
```
|
||||||
|
|
||||||
|
Then we created a config file for PostgreSQL in `etc/postgresql-log-slow-queries.conf`:
|
||||||
|
|
||||||
|
```
|
||||||
|
shared_preload_libraries = 'pg_stat_statements,auto_explain'
|
||||||
|
log_min_duration_statement = 1000
|
||||||
|
log_statement = 'all'
|
||||||
|
log_duration = on
|
||||||
|
pg_stat_statements.track = all
|
||||||
|
auto_explain.log_min_duration = '1s' # Logs queries taking longer than 1 second
|
||||||
|
auto_explain.log_analyze = on # Include actual run times
|
||||||
|
auto_explain.log_buffers = on # Include buffer usage statistics
|
||||||
|
auto_explain.log_format = 'json' # Format the log output in JSON
|
||||||
|
listen_addresses = '*'
|
||||||
|
```
|
||||||
|
|
||||||
|
And a Docker-Compose config in 'docker-compose.yml':
|
||||||
|
|
||||||
|
```
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres-with-contrib:15.5-bookworm
|
||||||
|
container_name: custom-postgres
|
||||||
|
environment:
|
||||||
|
POSTGRES_PASSWORD: password
|
||||||
|
volumes:
|
||||||
|
- /home/mi/Projekte/Hostsharing/hsadmin-ng/etc/postgresql-log-slow-queries.conf:/etc/postgresql/postgresql.conf
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
command:
|
||||||
|
- bash
|
||||||
|
- -c
|
||||||
|
- >
|
||||||
|
apt-get update &&
|
||||||
|
apt-get install -y postgresql-contrib &&
|
||||||
|
docker-entrypoint.sh postgres -c config_file=/etc/postgresql/postgresql.conf
|
||||||
|
```
|
||||||
|
|
||||||
|
### Activate the pg_stat_statements Extension
|
||||||
|
|
||||||
|
The pg_stat_statements extension was activated in our Liquibase-scripts:
|
||||||
|
|
||||||
|
```
|
||||||
|
create extension if not exists "pg_stat_statements";
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running the Tweaked PostgreSQL
|
||||||
|
|
||||||
|
Now we can run PostgreSQL with activated slow-query-logging:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running the Import
|
||||||
|
|
||||||
|
Using an environment like this:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
export HSADMINNG_POSTGRES_JDBC_URL=jdbc:postgresql://localhost:5432/postgres
|
||||||
|
export HSADMINNG_POSTGRES_ADMIN_USERNAME=postgres
|
||||||
|
export HSADMINNG_POSTGRES_ADMIN_PASSWORD=password
|
||||||
|
export HSADMINNG_POSTGRES_RESTRICTED_USERNAME=restricted
|
||||||
|
export HSADMINNG_SUPERUSER=superuser-alex@hostsharing.net
|
||||||
|
```
|
||||||
|
|
||||||
|
We can now run the hosting-assets-import:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
time gw-importHostingAssets
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fetch the Query Statistics
|
||||||
|
|
||||||
|
And afterward we can query the statistics in PostgreSQL, e.g.:
|
||||||
|
|
||||||
|
```SQL
|
||||||
|
WITH statements AS (
|
||||||
|
SELECT * FROM pg_stat_statements pss
|
||||||
|
)
|
||||||
|
SELECT calls,
|
||||||
|
total_exec_time::int/(60*1000) as total_mins,
|
||||||
|
mean_exec_time::int as mean_millis,
|
||||||
|
query
|
||||||
|
FROM statements
|
||||||
|
WHERE calls > 100 AND shared_blks_hit > 0
|
||||||
|
ORDER BY total_exec_time DESC
|
||||||
|
LIMIT 16;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reset the Query Statistics
|
||||||
|
|
||||||
|
```SQL
|
||||||
|
SELECT pg_stat_statements_reset();
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Analysis Result
|
||||||
|
|
||||||
|
### RBAC-Access-Rights Detection query
|
||||||
|
|
||||||
|
This CTE query was run over 4000 times during a single import and takes in total the whole execution time of the import process:
|
||||||
|
|
||||||
|
```SQL
|
||||||
|
WITH RECURSIVE grants AS (
|
||||||
|
SELECT descendantUuid, ascendantUuid, $5 AS level
|
||||||
|
FROM RbacGrants
|
||||||
|
WHERE assumed
|
||||||
|
AND ascendantUuid = any(subjectIds)
|
||||||
|
UNION ALL
|
||||||
|
SELECT g.descendantUuid, g.ascendantUuid, grants.level + $6 AS level
|
||||||
|
FROM RbacGrants g
|
||||||
|
INNER JOIN grants ON grants.descendantUuid = g.ascendantUuid
|
||||||
|
WHERE g.assumed
|
||||||
|
),
|
||||||
|
granted AS (
|
||||||
|
SELECT DISTINCT descendantUuid
|
||||||
|
FROM grants
|
||||||
|
)
|
||||||
|
SELECT DISTINCT perm.objectUuid
|
||||||
|
FROM granted
|
||||||
|
JOIN RbacPermission perm ON granted.descendantUuid = perm.uuid
|
||||||
|
JOIN RbacObject obj ON obj.uuid = perm.objectUuid
|
||||||
|
WHERE (requiredOp = $7 OR perm.op = requiredOp)
|
||||||
|
AND obj.objectTable = forObjectTable
|
||||||
|
LIMIT maxObjects+$8
|
||||||
|
```
|
||||||
|
|
||||||
|
That query is used to determine access rights of the currently active RBAC-subject(s).
|
||||||
|
|
||||||
|
We used `EXPLAIN` with a concrete version (parameters substituted with real values) of that query and got this result:
|
||||||
|
|
||||||
|
```
|
||||||
|
QUERY PLAN
|
||||||
|
Limit (cost=6549.08..6549.35 rows=54 width=16)
|
||||||
|
CTE grants
|
||||||
|
-> Recursive Union (cost=4.32..5845.97 rows=1103 width=36)
|
||||||
|
-> Bitmap Heap Scan on rbacgrants (cost=4.32..15.84 rows=3 width=36)
|
||||||
|
Recheck Cond: (ascendantuuid = ANY ('{ad1133dc-fbb7-43c9-8c20-0da3f89a2388}'::uuid[]))
|
||||||
|
Filter: assumed
|
||||||
|
-> Bitmap Index Scan on rbacgrants_ascendantuuid_idx (cost=0.00..4.32 rows=3 width=0)
|
||||||
|
Index Cond: (ascendantuuid = ANY ('{ad1133dc-fbb7-43c9-8c20-0da3f89a2388}'::uuid[]))
|
||||||
|
-> Nested Loop (cost=0.29..580.81 rows=110 width=36)
|
||||||
|
-> WorkTable Scan on grants grants_1 (cost=0.00..0.60 rows=30 width=20)
|
||||||
|
-> Index Scan using rbacgrants_ascendantuuid_idx on rbacgrants g (cost=0.29..19.29 rows=4 width=32)
|
||||||
|
Index Cond: (ascendantuuid = grants_1.descendantuuid)
|
||||||
|
Filter: assumed
|
||||||
|
-> Unique (cost=703.11..703.38 rows=54 width=16)
|
||||||
|
-> Sort (cost=703.11..703.25 rows=54 width=16)
|
||||||
|
Sort Key: perm.objectuuid
|
||||||
|
-> Nested Loop (cost=31.60..701.56 rows=54 width=16)
|
||||||
|
-> Hash Join (cost=31.32..638.78 rows=200 width=16)
|
||||||
|
Hash Cond: (perm.uuid = grants.descendantuuid)
|
||||||
|
-> Seq Scan on rbacpermission perm (cost=0.00..532.92 rows=28392 width=32)
|
||||||
|
-> Hash (cost=28.82..28.82 rows=200 width=16)
|
||||||
|
-> HashAggregate (cost=24.82..26.82 rows=200 width=16)
|
||||||
|
Group Key: grants.descendantuuid
|
||||||
|
-> CTE Scan on grants (cost=0.00..22.06 rows=1103 width=16)
|
||||||
|
-> Index Only Scan using rbacobject_objecttable_uuid_key on rbacobject obj (cost=0.28..0.31 rows=1 width=16)
|
||||||
|
Index Cond: ((objecttable = 'hs_hosting.asset'::text) AND (uuid = perm.objectuuid))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Office-Relation-Query
|
||||||
|
|
||||||
|
```SQL
|
||||||
|
SELECT hore1_0.uuid,a1_0.uuid,a1_0.familyname,a1_0.givenname,a1_0.persontype,a1_0.salutation,a1_0.title,a1_0.tradename,a1_0.version,c1_0.uuid,c1_0.caption,c1_0.emailaddresses,c1_0.phonenumbers,c1_0.postaladdress,c1_0.version,h1_0.uuid,h1_0.familyname,h1_0.givenname,h1_0.persontype,h1_0.salutation,h1_0.title,h1_0.tradename,h1_0.version,hore1_0.mark,hore1_0.type,hore1_0.version
|
||||||
|
FROM hs_office.relation_rv hore1_0
|
||||||
|
LEFT JOIN hs_office.person_rv a1_0 ON a1_0.uuid=hore1_0.anchoruuid
|
||||||
|
LEFT JOIN hs_office.contact_rv c1_0 ON c1_0.uuid=hore1_0.contactuuid
|
||||||
|
LEFT JOIN hs_office.person_rv h1_0 ON h1_0.uuid=hore1_0.holderuuid
|
||||||
|
WHERE hore1_0.uuid=$1
|
||||||
|
```
|
||||||
|
|
||||||
|
That query on the `hs_office.relation_rv`-table joins the three references anchor-person, holder-person and contact.
|
||||||
|
|
||||||
|
|
||||||
|
### Total-Query-Time > Total-Import-Runtime
|
||||||
|
|
||||||
|
That both queries total up to more than the runtime of the import-process is most likely due to internal parallel query processing.
|
||||||
|
|
||||||
|
|
||||||
|
## Attempts to Mitigate the Problem
|
||||||
|
|
||||||
|
### VACUUM ANALYZE
|
||||||
|
|
||||||
|
In the middle of the import, we updated the PostgreSQL statistics to recalibrate the query optimizer:
|
||||||
|
|
||||||
|
```SQL
|
||||||
|
VACUUM ANALYZE;
|
||||||
|
```
|
||||||
|
|
||||||
|
This did not improve the performance.
|
||||||
|
|
||||||
|
|
||||||
|
### Improving Joins + Indexes
|
||||||
|
|
||||||
|
We were suspicious about the sequential scan over all `rbacpermission` rows which was done by PostgreSQL to execute a HashJoin strategy. Turning off that strategy by
|
||||||
|
|
||||||
|
```SQL
|
||||||
|
ALTER FUNCTION rbac.queryAccessibleObjectUuidsOfSubjectIds SET enable_hashjoin = off;
|
||||||
|
```
|
||||||
|
|
||||||
|
did not improve the performance though. The HashJoin was actually still applied, but no full table scan anymore:
|
||||||
|
|
||||||
|
```
|
||||||
|
[...]
|
||||||
|
QUERY PLAN
|
||||||
|
-> Hash Join (cost=36.02..40.78 rows=1 width=16)
|
||||||
|
Hash Cond: (grants.descendantuuid = perm.uuid)
|
||||||
|
-> HashAggregate (cost=13.32..15.32 rows=200 width=16)
|
||||||
|
Group Key: grants.descendantuuid
|
||||||
|
-> CTE Scan on grants (cost=0.00..11.84 rows=592 width=16)
|
||||||
|
[...]
|
||||||
|
```
|
||||||
|
|
||||||
|
The HashJoin strategy could be great if the hash-map could be kept for multiple invocations. But during an import process, of course, there are always new rows in the underlying table and the hash-map would be outdated immediately.
|
||||||
|
|
||||||
|
Also creating indexes which should suppor the RBAC query, like the following, did not improve performance:
|
||||||
|
|
||||||
|
```SQL
|
||||||
|
create index on RbacPermission (objectUuid, op);
|
||||||
|
create index on RbacPermission (opTableName, op);
|
||||||
|
```
|
||||||
|
|
||||||
|
### LAZY loading for Relation.anchorPerson/.holderPerson/
|
||||||
|
|
||||||
|
At this point, the import took 21mins with these statistics:
|
||||||
|
|
||||||
|
| query | calls | total_m | mean_ms |
|
||||||
|
|-------|-------|---------|---------|
|
||||||
|
| select hore1_0.uuid,a1_0.uuid,a1_0.familyname,a1_0.givenname,a1_0.persontype,a1_0.salutation,a1_0.title,a1_0.tradename,a1_0.version,c1_0.uuid,c1_0.caption,c1_0.emailaddresses,c1_0.phonenumbers,c1_0.postaladdress, c1_0.version,h1_0.uuid,h1_0.familyname,h1_0.givenname,h1_0.persontype,h1_0.salutation,h1_0.title,h1_0.tradename,h1_0.version,hore1_0.mark,hore1_0.type,hore1_0.version from public.hs_office.relation_rv hore1_0 left join public.hs_office.person_rv a1_0 on a1_0.uuid=hore1_0.anchoruuid left join public.hs_office.contact_rv c1_0 on c1_0.uuid=hore1_0.contactuuid left join public.hs_office.person_rv h1_0 on h1_0.uuid=hore1_0.holderuuid where hore1_0.uuid=$1 | 517 | 11 | 1282 |
|
||||||
|
| select hope1_0.uuid,hope1_0.familyname,hope1_0.givenname,hope1_0.persontype,hope1_0.salutation,hope1_0.title,hope1_0.tradename,hope1_0.version from public.hs_office.person_rv hope1_0 where hope1_0.uuid=$1 | 973 | 4 | 254 |
|
||||||
|
| select hoce1_0.uuid,hoce1_0.caption,hoce1_0.emailaddresses,hoce1_0.phonenumbers,hoce1_0.postaladdress,hoce1_0.version from public.hs_office.contact_rv hoce1_0 where hoce1_0.uuid=$1 | 973 | 4 | 253 |
|
||||||
|
| call rbac.grantRoleToRole(roleUuid, superRoleUuid, superRoleDesc.assumed) | 31316 | 0 | 1 |
|
||||||
|
| call buildRbacSystemForHsHostingAsset(NEW) | 2258 | 0 | 7 |
|
||||||
|
| select * from rbac.isGranted(array[granteeId], grantedId) | 44613 | 0 | 0 |
|
||||||
|
| insert into public.hs_hosting.asset_rv (alarmcontactuuid,assignedtoassetuuid,bookingitemuuid,caption,config,identifier,parentassetuuid,type,version,uuid) values ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) | 2207 | 0 | 7 |
|
||||||
|
| insert into hs_hosting.asset (alarmcontactuuid, version, bookingitemuuid, type, parentassetuuid, assignedtoassetuuid, config, uuid, identifier, caption) values (new.alarmcontactuuid, new. version, new. bookingitemuuid, new. type, new. parentassetuuid, new. assignedtoassetuuid, new. config, new. uuid, new. identifier, new. caption) returning * | 2207 | 0 | 7 |
|
||||||
|
| insert into public.hs_office.relation_rv (anchoruuid,contactuuid,holderuuid,mark,type,version,uuid) values ($1,$2,$3,$4,$5,$6,$7) | 1261 | 0 | 9 |
|
||||||
|
| insert into hs_office.relation (uuid, version, anchoruuid, holderuuid, contactuuid, type, mark) values (new.uuid, new. version, new. anchoruuid, new. holderuuid, new. contactuuid, new. type, new. mark) returning * | 1261 | 0 | 9 |
|
||||||
|
| call buildRbacSystemForHsOfficeRelation(NEW) | 1276 | 0 | 8 |
|
||||||
|
| with recursive grants as ( select descendantUuid, ascendantUuid from RbacGrants where descendantUuid = grantedId union all select ""grant"".descendantUuid, ""grant"".ascendantUuid from RbacGrants ""grant"" inner join grants recur on recur.ascendantUuid = ""grant"".descendantUuid ) select exists ( select $3 from grants where ascendantUuid = any(granteeIds) ) or grantedId = any(granteeIds) | 47540 | 0 | 0 |
|
||||||
|
| insert into RbacGrants (grantedByTriggerOf, ascendantuuid, descendantUuid, assumed) values (currentTriggerObjectUuid(), superRoleId, subRoleId, doAssume) on conflict do nothing" | 40472 | 0 | 0 |
|
||||||
|
| insert into public.hs_booking.item_rv (caption,parentitemuuid,projectuuid,resources,type,validity,version,uuid) values ($1,$2,$3,$4,$5,$6,$7,$8) | 926 | 0 | 7 |
|
||||||
|
| insert into hs_booking.item (resources, version, projectuuid, type, parentitemuuid, validity, uuid, caption) values (new.resources, new. version, new. projectuuid, new. type, new. parentitemuuid, new. validity, new. uuid, new. caption) returning * | 926 | 0 | 7 |
|
||||||
|
|
||||||
|
|
||||||
|
The slowest query now was fetching Relations joined with Contact, Anchor-Person and Holder-Person, for all tables using the restricted (RBAC) views (_rv).
|
||||||
|
|
||||||
|
We changed these mappings from `EAGER` (default) to `LAZY` to `@ManyToOne(fetch = FetchType.LAZY)` and got this result:
|
||||||
|
|
||||||
|
:::small
|
||||||
|
| query | calls | total (min) | mean (ms) |
|
||||||
|
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------|-------------|----------|
|
||||||
|
| select hope1_0.uuid,hope1_0.familyname,hope1_0.givenname,hope1_0.persontype,hope1_0.salutation,hope1_0.title,hope1_0.tradename,hope1_0.version from public.hs_office.person_rv hope1_0 where hope1_0.uuid=$1 | 1015 | 4 | 238 |
|
||||||
|
| select hore1_0.uuid,hore1_0.anchoruuid,hore1_0.contactuuid,hore1_0.holderuuid,hore1_0.mark,hore1_0.type,hore1_0.version from public.hs_office.relation_rv hore1_0 where hore1_0.uuid=$1 | 517 | 4 | 439 |
|
||||||
|
| select hoce1_0.uuid,hoce1_0.caption,hoce1_0.emailaddresses,hoce1_0.phonenumbers,hoce1_0.postaladdress,hoce1_0.version from public.hs_office.contact_rv hoce1_0 where hoce1_0.uuid=$1 | 497 | 2 | 213 |
|
||||||
|
| call rbac.grantRoleToRole(roleUuid, superRoleUuid, superRoleDesc.assumed) | 31316 | 0 | 1 |
|
||||||
|
| select * from rbac.isGranted(array[granteeId], grantedId) | 44613 | 0 | 0 |
|
||||||
|
| call buildRbacSystemForHsHostingAsset(NEW) | 2258 | 0 | 7 |
|
||||||
|
| insert into public.hs_hosting.asset_rv (alarmcontactuuid,assignedtoassetuuid,bookingitemuuid,caption,config,identifier,parentassetuuid,type,version,uuid) values ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) | 2207 | 0 | 7 |
|
||||||
|
| insert into hs_hosting.asset (alarmcontactuuid, version, bookingitemuuid, type, parentassetuuid, assignedtoassetuuid, config, uuid, identifier, caption) values (new.alarmcontactuuid, new. version, new. bookingitemuuid, new. type, new. parentassetuuid, new. assignedtoassetuuid, new. config, new. uuid, new. identifier, new. caption) returning * | 2207 | 0 | 7 |
|
||||||
|
| with recursive grants as ( select descendantUuid, ascendantUuid from RbacGrants where descendantUuid = grantedId union all select ""grant"".descendantUuid, ""grant"".ascendantUuid from RbacGrants ""grant"" inner join grants recur on recur.ascendantUuid = ""grant"".descendantUuid ) select exists ( select $3 from grants where ascendantUuid = any(granteeIds) ) or grantedId = any(granteeIds) | 47538 | 0 | 0 |
|
||||||
|
insert into public.hs_office.relation_rv (anchoruuid,contactuuid,holderuuid,mark,type,version,uuid) values ($1,$2,$3,$4,$5,$6,$7) | 1261 | 0 | 8 |
|
||||||
|
| insert into hs_office.relation (uuid, version, anchoruuid, holderuuid, contactuuid, type, mark) values (new.uuid, new. version, new. anchoruuid, new. holderuuid, new. contactuuid, new. type, new. mark) returning * | 1261 | 0 | 8 |
|
||||||
|
| call buildRbacSystemForHsOfficeRelation(NEW) | 1276 | 0 | 7 |
|
||||||
|
| insert into public.hs_booking.item_rv (caption,parentitemuuid,projectuuid,resources,type,validity,version,uuid) values ($1,$2,$3,$4,$5,$6,$7,$8) | 926 | 0 | 7 |
|
||||||
|
| insert into hs_booking.item (resources, version, projectuuid, type, parentitemuuid, validity, uuid, caption) values (new.resources, new. version, new. projectuuid, new. type, new. parentitemuuid, new. validity, new. uuid, new. caption) returning * | 926 | 0 | 7 |
|
||||||
|
insert into RbacGrants (grantedByTriggerOf, ascendantuuid, descendantUuid, assumed) values (currentTriggerObjectUuid(), superRoleId, subRoleId, doAssume) on conflict do nothing | 40472 | 0 | 0 |
|
||||||
|
|
||||||
|
Now, finally, the total runtime of the import was down to 12 minutes. This is repeatable, where originally, the import took about 25mins in most cases and just rarely - and for unknown reasons - 10min.
|
||||||
|
|
||||||
|
### Importing UnixUser and EmailAlias Assets
|
||||||
|
|
||||||
|
But once UnixUser and EmailAlias assets got added to the import, the total time went up to about 110min.
|
||||||
|
|
||||||
|
This was not acceptable, especially not, considering that domains, email-addresses and database-assets are almost 10 times that number and thus the import would go up to over 1100min which is 20 hours.
|
||||||
|
|
||||||
|
In a first step, a `HsHostingAssetRawEntity` was created, mapped to the raw table (hs_hosting.asset) not to the RBAC-view (hs_hosting.asset_rv). Unfortunately we did not keep measurements, but that was only part of the problem anyway.
|
||||||
|
|
||||||
|
The main problem was, that there is something strange with persisting (`EntityManager.persist`) for EmailAlias assets. Where importing UnixUsers was mostly slow due to RBAC SELECT-permission checks, persisting EmailAliases suddenly created about a million (in numbers 1.000.000) SQL UPDATE statements after the INSERT, all with the same data, just increased version number (used for optimistic locking). We were not able to figure out why this happened.
|
||||||
|
|
||||||
|
Keep in mind, it's the same table with the same RBAC-triggers, just a different value in the type column.
|
||||||
|
|
||||||
|
Once `EntityManager.persist` was replaced by an explicit SQL INSERT - just for `HsHostingAssetRawEntity`, the total time was down to 17min. Thus importing the UnixUsers and EmailAliases took just 5min, which is an acceptable result. The total import of all HostingAssets is now estimated to about 1 hour (on my developer laptop).
|
||||||
|
|
||||||
|
Now, the longest running queries are these:
|
||||||
|
|
||||||
|
| No.| calls | total_m | mean_ms | query |
|
||||||
|
|---:|---------|--------:|--------:|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
|
| 1 | 13.093 | 4 | 21 | insert into hs_hosting.asset( uuid, type, bookingitemuuid, parentassetuuid, assignedtoassetuuid, alarmcontactuuid, identifier, caption, config, version) values ( $1, $2, $3, $4, $5, $6, $7, $8, cast($9 as jsonb), $10) |
|
||||||
|
| 2 | 517 | 4 | 502 | select hore1_0.uuid,hore1_0.anchoruuid,hore1_0.contactuuid,hore1_0.holderuuid,hore1_0.mark,hore1_0.type,hore1_0.version from public.hs_office.relation_rv hore1_0 where hore1_0.uuid=$1 |
|
||||||
|
| 3 | 13.144 | 4 | 21 | call buildRbacSystemForHsHostingAsset(NEW) |
|
||||||
|
| 4 | 96.632 | 3 | 2 | call rbac.grantRoleToRole(roleUuid, superRoleUuid, superRoleDesc.assumed) |
|
||||||
|
| 5 | 120.815 | 3 | 2 | select * from rbac.isGranted(array[granteeId], grantedId) |
|
||||||
|
| 6 | 123.740 | 3 | 2 | with recursive grants as ( select descendantUuid, ascendantUuid from RbacGrants where descendantUuid = grantedId union all select "grant".descendantUuid, "grant".ascendantUuid from RbacGrants "grant" inner join grants recur on recur.ascendantUuid = "grant".descendantUuid ) select exists ( select $3 from grants where ascendantUuid = any(granteeIds) ) or grantedId = any(granteeIds) |
|
||||||
|
| 7 | 497 | 2 | 259 | select hoce1_0.uuid,hoce1_0.caption,hoce1_0.emailaddresses,hoce1_0.phonenumbers,hoce1_0.postaladdress,hoce1_0.version from public.hs_office.contact_rv hoce1_0 where hoce1_0.uuid=$1 |
|
||||||
|
| 8 | 497 | 2 | 255 | select hope1_0.uuid,hope1_0.familyname,hope1_0.givenname,hope1_0.persontype,hope1_0.salutation,hope1_0.title,hope1_0.tradename,hope1_0.version from public.hs_office.person_rv hope1_0 where hope1_0.uuid=$1 |
|
||||||
|
| 9 | 13.144 | 1 | 8 | SELECT createRoleWithGrants( hs_hosting.asset_TENANT(NEW), permissions => array[$7], incomingSuperRoles => array[ hs_hosting.asset_AGENT(NEW), hs_office.contact_ADMIN(newAlarmContact)], outgoingSubRoles => array[ hs_booking.item_TENANT(newBookingItem), hs_hosting.asset_TENANT(newParentAsset)] ) |
|
||||||
|
| 10 | 13.144 | 1 | 5 | SELECT createRoleWithGrants( hs_hosting.asset_ADMIN(NEW), permissions => array[$7], incomingSuperRoles => array[ hs_booking.item_AGENT(newBookingItem), hs_hosting.asset_AGENT(newParentAsset), hs_hosting.asset_OWNER(NEW)] ) |
|
||||||
|
|
||||||
|
That the `INSERT into hs_hosting.asset` (No. 1) takes up the most time, seems to be normal, and 21ms for each call is also fine.
|
||||||
|
|
||||||
|
It seems that the trigger effects (eg. No. 3 and No. 4) are included in the measure for the causing INSERT, otherwise summing up the totals would exceed the actual total time of the whole import. And it was to be expected that building the RBAC rules for new business objects takes most of the time.
|
||||||
|
|
||||||
|
In production, the `SELECT ... FROM hs_office.relation_rv` (No. 2) with about 0.5 seconds could still be a problem. But once we apply the improvements from the hosting asset area also to the office area, this should not be a problem for the import anymore.
|
||||||
|
|
||||||
|
|
||||||
|
## Further Options To Explore
|
||||||
|
|
||||||
|
1. Instead of separate SQL INSERT statements, we could try bulk INSERT.
|
||||||
|
2. We could use the SQL INSERT method for all entity-classes, or at least for all which have high row counts.
|
||||||
|
3. For the production code, we could use raw-entities for referenced entities, here usually RBAC SELECT permission is given anyway.
|
||||||
|
|
||||||
|
|
||||||
|
## The Problematically Huge Join
|
||||||
|
|
||||||
|
The origin problem was the expensive RBAC check for many SELECT queries.
|
||||||
|
This consists of two parts:
|
||||||
|
|
||||||
|
1. The recursive CTE query to determine which object's UUIDs are visible for the current subject.
|
||||||
|
This query itself takes currently about 250ms thus is no problem by itself as long as we only need it once per request.
|
||||||
|
2. Joining the result from 1. with the result if a business query.
|
||||||
|
The performance of the business query itself is no problem, for the join see the following explanations.
|
||||||
|
|
||||||
|
Superusers can see all objects (currently already over 90.000)
|
||||||
|
and even high level roles of customers with many hosting assets can see several thousand objects.
|
||||||
|
This is the one side of that problematic join.
|
||||||
|
|
||||||
|
The other side of that problematic is the result of the business query.
|
||||||
|
For example if a user wants to select all of their e-mail-addresses, that might easily half of the visible objects.
|
||||||
|
|
||||||
|
Thus, we would have a join of for example 5.000 x 2.500 rows, which is going to be slow.
|
||||||
|
As there are currently about 84.000 objects are hosting assets and 33.000 e-mail-addresses in our system,
|
||||||
|
for a superuser we would even run into an 84.0000 x 33.0000 join.
|
||||||
|
|
||||||
|
We found some solution approaches:
|
||||||
|
|
||||||
|
1. Getting rid of the `rbacrole` and `rbacpermission` table and only having implicit roles with implicit grants (OWNER->ADMIN->AGENT->TENENT->REFERRER) by comparison of ordered enum values and fixed permission assignments (e.g. OWENER->DELETE, ADMIN->UPDATE etc.). We could also get rid of the table `rbacreferece` if we enter users as business objects.
|
||||||
|
|
||||||
|
This should dramatically reduce the size of the table `rbackgrant` as well as the recusion levels.
|
||||||
|
|
||||||
|
But since we only apply this query once for each business query, that would only improve performance once we have way more objects in our system, but does not help our current problem.
|
||||||
|
|
||||||
|
It's quite some effort to implement even just a prototype, so we did not further explore this idea.
|
||||||
|
|
||||||
|
2. Adding the object type to the table `rbacObject` to reduce the size of the result of the recursive CTE query.
|
||||||
|
|
||||||
|
See chapter below.
|
||||||
|
|
||||||
|
3. Inverting the recursion of the CTE-query, combined with the type condition.
|
||||||
|
|
||||||
|
Instead of starting the recursion with `currentSubjectOrAssumedRolesUuids()`,
|
||||||
|
we could start it with the target table name and row-type,
|
||||||
|
then recurse down to the `currentSubjectOrAssumedRolesUuids()`.
|
||||||
|
|
||||||
|
In the end, we need the object UUIDs, though.
|
||||||
|
But if we start with the join of `rbacObject` with `rbacPermission`,
|
||||||
|
we need to forward the object UUIDs through the whole recursion.
|
||||||
|
|
||||||
|
This idea was not yet further explored.
|
||||||
|
|
||||||
|
|
||||||
|
### Adding The Object Type To The Table `rbacObject`
|
||||||
|
|
||||||
|
This optimization idea came from Michael Hierweck and was promising.
|
||||||
|
The idea is to reduce the size of the result of the recursive CTE query and maybe even speed up that query itself.
|
||||||
|
|
||||||
|
To evaluate this, I added a type column to the `rbacObject` table, initially as an enum hsHostingAssetType. Then I entered the type there for all rows from hs_hosting.asset. This means that 83,886 of 92,545 rows in `rbacobject` have a type set, leaving 8,659 without.
|
||||||
|
|
||||||
|
If we do this for other types (we currently have 1,271 relations and 927 booking items), it gets more complicated because they are different enum types. As varchar(16), we could lose performance again due to the higher storage space requirements.
|
||||||
|
|
||||||
|
But the performance gained is not particularly high anyway.
|
||||||
|
See the average seconds per recursive CTE select as role 'hs_hosting.asset:<DEBITOR>defaultproject:ADMIN',
|
||||||
|
joined with business query for all `'EMAIL_ADDRESSES'`:
|
||||||
|
|
||||||
|
| | D-1000000-hsh | D-1000300-mih |
|
||||||
|
|-----------------------------------------------------|------------------|---------------|
|
||||||
|
| currently (without type comparision in rbacobject): | ~3.30 - ~3.49 | ~0.23 |
|
||||||
|
| optimized (with type comparision in rbacobject): | ~2.99 - ~3.08 | ~0.21 |
|
||||||
|
|
||||||
|
As you can see, the query is no problem at all for normal customers (in the example, yours truly). With Hostsharing (D-1000000-hsh) it is quite slow.
|
||||||
|
|
||||||
|
Luckily this experiment also shows that it's not a big problem, having all hosting assets in the same database table.
|
||||||
|
|
||||||
|
Implementing this approach would be a bit difficult anyway, because we would need to transfer the type query parameter into the definition of the restricted view. We have not even the slightest idea how this could be done.
|
||||||
|
|
||||||
|
See the related queries in [recursive-cte-experiments-for-accessible-uuids.sql](../sql/recursive-cte-experiments-for-accessible-uuids.sql). They might have changed independently since this document was written, but you can still check out the old version from git.
|
||||||
|
|
||||||
|
### Rearranging the Parts of the CTE-Query
|
||||||
|
|
||||||
|
I also moved the function call which determines into its own WITH-section, with no improvement.
|
||||||
|
|
||||||
|
Experimentally I moved the business condition into the CTE SELECT, also with no improvement.
|
||||||
|
|
||||||
|
Such rearrangements seem to be successfully done by the PostgreSQL query optimizer.
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
### What we did Achieve?
|
||||||
|
|
||||||
|
In a first step, the total import runtime for office entities was reduced from about 25min to about 10min.
|
||||||
|
|
||||||
|
In a second step, we reduced the import of booking- and hosting-assets from about 100min (not counting the required office entities) to 5min.
|
||||||
|
|
||||||
|
### What did not Help?
|
||||||
|
|
||||||
|
Rearranging the CTE query by extracting parts into WITH-clauses did not improve the performance.
|
||||||
|
|
||||||
|
Surprisingly little performance gain (<10% improvement) came from reducing the result of the CTE query by moving the hosting asset type into RBAC-system and using it in the inner SELECT query instead of in the outer SELECT query of the application side.
|
||||||
|
|
||||||
|
### What did Help?
|
||||||
|
|
||||||
|
Merging the recursive CTE query to determine the RBAC SELECT-permission, made it more clear which business-queries take the time.
|
||||||
|
|
||||||
|
Avoiding EAGER-loading where not necessary, reduced the total runtime of the import to about the half.
|
||||||
|
|
||||||
|
The major improvement came from using direct INSERT statements, which avoided some SELECT statements unnecessarily generated by the EntityManager and also completely bypassed the RBAC SELECT permission checks.
|
||||||
|
|
||||||
|
### What Still Has To Be Done?
|
||||||
|
|
||||||
|
Where this performance analysis was mostly helping the performance of the legacy data import, we still need measures and improvements for the productive code.
|
||||||
|
|
||||||
|
For sure, using more LAZY-loading also helps in the production code. For some more ideas see section _Further Options To Explore_.
|
||||||
|
|
||||||
|
|
711
doc/rbac.md
Normal file
711
doc/rbac.md
Normal file
@ -0,0 +1,711 @@
|
|||||||
|
## *hsadmin-ng*'s Role-Based-Access-Management (RBAC)
|
||||||
|
|
||||||
|
The requirements of *hsadmin-ng* include table-, row- and column-level-security for read and write access to business-objects.
|
||||||
|
More precisely, any access has to be controlled according to given rules depending on the accessing users, their roles and the accessed business-object.
|
||||||
|
Further, roles and business-objects are hierarchical.
|
||||||
|
|
||||||
|
To avoid misunderstandings, we are using the term "business-object" what's usually called a "domain-object".
|
||||||
|
But as we are in the context of a webhosting infrastructure provider, "domain" would have a double meaning.
|
||||||
|
|
||||||
|
Our implementation is based on Role-Based-Access-Management (RBAC) in conjunction with views and triggers on the business-objects.
|
||||||
|
As far as possible, we are using the same terms as defined in the RBAC standard, for our function names though, we chose more expressive names.
|
||||||
|
|
||||||
|
In RBAC, subjects can be assigned to roles, roles can be hierarchical and eventually have assigned permissions.
|
||||||
|
A permission allows a specific operation (e.g. SELECT or UPDATE) on a specific (business-) object.
|
||||||
|
|
||||||
|
You can find the entity structure as a UML class diagram as follows:
|
||||||
|
|
||||||
|
```plantuml
|
||||||
|
@startuml
|
||||||
|
' left to right direction
|
||||||
|
top to bottom direction
|
||||||
|
|
||||||
|
' hide the ugly E in a circle left to the entity name
|
||||||
|
hide circle
|
||||||
|
|
||||||
|
' use right-angled line routing
|
||||||
|
skinparam linetype ortho
|
||||||
|
|
||||||
|
package RBAC {
|
||||||
|
|
||||||
|
' forward declarations
|
||||||
|
entity RbacSubject
|
||||||
|
|
||||||
|
together {
|
||||||
|
|
||||||
|
entity RbacRole
|
||||||
|
entity RbacPermission
|
||||||
|
|
||||||
|
|
||||||
|
RbacSubject -[hidden]> RbacRole
|
||||||
|
RbacRole -[hidden]> RbacSubject
|
||||||
|
}
|
||||||
|
|
||||||
|
together {
|
||||||
|
entity RbacGrant
|
||||||
|
enum RbacReferenceType
|
||||||
|
entity RbacReference
|
||||||
|
}
|
||||||
|
RbacReference -[hidden]> RbacReferenceType
|
||||||
|
|
||||||
|
entity RbacGrant {
|
||||||
|
ascendantUuid: uuid(RbackReference)
|
||||||
|
descendantUuid: uuid(RbackReference)
|
||||||
|
auto
|
||||||
|
}
|
||||||
|
RbacGrant o-u-> RbacReference
|
||||||
|
RbacGrant o-u-> RbacReference
|
||||||
|
|
||||||
|
enum RbacReferenceType {
|
||||||
|
RbacSubject
|
||||||
|
RbacRole
|
||||||
|
RbacPermission
|
||||||
|
}
|
||||||
|
RbacReferenceType ..> RbacSubject
|
||||||
|
RbacReferenceType ..> RbacRole
|
||||||
|
RbacReferenceType ..> RbacPermission
|
||||||
|
|
||||||
|
entity RbacReference {
|
||||||
|
*uuid : uuid <<generated>>
|
||||||
|
--
|
||||||
|
type : RbacReferenceType
|
||||||
|
}
|
||||||
|
RbacReference o--> RbacReferenceType
|
||||||
|
entity RbacSubject {
|
||||||
|
*uuid : uuid <<generated>>
|
||||||
|
--
|
||||||
|
name : varchar
|
||||||
|
}
|
||||||
|
RbacSubject o-- RbacReference
|
||||||
|
|
||||||
|
entity RbacRole {
|
||||||
|
*uuid : uuid(RbacReference)
|
||||||
|
--
|
||||||
|
name : varchar
|
||||||
|
}
|
||||||
|
RbacRole o-- RbacReference
|
||||||
|
|
||||||
|
together {
|
||||||
|
enum RbacOperation
|
||||||
|
entity RbacObject
|
||||||
|
}
|
||||||
|
|
||||||
|
entity RbacPermission {
|
||||||
|
*uuid : uuid(RbacReference)
|
||||||
|
--
|
||||||
|
objectUuid: RbacObject
|
||||||
|
op: RbacOperation
|
||||||
|
}
|
||||||
|
RbacPermission o-- RbacReference
|
||||||
|
RbacPermission o-- RbacOperation
|
||||||
|
RbacPermission *-- RbacObject
|
||||||
|
|
||||||
|
enum RbacOperation {
|
||||||
|
INSERT:package
|
||||||
|
INSERT:domain
|
||||||
|
...
|
||||||
|
SELECT
|
||||||
|
UPDATE
|
||||||
|
DELETE
|
||||||
|
}
|
||||||
|
|
||||||
|
entity RbacObject {
|
||||||
|
*uuid : uuid <<generated>>
|
||||||
|
--
|
||||||
|
objectTable: varchar
|
||||||
|
}
|
||||||
|
RbacObject o- "Business Objects"
|
||||||
|
}
|
||||||
|
|
||||||
|
package "Business Objects" {
|
||||||
|
|
||||||
|
entity package
|
||||||
|
package *--u- RbacObject
|
||||||
|
|
||||||
|
entity customer
|
||||||
|
customer *--u- RbacObject
|
||||||
|
|
||||||
|
entity "..." as moreBusinessObjects
|
||||||
|
moreBusinessObjects *-u- RbacObject
|
||||||
|
}
|
||||||
|
|
||||||
|
@enduml
|
||||||
|
```
|
||||||
|
|
||||||
|
### The RBAC Entity Types
|
||||||
|
|
||||||
|
#### RbacReference
|
||||||
|
|
||||||
|
An *RbacReference* is a generalization of all entity types which participate in the hierarchical role system, defined via *RbacGrant*.
|
||||||
|
|
||||||
|
The primary key of the *RbacReference* and its referred object is always identical.
|
||||||
|
|
||||||
|
#### RbacReferenceType
|
||||||
|
|
||||||
|
The enum *RbacReferenceType* describes the type of reference.
|
||||||
|
It's only needed to make it easier to find the referred object in *RbacSubject*, *RbacRole* or *RbacPermission*.
|
||||||
|
|
||||||
|
#### RbacSubject
|
||||||
|
|
||||||
|
An *RbacSubject* is a type of RBAC-subject which references a login account outside this system, identified by a name (usually an email-address).
|
||||||
|
|
||||||
|
*RbacSubject*s can be assigned to multiple *RbacRole*s, through which they can get permissions to *RbacObject*s.
|
||||||
|
|
||||||
|
The primary key of the *RbacSubject* is identical to its related *RbacReference*.
|
||||||
|
|
||||||
|
#### RbacRole
|
||||||
|
|
||||||
|
An *RbacRole* represents a collection of directly or indirectly assigned *RbacPermission*s.
|
||||||
|
Each *RbacRole* can be assigned to *RbacSubject*s or to another *RbacRole*.
|
||||||
|
|
||||||
|
Both kinds of assignments are represented via *RbacGrant*.
|
||||||
|
|
||||||
|
*RbacRole* entities can *RbacObject*s, or more precise
|
||||||
|
|
||||||
|
#### RbacPermission
|
||||||
|
|
||||||
|
An *RbacPermission* allows a specific *RbacOperation* on a specific *RbacObject*.
|
||||||
|
|
||||||
|
#### RbacOperation
|
||||||
|
|
||||||
|
An *RbacOperation* determines, <u>what</u> an *RbacPermission* allows to do.
|
||||||
|
It can be one of:
|
||||||
|
|
||||||
|
- **'INSERT'** - permits inserting new rows related to the row, to which the permission belongs, in the table which is specified an extra column, includes 'SELECT'
|
||||||
|
- **'SELECT'** - permits selecting the row specified by the permission, is included in all other permissions
|
||||||
|
- **'UPDATE'** - permits updating (only the updatable columns of) the row specified by the permission, includes 'SELECT'
|
||||||
|
- **'DELETE'** - permits deleting the row specified by the permission, includes 'SELECT'
|
||||||
|
|
||||||
|
This list is extensible according to the needs of the access rule system.
|
||||||
|
|
||||||
|
Please notice, that there is no **create** operation to create new instances of unrelated business-object-types.
|
||||||
|
For such a singleton business-object-type, e.g. *Organization" or "Hostsharing" has to be defined, and its single entity is referred in the permission.
|
||||||
|
Only with this rule, the foreign key in *RbacPermission* can be defined as `NOT NULL`.
|
||||||
|
|
||||||
|
#### RbacGrant
|
||||||
|
|
||||||
|
The *RbacGrant* entities represent the access-rights structure from *RbacSubject*s via hierarchical *RbacRoles* down to *RbacPermission*s.
|
||||||
|
|
||||||
|
The core SQL queries to determine access rights are all recursive queries on the *RbacGrant* table.
|
||||||
|
|
||||||
|
### Role naming
|
||||||
|
|
||||||
|
The naming pattern of a role is important to be able to address specific roles.
|
||||||
|
E.g. if a new package is added, the admin-role of the related customer has to be addressed.
|
||||||
|
|
||||||
|
There can be global roles like 'administrators'.
|
||||||
|
Most roles, though, are specific for certain business-objects and automatically generated as such:
|
||||||
|
|
||||||
|
business-object-table#business-object-name.role-stereotype
|
||||||
|
|
||||||
|
|
||||||
|
Where *business-object-table* is the name of the SQL table of the business object (e.g *customer* or 'package'),
|
||||||
|
*business-object-name* is generated from an immutable business key(e.g. a prefix like 'xyz' or 'xyz00')
|
||||||
|
and the *role-stereotype* describes a role relative to a referenced business-object as follows:
|
||||||
|
|
||||||
|
#### owner
|
||||||
|
|
||||||
|
The owner-role is granted to the subject which created the business object.
|
||||||
|
E.g. for a new *customer* it would be granted to 'administrators' and for a new *package* to the 'customer#...:ADMIN'.
|
||||||
|
|
||||||
|
Whoever has the owner-role assigned can do everything with the related business-object, including deleting (or deactivating) it.
|
||||||
|
|
||||||
|
In most cases, the permissions to other operations than 'DELETE' are granted through the 'admin' role.
|
||||||
|
By this, all roles ob sub-objects, which are assigned to the 'admin' role, are also granted to the 'owner'.
|
||||||
|
|
||||||
|
#### ADMIN
|
||||||
|
|
||||||
|
The admin-role is granted to a role of those subjects who manage the business object.
|
||||||
|
E.g. a 'package' is manged by the admin of the customer.
|
||||||
|
|
||||||
|
Whoever has the admin-role assigned, can usually update the related business-object but not delete (or deactivating) it.
|
||||||
|
|
||||||
|
The admin-role also comprises lesser roles, through which the SELECT-permission is granted.
|
||||||
|
|
||||||
|
#### AGENT
|
||||||
|
|
||||||
|
The agent-role is not used in the examples of this document, because it's for more complex cases.
|
||||||
|
It's usually granted to those roles and users who represent the related business-object, but are not allowed to update it.
|
||||||
|
|
||||||
|
Other than the tenant-role, it usually offers broader visibility of sub-business-objects (joined entities).
|
||||||
|
E.g. a package-admin is allowed to see the related debitor-business-object,
|
||||||
|
but not its banking data.
|
||||||
|
|
||||||
|
#### TENANT
|
||||||
|
|
||||||
|
The tenant-role is granted to everybody who needs to be able to select the business-object and (probably some) related business-objects.
|
||||||
|
Usually all owners, admins and tenants of sub-objects get this role granted.
|
||||||
|
|
||||||
|
Some business-objects only have very limited data directly in the main business-object and store more sensitive data in special sub-objects (e.g. 'customer-details') to which tenants of sub-objects of the main-object (e.g. package admins) do not get SELECT permission.
|
||||||
|
|
||||||
|
#### GUEST
|
||||||
|
|
||||||
|
(Deprecated)
|
||||||
|
|
||||||
|
#### REFERRER
|
||||||
|
|
||||||
|
Like the agent-role, the guest-role too is not used in the examples of this document, because it's for more complex cases.
|
||||||
|
|
||||||
|
If the referrer-role exists, the SELECT-permission is granted to it, instead of to the tenant-role.
|
||||||
|
Other than the tenant-role, the referrer-roles does never grant any roles of related objects.
|
||||||
|
|
||||||
|
Also, if the referrer-role exists, the tenant-role receives the SELECT-permission through the referrer-role.
|
||||||
|
|
||||||
|
|
||||||
|
### Referenced Business Objects and Role-Depreciation
|
||||||
|
|
||||||
|
A general rule is, if one business object *origin* references another object *target* (in other words: one database table joins another table),
|
||||||
|
**and** a role for *origin* needs also access to *target*,
|
||||||
|
then usually the *target* role is granted to the *origin* role which is one level lower.
|
||||||
|
|
||||||
|
E.g. the admin-role of the *origin* object gets granted the agent-role (or, if it does not exist, then the tenant-role) of the *target* object.
|
||||||
|
|
||||||
|
Following this rule, also implies, that the number of indirections to which visibility can be granted is limited.
|
||||||
|
The admin-role of one object could be granted visibility to another object through at maximum 3 joins (agent->tenant->guest).
|
||||||
|
|
||||||
|
But not in all cases role-depreciation takes place.
|
||||||
|
E.g. often a tenant-role is granted another tenant-role,
|
||||||
|
because it should be again allowed to select sub-objects.
|
||||||
|
The same for the agent-role, often it is granted another agent-role.
|
||||||
|
|
||||||
|
|
||||||
|
## Example Users, Roles, Permissions and Business-Objects
|
||||||
|
|
||||||
|
The following diagram shows how users, roles and permissions could be granted access to operations on business objects.
|
||||||
|
|
||||||
|
```plantuml
|
||||||
|
@startuml
|
||||||
|
' left to right direction
|
||||||
|
top to bottom direction
|
||||||
|
|
||||||
|
' hide the ugly E in a circle left to the entity name
|
||||||
|
hide circle
|
||||||
|
|
||||||
|
' use right-angled line routing
|
||||||
|
' skinparam linetype ortho
|
||||||
|
|
||||||
|
package RbacSubjects {
|
||||||
|
object UserMike
|
||||||
|
object UserSuse
|
||||||
|
object UserPaul
|
||||||
|
}
|
||||||
|
|
||||||
|
package RbacRoles {
|
||||||
|
object RoleAdministrators
|
||||||
|
object RoleCustXyz_Owner
|
||||||
|
object RoleCustXyz_Admin
|
||||||
|
object RolePackXyz00_Owner
|
||||||
|
}
|
||||||
|
RbacSubjects -[hidden]> RbacRoles
|
||||||
|
|
||||||
|
package RbacPermissions {
|
||||||
|
object PermCustXyz_SELECT
|
||||||
|
object PermCustXyz_UPDATE
|
||||||
|
object PermCustXyz_DELETE
|
||||||
|
object PermCustXyz_INSERT:Package
|
||||||
|
object PermPackXyz00_SELECT
|
||||||
|
object PermPackXyz00_EDIT
|
||||||
|
object PermPackXyz00_DELETE
|
||||||
|
object PermPackXyz00_INSERT:USER
|
||||||
|
}
|
||||||
|
RbacRoles -[hidden]> RbacPermissions
|
||||||
|
|
||||||
|
package BusinessObjects {
|
||||||
|
object CustXyz
|
||||||
|
object PackXyz00
|
||||||
|
}
|
||||||
|
RbacPermissions -[hidden]> BusinessObjects
|
||||||
|
|
||||||
|
UserMike o---> RoleAdministrators
|
||||||
|
UserSuse o--> RoleCustXyz_Admin
|
||||||
|
UserPaul o--> RolePackXyz00_Owner
|
||||||
|
|
||||||
|
RoleAdministrators o..> RoleCustXyz_Owner
|
||||||
|
RoleCustXyz_Owner o-> RoleCustXyz_Admin
|
||||||
|
RoleCustXyz_Admin o-> RolePackXyz00_Owner
|
||||||
|
|
||||||
|
RoleCustXyz_Owner o--> PermCustXyz_UPDATE
|
||||||
|
RoleCustXyz_Owner o--> PermCustXyz_DELETE
|
||||||
|
RoleCustXyz_Admin o--> PermCustXyz_SELECT
|
||||||
|
RoleCustXyz_Admin o--> PermCustXyz_INSERT:Package
|
||||||
|
RolePackXyz00_Owner o--> PermPackXyz00_SELECT
|
||||||
|
RolePackXyz00_Owner o--> PermPackXyz00_UPDATE
|
||||||
|
RolePackXyz00_Owner o--> PermPackXyz00_DELETE
|
||||||
|
RolePackXyz00_Owner o--> PermPackXyz00_INSERT:User
|
||||||
|
|
||||||
|
PermCustXyz_SELECT o--> CustXyz
|
||||||
|
PermCustXyz_UPDATE o--> CustXyz
|
||||||
|
PermCustXyz_DELETE o--> CustXyz
|
||||||
|
PermCustXyz_INSERT:Package o--> CustXyz
|
||||||
|
PermPackXyz00_SELECT o--> PackXyz00
|
||||||
|
PermPackXyz00_UPDATE o--> PackXyz00
|
||||||
|
PermPackXyz00_DELETE o--> PackXyz00
|
||||||
|
PermPackXyz00_INSERT:User o--> PackXyz00
|
||||||
|
|
||||||
|
@enduml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Business-Object-Tables, Triggers and Views
|
||||||
|
|
||||||
|
To support the RBAC system, for each business-object-table, some more artifacts are created in the database:
|
||||||
|
|
||||||
|
- a `BEFORE INSERT TRIGGER` which creates the related *RbacObject* instance,
|
||||||
|
- an `AFTER INSERT TRIGGER` which creates the related *RbacRole*s, *RbacPermission*s together with their related *RbacReference*s as well as *RbacGrant*s,
|
||||||
|
- a restricted view (e.g. *customer_rv*) through which restricted users can access the underlying data.
|
||||||
|
|
||||||
|
Not yet implemented, but planned are these actions:
|
||||||
|
|
||||||
|
- an `ON DELETE ... DO INSTEAD` rule to allow `SQL DELETE` if applicable for the business-object-table and the user has 'DELETE' permission,
|
||||||
|
- an `ON UPDATE ... DO INSTEAD` rule to allow `SQL UPDATE` if the user has 'UPDATE' right,
|
||||||
|
- an `ON INSERT ... DO INSTEAD` rule to allow `SQL INSERT` if the user has the 'INSERT' right for the parent-business-object.
|
||||||
|
|
||||||
|
The restricted view takes the current user from a session property and applies the hierarchy of its roles all the way down to the permissions related to the respective business-object-table.
|
||||||
|
This way, each user can only select the data they have 'SELECT'-permission for, only create those they have 'add-...'-permission, only update those they have 'UPDATE'- and only delete those they have 'DELETE'-permission to.
|
||||||
|
|
||||||
|
### Current User
|
||||||
|
|
||||||
|
The current use is taken from the session variable `hsadminng.currentSubject` which contains the name of the user as stored in the
|
||||||
|
*RbacSubject*s table. Example:
|
||||||
|
|
||||||
|
SET LOCAL hsadminng.currentSubject = 'mike@hostsharing.net';
|
||||||
|
|
||||||
|
That user is also used for historicization and audit log, but which is a different topic.
|
||||||
|
|
||||||
|
### Assuming Roles
|
||||||
|
|
||||||
|
If the session variable `hsadminng.assumedRoles` is set to a non-empty value, its content is interpreted as a list of semicolon-separated role names.
|
||||||
|
Example:
|
||||||
|
|
||||||
|
SET LOCAL hsadminng.assumedRoles = 'customer#aab:admin;customer#aac:admin';
|
||||||
|
|
||||||
|
In this case, not the current user but the assumed roles are used as a starting point for any further queries.
|
||||||
|
Roles which are not granted to the current user, directly or indirectly, cannot be assumed.
|
||||||
|
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
A full example is shown here:
|
||||||
|
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
SET SESSION SESSION AUTHORIZATION restricted;
|
||||||
|
SET LOCAL hsadminng.currentSubject = 'mike@hostsharing.net';
|
||||||
|
SET LOCAL hsadminng.assumedRoles = 'customer#aab:admin;customer#aac:admin';
|
||||||
|
|
||||||
|
SELECT c.prefix, p.name as "package", ema.localPart || '@' || dom.name as "email-address"
|
||||||
|
FROM emailaddress_rv ema
|
||||||
|
JOIN domain_rv dom ON dom.uuid = ema.domainuuid
|
||||||
|
JOIN domain_rv uu ON uu.uuid = dom.domainuuid
|
||||||
|
JOIN package_rv p ON p.uuid = uu.packageuuid
|
||||||
|
JOIN customer_rv c ON c.uuid = p.customeruuid;
|
||||||
|
END TRANSACTION;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Roles and Their Assignments for Certain Business Objects
|
||||||
|
|
||||||
|
To give you an overview of the business-object-types for the following role-examples,
|
||||||
|
check this diagram:
|
||||||
|
|
||||||
|
```plantuml
|
||||||
|
@startuml
|
||||||
|
left to right direction
|
||||||
|
' top to bottom direction
|
||||||
|
|
||||||
|
' hide the ugly E in a circle left to the entity name
|
||||||
|
hide circle
|
||||||
|
|
||||||
|
' use right-angled line routing
|
||||||
|
' skinparam linetype ortho
|
||||||
|
|
||||||
|
entity EMailAddress
|
||||||
|
|
||||||
|
entity Domain
|
||||||
|
Domain o-- "*" EMailAddress
|
||||||
|
|
||||||
|
entity domain
|
||||||
|
domain o-- "*" Domain
|
||||||
|
|
||||||
|
entity Package
|
||||||
|
Package o.. "*" domain
|
||||||
|
|
||||||
|
entity Customer
|
||||||
|
Customer o-- "*" Package
|
||||||
|
|
||||||
|
@enduml
|
||||||
|
```
|
||||||
|
|
||||||
|
It's mostly an example hierarchy of business-object-types, but resembles a part of Hostsharing's actual hosting infrastructure.
|
||||||
|
|
||||||
|
The following diagrams show which roles are created for each business-object-type
|
||||||
|
and how they relate to roles from other business-object-types.
|
||||||
|
|
||||||
|
### Customer Roles
|
||||||
|
|
||||||
|
The highest level of the business-object-type-hierarchy is the *Customer*.
|
||||||
|
|
||||||
|
```plantuml
|
||||||
|
@startuml
|
||||||
|
' left to right direction
|
||||||
|
top to bottom direction
|
||||||
|
|
||||||
|
' hide the ugly E in a circle left to the entity name
|
||||||
|
hide circle
|
||||||
|
|
||||||
|
' use right-angled line routing
|
||||||
|
' skinparam linetype ortho
|
||||||
|
|
||||||
|
' needs PlantUML 1.2021.14 as Markdown plugin
|
||||||
|
allow_mixing
|
||||||
|
|
||||||
|
entity "BObj customer#xyz" as boCustXyz
|
||||||
|
|
||||||
|
together {
|
||||||
|
entity "Perm customer#xyz *" as permCustomerXyzDELETE
|
||||||
|
permCustomerXyzDELETE --> boCustXyz
|
||||||
|
|
||||||
|
entity "Perm customer#xyz INSERT:package" as permCustomerXyzINSERT:package
|
||||||
|
permCustomerXyzINSERT:package --> boCustXyz
|
||||||
|
|
||||||
|
entity "Perm customer#xyz SELECT" as permCustomerXyzSELECT
|
||||||
|
permCustomerXyzSELECT--> boCustXyz
|
||||||
|
}
|
||||||
|
|
||||||
|
entity "Role customer#xyz:TENANT" as roleCustXyzTenant
|
||||||
|
roleCustXyzTenant --> permCustomerXyzSELECT
|
||||||
|
|
||||||
|
entity "Role customer#xyz:ADMIN" as roleCustXyzAdmin
|
||||||
|
roleCustXyzAdmin --> roleCustXyzTenant
|
||||||
|
roleCustXyzAdmin --> permCustomerXyzINSERT:package
|
||||||
|
|
||||||
|
entity "Role customer#xyz:OWNER" as roleCustXyzOwner
|
||||||
|
roleCustXyzOwner ..> roleCustXyzAdmin
|
||||||
|
roleCustXyzOwner --> permCustomerXyzDELETE
|
||||||
|
|
||||||
|
actor "Customer XYZ Admin" as actorCustXyzAdmin
|
||||||
|
actorCustXyzAdmin --> roleCustXyzAdmin
|
||||||
|
|
||||||
|
entity "Role administrators" as roleAdmins
|
||||||
|
roleAdmins --> roleCustXyzOwner
|
||||||
|
|
||||||
|
actor "Any Hostmaster" as actorHostmaster
|
||||||
|
actorHostmaster --> roleAdmins
|
||||||
|
@enduml
|
||||||
|
```
|
||||||
|
|
||||||
|
As you can see, there something special:
|
||||||
|
From the 'Role customer#xyz:OWNER' to the 'Role customer#xyz:admin' there is a dashed line, whereas all other lines are solid lines.
|
||||||
|
Solid lines means, that one role is granted to another and automatically assumed in all queries to the restricted views.
|
||||||
|
The dashed line means that one role is granted to another but not automatically assumed in queries to the restricted views.
|
||||||
|
|
||||||
|
The reason here is that otherwise simply too many objects would be accessible to those with the 'administrators' role and all queries would be slowed down vastly.
|
||||||
|
|
||||||
|
Grants which are not automatically assumed are still valid grants for `hsadminng.assumedRoles`.
|
||||||
|
Thus, if you want to access anything below a customer, assume its role first.
|
||||||
|
|
||||||
|
There is actually another speciality in the customer roles:
|
||||||
|
For all others, a user defined by the customer gets the owner role assigned, just for the customer, the owner's role is assigned to the 'administrators' role.
|
||||||
|
|
||||||
|
|
||||||
|
### Package Roles
|
||||||
|
|
||||||
|
One example of the business-object-type-level right below is the *Package*.
|
||||||
|
|
||||||
|
```plantuml
|
||||||
|
@startuml
|
||||||
|
' left to right direction
|
||||||
|
top to bottom direction
|
||||||
|
|
||||||
|
' hide the ugly E in a circle left to the entity name
|
||||||
|
hide circle
|
||||||
|
|
||||||
|
' use right-angled line routing
|
||||||
|
' skinparam linetype ortho
|
||||||
|
|
||||||
|
' needs PlantUML 1.2021.14 as Markdown plugin
|
||||||
|
allow_mixing
|
||||||
|
|
||||||
|
entity "BObj package#xyz00" as boPacXyz00
|
||||||
|
|
||||||
|
together {
|
||||||
|
entity "Perm package#xyz00 *" as permPackageXyzDELETE
|
||||||
|
permPackageXyzDELETE --> boPacXyz00
|
||||||
|
|
||||||
|
entity "Perm package#xyz00 INSERT:domain" as permPacXyz00INSERT:user
|
||||||
|
permPacXyz00INSERT:user --> boPacXyz00
|
||||||
|
|
||||||
|
entity "Perm package#xyz00 UPDATE" as permPacXyz00UPDATE
|
||||||
|
permPacXyz00UPDATE --> boPacXyz00
|
||||||
|
|
||||||
|
entity "Perm package#xyz00 SELECT" as permPacXyz00SELECT
|
||||||
|
permPacXyz00SELECT --> boPacXyz00
|
||||||
|
}
|
||||||
|
|
||||||
|
package {
|
||||||
|
entity "Role customer#xyz:TENANT" as roleCustXyzTenant
|
||||||
|
entity "Role customer#xyz:ADMIN" as roleCustXyzAdmin
|
||||||
|
entity "Role customer#xyz:OWNER" as roleCustXyzOwner
|
||||||
|
}
|
||||||
|
|
||||||
|
package {
|
||||||
|
entity "Role package#xyz00:OWNER" as rolePacXyz00Owner
|
||||||
|
entity "Role package#xyz00:ADMIN" as rolePacXyz00Admin
|
||||||
|
entity "Role package#xyz00:TENANT" as rolePacXyz00Tenant
|
||||||
|
}
|
||||||
|
|
||||||
|
rolePacXyz00Tenant --> permPacXyz00SELECT
|
||||||
|
rolePacXyz00Tenant --> roleCustXyzTenant
|
||||||
|
|
||||||
|
rolePacXyz00Owner --> rolePacXyz00Admin
|
||||||
|
rolePacXyz00Owner --> permPackageXyzDELETE
|
||||||
|
|
||||||
|
roleCustXyzAdmin --> rolePacXyz00Owner
|
||||||
|
roleCustXyzAdmin --> roleCustXyzTenant
|
||||||
|
|
||||||
|
roleCustXyzOwner ..> roleCustXyzAdmin
|
||||||
|
|
||||||
|
rolePacXyz00Admin --> rolePacXyz00Tenant
|
||||||
|
rolePacXyz00Admin --> permPacXyz00INSERT:user
|
||||||
|
rolePacXyz00Admin --> permPacXyz00UPDATE
|
||||||
|
|
||||||
|
actor "Package XYZ00 Admin" as actorPacXyzAdmin
|
||||||
|
actorPacXyzAdmin -l-> rolePacXyz00Admin
|
||||||
|
|
||||||
|
actor "Customer XYZ Admin" as actorCustXyzAdmin
|
||||||
|
actorCustXyzAdmin --> roleCustXyzAdmin
|
||||||
|
|
||||||
|
entity "Role administrators" as roleAdmins
|
||||||
|
roleAdmins --> roleCustXyzOwner
|
||||||
|
|
||||||
|
actor "Any Hostmaster" as actorHostmaster
|
||||||
|
actorHostmaster --> roleAdmins
|
||||||
|
|
||||||
|
@enduml
|
||||||
|
```
|
||||||
|
|
||||||
|
Initially, the customer's admin role is assigned to the package owner role.
|
||||||
|
They can use the package's admin role to hand over most management functionality to a third party.
|
||||||
|
The 'administrators' can get access through an assumed customer's admin role or directly by assuming the package's owner or admin role.
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
We did not define maximum response time in our requirements,
|
||||||
|
but set a target of 7.000 customers, 15.000 packages, 150.000 Unix users, 100.000 domains and 500.000 email-addresses.
|
||||||
|
|
||||||
|
For such a dataset the response time for typical queries from a UI should be acceptable.
|
||||||
|
Also, when adding data beyond these quantities, increase in response time should be roughly linear or below.
|
||||||
|
For this, we increased the dataset by 14% and then by another 25%, ending up with 10.000 customers, almost 25.000 packages, over 174.000 unix users, over 120.000 domains and almost 750.000 email-addresses.
|
||||||
|
|
||||||
|
The performance test suite comprised 8 SELECT queries issued by an administrator, mostly with two assumed customer owner roles.
|
||||||
|
The tests started with finding a specific customer and ended with listing all accessible email-addresses joined with their domains, unix-users, packages and customers.
|
||||||
|
|
||||||
|
Find the SQL script here: `28-hs-tests.sql`.
|
||||||
|
|
||||||
|
### Two View Query Variants
|
||||||
|
|
||||||
|
We have tested two variants of the query for the restricted view,
|
||||||
|
both utilizing a PostgreSQL function like this:
|
||||||
|
|
||||||
|
FUNCTION rbac.queryAccessibleObjectUuidsOfSubjectIds(
|
||||||
|
requiredOp rbac.RbacOp,
|
||||||
|
forObjectTable varchar,
|
||||||
|
subjectIds uuid[],
|
||||||
|
maxObjects integer = 16000)
|
||||||
|
RETURNS SETOF uuid
|
||||||
|
|
||||||
|
The function returns all object uuids for which the given subjectIds (user o assumed roles) have a permission or required operation.
|
||||||
|
|
||||||
|
Let's have a look at the two view queries:
|
||||||
|
|
||||||
|
#### Using WHERE ... IN
|
||||||
|
|
||||||
|
CREATE OR REPLACE VIEW customer_rv AS
|
||||||
|
SELECT DISTINCT target.*
|
||||||
|
FROM customer AS target
|
||||||
|
WHERE target.uuid IN (
|
||||||
|
SELECT uuid
|
||||||
|
FROM rbac.queryAccessibleObjectUuidsOfSubjectIds(
|
||||||
|
'SELECT, 'customer', currentSubjectOrAssumedRolesUuids()));
|
||||||
|
|
||||||
|
This view should be automatically updatable.
|
||||||
|
Where, for updates, we actually have to check for 'UPDATE' instead of 'SELECT' operation, which makes it a bit more complicated.
|
||||||
|
|
||||||
|
With the larger dataset, the test suite initially needed over 7 seconds with this view query.
|
||||||
|
At this point the second variant was tried.
|
||||||
|
|
||||||
|
But after the initial query, the execution time was drastically reduced,
|
||||||
|
even with different query values.
|
||||||
|
Looks like the query optimizer needed some statistics to find the best path.
|
||||||
|
|
||||||
|
#### Using A JOIN
|
||||||
|
|
||||||
|
CREATE OR REPLACE VIEW customer_rv AS
|
||||||
|
SELECT DISTINCT target.*
|
||||||
|
FROM customer AS target
|
||||||
|
JOIN rbac.queryAccessibleObjectUuidsOfSubjectIds(
|
||||||
|
'SELECT, 'customer', currentSubjectOrAssumedRolesUuids()) AS allowedObjId
|
||||||
|
ON target.uuid = allowedObjId;
|
||||||
|
|
||||||
|
This view cannot is not updatable automatically,
|
||||||
|
but it was quite fast from the beginning.
|
||||||
|
|
||||||
|
### Performance Results
|
||||||
|
|
||||||
|
The following table shows the average between the second and the third repeat of the test-suite:
|
||||||
|
|
||||||
|
| Dataset | using JOIN | using WHERE IN |
|
||||||
|
|----------------:|-----------:|---------------:|
|
||||||
|
| 7000 customers | 670ms | 1040ms |
|
||||||
|
| 10000 customers | 1050ms | 1125ms |
|
||||||
|
| +43% | +57% | +8% |
|
||||||
|
|
||||||
|
The JOIN-variant is still faster, but the growth in execution time exceeded the growth of the dataset.
|
||||||
|
|
||||||
|
The WHERE-IN-variant is about 50% slower on the smaller dataset, but almost keeps its performance on the larger dataset.
|
||||||
|
|
||||||
|
Both variants a viable option, depending on other needs, e.g. updatable views.
|
||||||
|
|
||||||
|
|
||||||
|
## Access Control to RBAC-Objects
|
||||||
|
|
||||||
|
Access Control for business objects checked according to the assigned roles.
|
||||||
|
But we decided not to create such roles and permissions for the RBAC-Objects itself.
|
||||||
|
It would have overcomplicated the system and the necessary information can easily be added to the RBAC-Objects itself, mostly the `RbacGrant`s.
|
||||||
|
|
||||||
|
### RbacSubject
|
||||||
|
|
||||||
|
Users can self-register, thus to create a new RbacSubject entity, no login is required.
|
||||||
|
But such a user has no access-rights except viewing itself.
|
||||||
|
|
||||||
|
Users can view themselves.
|
||||||
|
And any user can view all other users as long as they have the same roles assigned.
|
||||||
|
As an exception, users which are assigned to global roles are not visible by other users.
|
||||||
|
|
||||||
|
At least an indirect lookup of known user-names (e.g. email address of the user) is possible
|
||||||
|
by users who have an empowered assignment of any role.
|
||||||
|
Otherwise, it would not be possible to assign roles to new users.
|
||||||
|
|
||||||
|
### RbacRole
|
||||||
|
|
||||||
|
All roles are system-defined and cannot be created or modified by any external API.
|
||||||
|
|
||||||
|
Users can view only the roles to which are granted to them.
|
||||||
|
|
||||||
|
## RbacGrant
|
||||||
|
|
||||||
|
Grant can be `empowered`, this means that the grantee user can grant the granted role to other users
|
||||||
|
and revoke grants to that role.
|
||||||
|
(TODO: access control part not yet implemented, currently all accessible roles can be granted to other users)
|
||||||
|
|
||||||
|
Grants can be `managed`, which means they are created and deleted by system-defined rules.
|
||||||
|
If a grant is not managed, it was created by an empowered user and can be deleted by empowered users.
|
||||||
|
|
||||||
|
Grants can be `assumed`, which means that they are immediately active.
|
||||||
|
If a grant is not assumed, the grantee user needs to use `assumeRoles` to activate it.
|
||||||
|
|
||||||
|
Users can see only grants of roles to which they are (directly?) assigned themselves.
|
||||||
|
|
||||||
|
TODO: If a user grants an indirect role to another user, that grant would not be visible to the user.
|
||||||
|
But if we make indirect grants visible, this would reveal too much information.
|
||||||
|
We also cannot keep the granting user in the grant because grants must survive deleted users,
|
||||||
|
e.g. if after an account was transferred to another user.
|
||||||
|
|
124
doc/scenarios/template.html
Normal file
124
doc/scenarios/template.html
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html $if(lang)$ lang="$lang$" $endif$>
|
||||||
|
<head>
|
||||||
|
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<!--[if lt IE 9]>
|
||||||
|
<script src="http://css3-mediaqueries-js.googlecode.com/svn/trunk/css3-mediaqueries.js"></script>
|
||||||
|
<![endif]-->
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||||
|
<meta http-equiv="Content-Style-Type" content="text/css" />
|
||||||
|
|
||||||
|
<!-- <link rel="stylesheet" type="text/css" href="template.css" /> -->
|
||||||
|
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/template.css" />
|
||||||
|
|
||||||
|
<link href="https://vjs.zencdn.net/5.4.4/video-js.css" rel="stylesheet" />
|
||||||
|
|
||||||
|
<script src="https://code.jquery.com/jquery-2.2.1.min.js"></script>
|
||||||
|
<!-- <script type='text/javascript' src='menu/js/jquery.cookie.js'></script> -->
|
||||||
|
<!-- <script type='text/javascript' src='menu/js/jquery.hoverIntent.minified.js'></script> -->
|
||||||
|
<!-- <script type='text/javascript' src='menu/js/jquery.dcjqaccordion.2.7.min.js'></script> -->
|
||||||
|
|
||||||
|
<!-- <link href="menu/css/skins/blue.css" rel="stylesheet" type="text/css" /> -->
|
||||||
|
<!-- <link href="menu/css/skins/graphite.css" rel="stylesheet" type="text/css" /> -->
|
||||||
|
<!-- <link href="menu/css/skins/grey.css" rel="stylesheet" type="text/css" /> -->
|
||||||
|
|
||||||
|
<!-- <script src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script> -->
|
||||||
|
|
||||||
|
|
||||||
|
<!-- <script src="script.js"></script> -->
|
||||||
|
|
||||||
|
<!-- <script src="jquery.sticky-kit.js "></script> -->
|
||||||
|
<script type='text/javascript' src='https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/menu/js/jquery.cookie.js'></script>
|
||||||
|
<script type='text/javascript' src='https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/menu/js/jquery.hoverIntent.minified.js'></script>
|
||||||
|
<script type='text/javascript' src='https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/menu/js/jquery.dcjqaccordion.2.7.min.js'></script>
|
||||||
|
|
||||||
|
<link href="https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/menu/css/skins/blue.css" rel="stylesheet" type="text/css" />
|
||||||
|
<link href="https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/menu/css/skins/graphite.css" rel="stylesheet" type="text/css" />
|
||||||
|
<link href="https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/menu/css/skins/grey.css" rel="stylesheet" type="text/css" />
|
||||||
|
<link href="https://cdn.jsdelivr.net/gh/ryangrose/easy-pandoc-templates@948e28e5/css/elegant_bootstrap.css" rel="stylesheet" type="text/css" />
|
||||||
|
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.4/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/script.js"></script>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/jquery.sticky-kit.js"></script>
|
||||||
|
<meta name="generator" content="pandoc" />
|
||||||
|
$for(author-meta)$
|
||||||
|
<meta name="author" content="$author-meta$" />
|
||||||
|
$endfor$
|
||||||
|
$if(date-meta)$
|
||||||
|
<meta name="date" content="$date-meta$" />
|
||||||
|
$endif$
|
||||||
|
<title>$if(title-prefix)$$title-prefix$ - $endif$$pagetitle$</title>
|
||||||
|
<style type="text/css">code{white-space: pre;}</style>
|
||||||
|
$if(quotes)$
|
||||||
|
<style type="text/css">q { quotes: "“" "”" "‘" "’"; }</style>
|
||||||
|
$endif$
|
||||||
|
$if(highlighting-css)$
|
||||||
|
<style type="text/css">
|
||||||
|
$highlighting-css$
|
||||||
|
</style>
|
||||||
|
$endif$
|
||||||
|
$for(css)$
|
||||||
|
<link rel="stylesheet" href="$css$" $if(html5)$$else$type="text/css" $endif$/>
|
||||||
|
$endfor$
|
||||||
|
$if(math)$
|
||||||
|
$math$
|
||||||
|
$endif$
|
||||||
|
$for(header-includes)$
|
||||||
|
$header-includes$
|
||||||
|
$endfor$
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
|
||||||
|
$if(title)$
|
||||||
|
<div class="navbar navbar-static-top">
|
||||||
|
<div class="navbar-inner">
|
||||||
|
<div class="container">
|
||||||
|
<span class="doc-title">$title$</span>
|
||||||
|
<ul class="nav pull-right doc-info">
|
||||||
|
$for(author)$
|
||||||
|
<li><p class="navbar-text">$author$</p></li>
|
||||||
|
$endfor$
|
||||||
|
$if(date)$
|
||||||
|
<li><p class="navbar-text">$date$</p></li>
|
||||||
|
$endif$
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
$endif$
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
$if(toc)$
|
||||||
|
<div id="$idprefix$TOC" class="span3">
|
||||||
|
<div class="well toc">
|
||||||
|
|
||||||
|
$toc$
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
$endif$
|
||||||
|
<div class="span$if(toc)$9$else$12$endif$">
|
||||||
|
|
||||||
|
$if(abstract)$
|
||||||
|
<H1>$abstract-title$</H1>
|
||||||
|
$abstract$
|
||||||
|
$endif$
|
||||||
|
|
||||||
|
$for(include-before)$
|
||||||
|
$include-before$
|
||||||
|
$endfor$
|
||||||
|
$body$
|
||||||
|
$for(include-after)$
|
||||||
|
$include-after$
|
||||||
|
$endfor$
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="https://vjs.zencdn.net/5.4.4/video.js"></script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
124
doc/test-concept.md
Normal file
124
doc/test-concept.md
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
## Test-Concept
|
||||||
|
|
||||||
|
<!-- generated TOC begin: -->
|
||||||
|
- [Unit-Tests](#unit-tests)
|
||||||
|
- [REST-Tests](#rest-tests)
|
||||||
|
- [Integration-Tests](#integration-tests)
|
||||||
|
- [Acceptance-Tests](#acceptance-tests)
|
||||||
|
- [Performance-Tests](#performance-tests)
|
||||||
|
- [System-Integration-Tests](#system-integration-tests)
|
||||||
|
<!-- generated TOC end. -->
|
||||||
|
|
||||||
|
### General Concepts
|
||||||
|
|
||||||
|
The following test concept uses terms like "double" and "mock" (maybe in inflected form like "mocking" or "mocked"), "whitebox-test" and "blackbox-tests" and "test-fixture".
|
||||||
|
Please look up their definition in the [glossary](glossary.md)
|
||||||
|
|
||||||
|
Where our APIs should be designed in a way that it's possible, using a mocking library like *Mockito* often leads to shorter test code.
|
||||||
|
|
||||||
|
Most important for a test is, to clearly express what it actually is testing.
|
||||||
|
For this, it might help to wrap test setup and assertions into test fixture
|
||||||
|
|
||||||
|
|
||||||
|
### Kinds of Tests
|
||||||
|
|
||||||
|
Depending on the concrete aspects which we want to test, we are using different kinds of tests as described as follows.
|
||||||
|
|
||||||
|
#### Unit-Tests
|
||||||
|
|
||||||
|
In this project a *Unit* for *UnitTests* can be a single method (function), a class or even a group of classes which express a common concept.
|
||||||
|
|
||||||
|
The unit are technically whitebox-tests and count into test-code-coverage.
|
||||||
|
But the whitebox-knowledge should only be used for the [test-fixture](./glossary.md#test-fixture).
|
||||||
|
|
||||||
|
Unit-Test in this project are implemented with *JUnit Jupiter*, *Mockito* and *AssertJ*.
|
||||||
|
|
||||||
|
Unit-Tests do not use any external systems, not even a database.
|
||||||
|
They just test the unit, not any dependencies or proper integration with dependencies.
|
||||||
|
|
||||||
|
Such tests usually run very fast and should test all branches.
|
||||||
|
|
||||||
|
These Tests are always named `...UnitTest` and can automatically run in the build-process.
|
||||||
|
|
||||||
|
|
||||||
|
#### REST-Tests
|
||||||
|
|
||||||
|
At the level of REST-Controllers, *Spring's* `WebMvcTest`, a special kind of Unit-Test, are utilized to replace simple unit tests.
|
||||||
|
Such tests issue REST-requests through a mocked REST-Layer and therefore use the controllers similar to a real client.
|
||||||
|
Otherwise, the implementation technologies are like those of Unit-Tests.
|
||||||
|
|
||||||
|
Being unit-tests, also REST-tests are whitebox-tests and count into test-code-coverage.
|
||||||
|
|
||||||
|
Like other Unit-Tests, REST-Test do not use any external systems, not even a database.
|
||||||
|
They just test the REST-related parts of the unit, e.g. URL-Mappings, HTTP-Headers and proper JSON encoding of request and response data.
|
||||||
|
Other dependencies and integrations with such are not tested on this level.
|
||||||
|
|
||||||
|
Such tests usually run very fast, but should focus on REST-specific issues, leaving branch-testing to pure Unit-Tests.
|
||||||
|
|
||||||
|
These Tests are always named `...RestTest` and can automatically run in the build-process.
|
||||||
|
|
||||||
|
|
||||||
|
#### Integration-Tests
|
||||||
|
|
||||||
|
Integration-Tests in this context mean integration with support systems like databases or messaging-systems, but not integration with external systems.
|
||||||
|
|
||||||
|
Integration-tests, are blackbox-tests and do <u>not</u> count into test-code-coverage.
|
||||||
|
|
||||||
|
Such tests are implemented with *JUnit Jupiter* through some sort of `@SpringBootTest`, e.g. `DataJpaTest` and usually utilize *Testcontainers* and *Docker* to wrap the supporting system, e.g. the *PostgreSQL* database.
|
||||||
|
*Mockito* can also be used for this kind of tests, to separate multiple integrations.
|
||||||
|
|
||||||
|
Integration-Tests are relatively slow and therefore should focus on the integration.
|
||||||
|
Java-internal issues should be tested through Unit-Tests.
|
||||||
|
|
||||||
|
These Tests are always named `...IntegrationTest` and can automatically run in the build-process.
|
||||||
|
|
||||||
|
##### DataJpaTest / Database-Integration-Tests
|
||||||
|
|
||||||
|
In this project, a major part of the program logic is coded in the database as stored procedures, functions and triggers.
|
||||||
|
|
||||||
|
This program logic is tested through *integration tests* using `DataJpaTest`
|
||||||
|
because pure unit tests in the database are not only cumbersome but also easily lead to large test gaps.
|
||||||
|
|
||||||
|
|
||||||
|
#### Acceptance-Tests
|
||||||
|
|
||||||
|
We define Acceptance-Tests as test which describe user-stories, respectively high-level business requirements.
|
||||||
|
Acceptance-Tests run on a fully integrated and deployed system with deployed doubles for external systems.
|
||||||
|
|
||||||
|
Acceptance-tests, are blackbox-tests and do <u>not</u> count into test-code-coverage.
|
||||||
|
|
||||||
|
TODO.test: Complete the Acceptance-Tests test concept.
|
||||||
|
|
||||||
|
|
||||||
|
#### Scenario-Tests
|
||||||
|
|
||||||
|
Our Scenario-tests are induced by business use-cases.
|
||||||
|
They test from the REST API all the way down to the database.
|
||||||
|
|
||||||
|
Most scenario-tests are positive tests, they test if business scenarios do work.
|
||||||
|
But few might be negative tests, which test if specific forbidden data gets rejected.
|
||||||
|
|
||||||
|
Our scenario tests also generate test-reports which contain the REST-API calls needed for each scenario.
|
||||||
|
These reports can be used as examples for the API usage from a business perspective.
|
||||||
|
|
||||||
|
There is an extra document regarding scenario-test, see [Scenario-Tests README](../src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/README.md).
|
||||||
|
|
||||||
|
|
||||||
|
#### Performance-Tests
|
||||||
|
|
||||||
|
Performance-critical scenarios have to be identified and a special performance-test has to be implemented.
|
||||||
|
|
||||||
|
The implementation-technologie depends on the scenario.
|
||||||
|
|
||||||
|
Performance-tests, are blackbox-tests and do <u>not</u> count into test-code-coverage.
|
||||||
|
|
||||||
|
Such tests usually are very slow and should not be automatically run in the build-pipeline but manually, after critical areas have been changed.
|
||||||
|
|
||||||
|
|
||||||
|
#### System-Integration-Tests
|
||||||
|
|
||||||
|
We define System-Integration-Tests as test in which this system is deployed in a production-like environment to test integration with external systems.
|
||||||
|
|
||||||
|
System-Integration-tests, are blackbox-tests and do <u>not</u> count into test-code-coverage.
|
||||||
|
|
||||||
|
TODO.test: Complete the System-Integration-Tests test concept.
|
54
etc/allowed-licenses.json
Normal file
54
etc/allowed-licenses.json
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
{
|
||||||
|
"allowedLicenses": [
|
||||||
|
{ "moduleLicense": "Apache 2" },
|
||||||
|
{ "moduleLicense": "Apache 2.0" },
|
||||||
|
{ "moduleLicense": "Apache-2.0" },
|
||||||
|
{ "moduleLicense": "Apache License 2.0" },
|
||||||
|
{ "moduleLicense": "Apache License v2.0" },
|
||||||
|
{ "moduleLicense": "Apache License, Version 2.0" },
|
||||||
|
{ "moduleLicense": "The Apache Software License, Version 2.0" },
|
||||||
|
|
||||||
|
{ "moduleLicense": "BSD License" },
|
||||||
|
{ "moduleLicense": "BSD-2-Clause" },
|
||||||
|
{ "moduleLicense": "BSD-3-Clause" },
|
||||||
|
{ "moduleLicense": "The BSD License" },
|
||||||
|
|
||||||
|
{ "moduleLicense": "The New BSD License" },
|
||||||
|
|
||||||
|
{ "moduleLicense": "CDDL 1.1" },
|
||||||
|
{ "moduleLicense": "CDDL/GPLv2+CE" },
|
||||||
|
{ "moduleLicense": "COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.0" },
|
||||||
|
|
||||||
|
{ "moduleLicense": "EDL 1.0" },
|
||||||
|
{ "moduleLicense": "Eclipse Distribution License 1.0" },
|
||||||
|
|
||||||
|
{ "moduleLicense": "Eclipse Public License - v 1.0" },
|
||||||
|
{ "moduleLicense": "Eclipse Public License - v 2.0" },
|
||||||
|
{ "moduleLicense": "Eclipse Public License - v. 2.0" },
|
||||||
|
{ "moduleLicense": "Eclipse Public License - v1.0" },
|
||||||
|
{ "moduleLicense": "Eclipse Public License v 2.0" },
|
||||||
|
{ "moduleLicense": "Eclipse Public License v. 2.0" },
|
||||||
|
|
||||||
|
{ "moduleLicense": "GNU Library General Public License v2.1 or later" },
|
||||||
|
{ "moduleLicense": "GNU General Public License, version 2 with the GNU Classpath Exception" },
|
||||||
|
{ "moduleLicense": "GPL2 w/ CPE" },
|
||||||
|
|
||||||
|
{ "moduleLicense": "LGPL, version 2.1"},
|
||||||
|
{ "moduleLicense": "LGPL-2.1-or-later"},
|
||||||
|
|
||||||
|
{ "moduleLicense": "MIT License" },
|
||||||
|
{ "moduleLicense": "MIT" },
|
||||||
|
{ "moduleLicense": "The MIT License (MIT)" },
|
||||||
|
{ "moduleLicense": "The MIT License" },
|
||||||
|
|
||||||
|
{ "moduleLicense": "WTFPL" },
|
||||||
|
|
||||||
|
{
|
||||||
|
"moduleLicense": null,
|
||||||
|
"#moduleLicense": "Apache License 2.0, see https://github.com/springdoc/springdoc-openapi/blob/main/LICENSE",
|
||||||
|
"moduleVersion": "2.4.0",
|
||||||
|
"moduleName": "org.springdoc:springdoc-openapi"
|
||||||
|
}
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
27
etc/docker-compose.yml
Normal file
27
etc/docker-compose.yml
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres-with-contrib:15.5-bookworm
|
||||||
|
container_name: custom-postgres
|
||||||
|
environment:
|
||||||
|
POSTGRES_PASSWORD: password
|
||||||
|
volumes:
|
||||||
|
- ./postgresql-log-slow-queries.conf:/etc/postgresql/postgresql.conf
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
command:
|
||||||
|
- bash
|
||||||
|
- -c
|
||||||
|
- >
|
||||||
|
apt-get update &&
|
||||||
|
apt-get install -y postgresql-contrib &&
|
||||||
|
docker-entrypoint.sh postgres -c config_file=/etc/postgresql/postgresql.conf
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '2'
|
||||||
|
memory: 8G
|
||||||
|
reservations:
|
||||||
|
cpus: '1'
|
||||||
|
memory: 2G
|
6
etc/jenkinsAgent.Dockerfile
Normal file
6
etc/jenkinsAgent.Dockerfile
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
FROM eclipse-temurin:21-jdk
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y bind9-utils pandoc && \
|
||||||
|
apt-get clean && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
16
etc/owasp-dependency-check-suppression.xml
Normal file
16
etc/owasp-dependency-check-suppression.xml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<suppressions xmlns="https://jeremylong.github.io/DependencyCheck/dependency-suppression.1.3.xsd">
|
||||||
|
<suppress>
|
||||||
|
<notes><![CDATA[
|
||||||
|
Internal tooling, not exposed to the Internet.
|
||||||
|
]]></notes>
|
||||||
|
<packageUrl regex="true">^pkg:maven/org\.pitest/pitest\-command\-line@.*$</packageUrl>
|
||||||
|
<cpe>cpe:/a:line:line</cpe>
|
||||||
|
</suppress>
|
||||||
|
<suppress>
|
||||||
|
<notes><![CDATA[
|
||||||
|
Malicious HTTP redirect in JAXB on a REST-endpoint is not that dangerous.
|
||||||
|
]]></notes>
|
||||||
|
<cve>CVE-2024-9329</cve>
|
||||||
|
</suppress>
|
||||||
|
</suppressions>
|
10
etc/postgresql-log-slow-queries.conf
Normal file
10
etc/postgresql-log-slow-queries.conf
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
shared_preload_libraries = 'pg_stat_statements,auto_explain'
|
||||||
|
log_min_duration_statement = 1000
|
||||||
|
log_statement = 'all'
|
||||||
|
log_duration = on
|
||||||
|
pg_stat_statements.track = all
|
||||||
|
auto_explain.log_min_duration = '1s' # Logs queries taking longer than 1 second
|
||||||
|
auto_explain.log_analyze = on # Include actual run times
|
||||||
|
auto_explain.log_buffers = on # Include buffer usage statistics
|
||||||
|
auto_explain.log_format = 'json' # Format the log output in JSON
|
||||||
|
listen_addresses = '*'
|
@ -1,53 +1,17 @@
|
|||||||
rootProject.name=hsadmin-ng
|
# Gradle Java Toolchain-support
|
||||||
profile=dev
|
org.gradle.java.installations.auto-detect=true
|
||||||
|
org.gradle.java.installations.auto-download=true
|
||||||
|
# org.gradle.jvm.toolchain.install.adoptopenjdk.baseUri
|
||||||
|
# org.gradle.java.installations.paths -- uncomment and set if needed
|
||||||
|
|
||||||
# Build properties
|
# Spring BOM overrides
|
||||||
node_version=10.15.3
|
# currently none necessary
|
||||||
npm_version=6.4.1
|
|
||||||
yarn_version=1.13.0
|
|
||||||
|
|
||||||
# Dependency versions
|
# TODO: can be removed if all dependencies are JDK 16 compliant, check with `gw clean check`
|
||||||
jhipster_dependencies_version=2.1.1
|
# and check output for "cannot access class ... because module jdk.compiler does not export ..."
|
||||||
# The spring-boot version should match the one managed by
|
org.gradle.jvmargs= \
|
||||||
# https://mvnrepository.com/artifact/io.github.jhipster/jhipster-dependencies/${jhipster_dependencies_version}
|
--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \
|
||||||
spring_boot_version=2.0.8.RELEASE
|
--add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \
|
||||||
# The hibernate version should match the one managed by
|
--add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \
|
||||||
# https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-dependencies/${spring-boot.version} -->
|
--add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \
|
||||||
hibernate_version=5.2.17.Final
|
--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
|
||||||
mapstruct_version=1.2.0.Final
|
|
||||||
|
|
||||||
liquibase_hibernate5_version=3.6
|
|
||||||
liquibaseTaskPrefix=liquibase
|
|
||||||
|
|
||||||
# jhipster-needle-gradle-property - JHipster will add additional properties here
|
|
||||||
|
|
||||||
## below are some of the gradle performance improvement settings that can be used as required, these are not enabled by default
|
|
||||||
|
|
||||||
## The Gradle daemon aims to improve the startup and execution time of Gradle.
|
|
||||||
## The daemon is enabled by default in Gradle 3+ setting this to false will disable this.
|
|
||||||
## TODO: disable daemon on CI, since builds should be clean and reliable on servers
|
|
||||||
## https://docs.gradle.org/current/userguide/gradle_daemon.html#sec:ways_to_disable_gradle_daemon
|
|
||||||
## un comment the below line to disable the daemon
|
|
||||||
|
|
||||||
#org.gradle.daemon=false
|
|
||||||
|
|
||||||
## Specifies the JVM arguments used for the daemon process.
|
|
||||||
## The setting is particularly useful for tweaking memory settings.
|
|
||||||
## Default value: -Xmx1024m -XX:MaxPermSize=256m
|
|
||||||
## un comment the below line to override the daemon defaults
|
|
||||||
|
|
||||||
#org.gradle.jvmargs=-Xmx1024m -XX:MaxPermSize=256m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
|
||||||
|
|
||||||
## When configured, Gradle will run in incubating parallel mode.
|
|
||||||
## This option should only be used with decoupled projects. More details, visit
|
|
||||||
## http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
|
||||||
## un comment the below line to enable parallel mode
|
|
||||||
|
|
||||||
#org.gradle.parallel=true
|
|
||||||
|
|
||||||
## Enables new incubating mode that makes Gradle selective when configuring projects.
|
|
||||||
## Only relevant projects are configured which results in faster builds for large multi-projects.
|
|
||||||
## http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:configuration_on_demand
|
|
||||||
## un comment the below line to enable the selective mode
|
|
||||||
|
|
||||||
#org.gradle.configureondemand=true
|
|
||||||
|
@ -1,35 +0,0 @@
|
|||||||
buildscript {
|
|
||||||
repositories {
|
|
||||||
gradlePluginPortal()
|
|
||||||
}
|
|
||||||
dependencies {
|
|
||||||
classpath "gradle.plugin.com.google.cloud.tools:jib-gradle-plugin:0.9.11"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
apply plugin: com.google.cloud.tools.jib.gradle.JibPlugin
|
|
||||||
|
|
||||||
jib {
|
|
||||||
from {
|
|
||||||
image = 'openjdk:8-jre-alpine'
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
image = 'hsadminng:latest'
|
|
||||||
}
|
|
||||||
container {
|
|
||||||
entrypoint = ['sh', '-c', 'chmod +x /entrypoint.sh && sync && /entrypoint.sh']
|
|
||||||
ports = ['8080']
|
|
||||||
environment = [
|
|
||||||
SPRING_OUTPUT_ANSI_ENABLED: 'ALWAYS',
|
|
||||||
JHIPSTER_SLEEP: '0'
|
|
||||||
]
|
|
||||||
useCurrentTimestamp = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
task copyWwwIntoStatic (type: Copy) {
|
|
||||||
from 'build/www/'
|
|
||||||
into 'build/resources/main/static'
|
|
||||||
}
|
|
||||||
|
|
||||||
jibDockerBuild.dependsOn copyWwwIntoStatic
|
|
@ -1,63 +0,0 @@
|
|||||||
import org.gradle.internal.os.OperatingSystem
|
|
||||||
|
|
||||||
apply plugin: 'org.springframework.boot'
|
|
||||||
apply plugin: 'com.moowork.node'
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
compile "org.springframework.boot:spring-boot-devtools"
|
|
||||||
compile "com.h2database:h2"
|
|
||||||
}
|
|
||||||
|
|
||||||
def profiles = 'dev'
|
|
||||||
if (project.hasProperty('no-liquibase')) {
|
|
||||||
profiles += ',no-liquibase'
|
|
||||||
}
|
|
||||||
if (project.hasProperty('tls')) {
|
|
||||||
profiles += ',tls'
|
|
||||||
}
|
|
||||||
|
|
||||||
springBoot {
|
|
||||||
buildInfo {
|
|
||||||
properties {
|
|
||||||
time = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bootRun {
|
|
||||||
args = []
|
|
||||||
}
|
|
||||||
|
|
||||||
task webpackBuildDev(type: NpmTask) {
|
|
||||||
inputs.files(fileTree('src/main/webapp/'))
|
|
||||||
|
|
||||||
def webpackDevFiles = fileTree('webpack//')
|
|
||||||
webpackDevFiles.exclude('webpack.prod.js')
|
|
||||||
inputs.files(webpackDevFiles)
|
|
||||||
|
|
||||||
outputs.files(fileTree("build/www/"))
|
|
||||||
|
|
||||||
dependsOn npmInstall
|
|
||||||
|
|
||||||
args = ["run", "webpack:build"]
|
|
||||||
}
|
|
||||||
|
|
||||||
task copyIntoStatic (type: Copy) {
|
|
||||||
from 'build/www/'
|
|
||||||
into 'build/resources/main/static'
|
|
||||||
}
|
|
||||||
|
|
||||||
processResources {
|
|
||||||
filesMatching('**/application.yml') {
|
|
||||||
filter {
|
|
||||||
it.replace('#project.version#', version)
|
|
||||||
}
|
|
||||||
filter {
|
|
||||||
it.replace('#spring.profiles.active#', profiles)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
processResources.dependsOn webpackBuildDev
|
|
||||||
copyIntoStatic.dependsOn processResources
|
|
||||||
bootJar.dependsOn copyIntoStatic
|
|
@ -1,63 +0,0 @@
|
|||||||
apply plugin: 'org.springframework.boot'
|
|
||||||
apply plugin: 'com.gorylenko.gradle-git-properties'
|
|
||||||
apply plugin: 'com.moowork.node'
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
testCompile "com.h2database:h2"
|
|
||||||
}
|
|
||||||
|
|
||||||
def profiles = 'prod'
|
|
||||||
if (project.hasProperty('no-liquibase')) {
|
|
||||||
profiles += ',no-liquibase'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (project.hasProperty('swagger')) {
|
|
||||||
profiles += ',swagger'
|
|
||||||
}
|
|
||||||
|
|
||||||
springBoot {
|
|
||||||
buildInfo()
|
|
||||||
}
|
|
||||||
|
|
||||||
bootRun {
|
|
||||||
args = []
|
|
||||||
}
|
|
||||||
|
|
||||||
task webpack_test(type: NpmTask, dependsOn: 'npm_install') {
|
|
||||||
args = ["run", "webpack:test"]
|
|
||||||
}
|
|
||||||
|
|
||||||
task webpack(type: NpmTask, dependsOn: 'npm_install') {
|
|
||||||
args = ["run", "webpack:prod"]
|
|
||||||
}
|
|
||||||
|
|
||||||
task copyIntoStatic (type: Copy) {
|
|
||||||
from 'build/www/'
|
|
||||||
into 'build/resources/main/static'
|
|
||||||
}
|
|
||||||
|
|
||||||
processResources {
|
|
||||||
filesMatching('**/application.yml') {
|
|
||||||
filter {
|
|
||||||
it.replace('#project.version#', version)
|
|
||||||
}
|
|
||||||
filter {
|
|
||||||
it.replace('#spring.profiles.active#', profiles)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
generateGitProperties {
|
|
||||||
onlyIf {
|
|
||||||
!source.isEmpty()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
gitProperties {
|
|
||||||
keys = ['git.branch', 'git.commit.id.abbrev', 'git.commit.id.describe']
|
|
||||||
}
|
|
||||||
|
|
||||||
test.dependsOn webpack_test
|
|
||||||
processResources.dependsOn webpack
|
|
||||||
copyIntoStatic.dependsOn processResources
|
|
||||||
bootJar.dependsOn copyIntoStatic
|
|
@ -1,47 +0,0 @@
|
|||||||
apply plugin: "org.sonarqube"
|
|
||||||
apply plugin: 'jacoco'
|
|
||||||
|
|
||||||
jacoco {
|
|
||||||
toolVersion = '0.8.2'
|
|
||||||
}
|
|
||||||
|
|
||||||
jacocoTestReport {
|
|
||||||
reports {
|
|
||||||
xml.enabled true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sonarqube {
|
|
||||||
properties {
|
|
||||||
property "sonar.host.url", "http://localhost:9001"
|
|
||||||
property "sonar.exclusions", "src/main/webapp/content/**/*.*,src/main/webapp/i18n/*.js, build/www/**/*.*"
|
|
||||||
|
|
||||||
property "sonar.issue.ignore.multicriteria", "S3437,S4502,S4684,UndocumentedApi,BoldAndItalicTagsCheck"
|
|
||||||
|
|
||||||
// Rule https://sonarcloud.io/coding_rules?open=Web%3ABoldAndItalicTagsCheck&rule_key=Web%3ABoldAndItalicTagsCheck is ignored. Even if we agree that using the "i" tag is an awful practice, this is what is recommended by http://fontawesome.io/examples/
|
|
||||||
property "sonar.issue.ignore.multicriteria.BoldAndItalicTagsCheck.resourceKey", ">src/main/webapp/app/**/*.*"
|
|
||||||
property "sonar.issue.ignore.multicriteria.BoldAndItalicTagsCheck.ruleKey", "Web:BoldAndItalicTagsCheck"
|
|
||||||
|
|
||||||
// Rule https://sonarcloud.io/coding_rules?open=squid%3AS3437&rule_key=squid%3AS3437 is ignored, as a JPA-managed field cannot be transient
|
|
||||||
property "sonar.issue.ignore.multicriteria.S3437.resourceKey", "src/main/java/**/*"
|
|
||||||
property "sonar.issue.ignore.multicriteria.S3437.ruleKey", "squid:S3437"
|
|
||||||
|
|
||||||
// Rule https://sonarcloud.io/coding_rules?open=squid%3AUndocumentedApi&rule_key=squid%3AUndocumentedApi is ignored, as we want to follow "clean code" guidelines and classes, methods and arguments names should be self-explanatory
|
|
||||||
property "sonar.issue.ignore.multicriteria.UndocumentedApi.resourceKey", "src/main/java/**/*"
|
|
||||||
property "sonar.issue.ignore.multicriteria.UndocumentedApi.ruleKey", "squid:UndocumentedApi"
|
|
||||||
// Rule https://sonarcloud.io/coding_rules?open=squid%3AS4502&rule_key=squid%3AS4502 is ignored, as for JWT tokens we are not subject to CSRF attack
|
|
||||||
property "sonar.issue.ignore.multicriteria.S4502.resourceKey", "src/main/java/**/*"
|
|
||||||
property "sonar.issue.ignore.multicriteria.S4502.ruleKey", "squid:S4502"
|
|
||||||
|
|
||||||
// Rule https://sonarcloud.io/coding_rules?open=squid%3AS4684&rule_key=squid%3AS4684
|
|
||||||
property "sonar.issue.ignore.multicriteria.S4684.resourceKey", "src/main/java/**/*"
|
|
||||||
property "sonar.issue.ignore.multicriteria.S4684.ruleKey", "squid:S4684"
|
|
||||||
|
|
||||||
property "sonar.jacoco.reportPaths", "${project.buildDir}/jacoco/test.exec"
|
|
||||||
property "sonar.java.codeCoveragePlugin", "jacoco"
|
|
||||||
property "sonar.typescript.lcov.reportPaths", "${project.buildDir}/test-results/lcov.info"
|
|
||||||
property "sonar.junit.reportPaths", "${project.buildDir}/test-results"
|
|
||||||
property "sonar.sources", "${project.projectDir}/src/main/"
|
|
||||||
property "sonar.tests", "${project.projectDir}/src/test/"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,28 +0,0 @@
|
|||||||
/*
|
|
||||||
* Plugin that provides API-first development using OpenAPI-generator to
|
|
||||||
* generate Spring-MVC endpoint stubs at compile time from an OpenAPI definition file
|
|
||||||
*/
|
|
||||||
apply plugin: 'org.openapi.generator'
|
|
||||||
|
|
||||||
openApiGenerate {
|
|
||||||
generatorName = "spring"
|
|
||||||
inputSpec = "$rootDir/src/main/resources/swagger/api.yml".toString()
|
|
||||||
outputDir = "$buildDir/openapi".toString()
|
|
||||||
apiPackage = "org.hostsharing.hsadminng.web.api"
|
|
||||||
modelPackage = "org.hostsharing.hsadminng.web.api.model"
|
|
||||||
apiFilesConstrainedTo = [""]
|
|
||||||
modelFilesConstrainedTo = [""]
|
|
||||||
supportingFilesConstrainedTo = ["ApiUtil.java"]
|
|
||||||
configOptions = [delegatePattern: "true"]
|
|
||||||
validateSpec = true
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceSets {
|
|
||||||
main {
|
|
||||||
java {
|
|
||||||
srcDir file("${project.buildDir.path}/openapi/src/main/java")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
compileJava.dependsOn("openApiGenerate")
|
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,5 +1,7 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
|
||||||
|
networkTimeout=10000
|
||||||
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
dependencies {
|
|
||||||
compile "org.springframework.cloud:spring-cloud-starter-zipkin"
|
|
||||||
}
|
|
309
gradlew
vendored
309
gradlew
vendored
@ -1,78 +1,127 @@
|
|||||||
#!/usr/bin/env sh
|
#!/bin/sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright © 2015-2021 the original authors.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
##############################################################################
|
##############################################################################
|
||||||
##
|
#
|
||||||
## Gradle start up script for UN*X
|
# Gradle start up script for POSIX generated by Gradle.
|
||||||
##
|
#
|
||||||
|
# Important for running:
|
||||||
|
#
|
||||||
|
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||||
|
# noncompliant, but you have some other compliant shell such as ksh or
|
||||||
|
# bash, then to run this script, type that shell name before the whole
|
||||||
|
# command line, like:
|
||||||
|
#
|
||||||
|
# ksh Gradle
|
||||||
|
#
|
||||||
|
# Busybox and similar reduced shells will NOT work, because this script
|
||||||
|
# requires all of these POSIX shell features:
|
||||||
|
# * functions;
|
||||||
|
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||||
|
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||||
|
# * compound commands having a testable exit status, especially «case»;
|
||||||
|
# * various built-in commands including «command», «set», and «ulimit».
|
||||||
|
#
|
||||||
|
# Important for patching:
|
||||||
|
#
|
||||||
|
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||||
|
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||||
|
#
|
||||||
|
# The "traditional" practice of packing multiple parameters into a
|
||||||
|
# space-separated string is a well documented source of bugs and security
|
||||||
|
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||||
|
# options in "$@", and eventually passing that to Java.
|
||||||
|
#
|
||||||
|
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||||
|
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||||
|
# see the in-line comments for details.
|
||||||
|
#
|
||||||
|
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||||
|
# Darwin, MinGW, and NonStop.
|
||||||
|
#
|
||||||
|
# (3) This script is generated from the Groovy template
|
||||||
|
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
|
# within the Gradle project.
|
||||||
|
#
|
||||||
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
#
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
# Attempt to set APP_HOME
|
# Attempt to set APP_HOME
|
||||||
|
|
||||||
# Resolve links: $0 may be a link
|
# Resolve links: $0 may be a link
|
||||||
PRG="$0"
|
app_path=$0
|
||||||
# Need this for relative symlinks.
|
|
||||||
while [ -h "$PRG" ] ; do
|
# Need this for daisy-chained symlinks.
|
||||||
ls=`ls -ld "$PRG"`
|
while
|
||||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||||
if expr "$link" : '/.*' > /dev/null; then
|
[ -h "$app_path" ]
|
||||||
PRG="$link"
|
do
|
||||||
else
|
ls=$( ls -ld "$app_path" )
|
||||||
PRG=`dirname "$PRG"`"/$link"
|
link=${ls#*' -> '}
|
||||||
fi
|
case $link in #(
|
||||||
|
/*) app_path=$link ;; #(
|
||||||
|
*) app_path=$APP_HOME$link ;;
|
||||||
|
esac
|
||||||
done
|
done
|
||||||
SAVED="`pwd`"
|
|
||||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
|
||||||
APP_HOME="`pwd -P`"
|
|
||||||
cd "$SAVED" >/dev/null
|
|
||||||
|
|
||||||
APP_NAME="Gradle"
|
# This is normally unused
|
||||||
APP_BASE_NAME=`basename "$0"`
|
# shellcheck disable=SC2034
|
||||||
|
APP_BASE_NAME=${0##*/}
|
||||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||||
DEFAULT_JVM_OPTS=""
|
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
MAX_FD="maximum"
|
MAX_FD=maximum
|
||||||
|
|
||||||
warn () {
|
warn () {
|
||||||
echo "$*"
|
echo "$*"
|
||||||
}
|
} >&2
|
||||||
|
|
||||||
die () {
|
die () {
|
||||||
echo
|
echo
|
||||||
echo "$*"
|
echo "$*"
|
||||||
echo
|
echo
|
||||||
exit 1
|
exit 1
|
||||||
}
|
} >&2
|
||||||
|
|
||||||
# OS specific support (must be 'true' or 'false').
|
# OS specific support (must be 'true' or 'false').
|
||||||
cygwin=false
|
cygwin=false
|
||||||
msys=false
|
msys=false
|
||||||
darwin=false
|
darwin=false
|
||||||
nonstop=false
|
nonstop=false
|
||||||
case "`uname`" in
|
case "$( uname )" in #(
|
||||||
CYGWIN* )
|
CYGWIN* ) cygwin=true ;; #(
|
||||||
cygwin=true
|
Darwin* ) darwin=true ;; #(
|
||||||
;;
|
MSYS* | MINGW* ) msys=true ;; #(
|
||||||
Darwin* )
|
NONSTOP* ) nonstop=true ;;
|
||||||
darwin=true
|
|
||||||
;;
|
|
||||||
MINGW* )
|
|
||||||
msys=true
|
|
||||||
;;
|
|
||||||
NONSTOP* )
|
|
||||||
nonstop=true
|
|
||||||
;;
|
|
||||||
esac
|
esac
|
||||||
|
|
||||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
# Determine the Java command to use to start the JVM.
|
# Determine the Java command to use to start the JVM.
|
||||||
if [ -n "$JAVA_HOME" ] ; then
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
# IBM's JDK on AIX uses strange locations for the executables
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||||
else
|
else
|
||||||
JAVACMD="$JAVA_HOME/bin/java"
|
JAVACMD=$JAVA_HOME/bin/java
|
||||||
fi
|
fi
|
||||||
if [ ! -x "$JAVACMD" ] ; then
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
@ -81,92 +130,120 @@ Please set the JAVA_HOME variable in your environment to match the
|
|||||||
location of your Java installation."
|
location of your Java installation."
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
JAVACMD="java"
|
JAVACMD=java
|
||||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
if ! command -v java >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
Please set the JAVA_HOME variable in your environment to match the
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
location of your Java installation."
|
location of your Java installation."
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# Increase the maximum file descriptors if we can.
|
# Increase the maximum file descriptors if we can.
|
||||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
MAX_FD_LIMIT=`ulimit -H -n`
|
case $MAX_FD in #(
|
||||||
if [ $? -eq 0 ] ; then
|
max*)
|
||||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||||
MAX_FD="$MAX_FD_LIMIT"
|
# shellcheck disable=SC2039,SC3045
|
||||||
fi
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
ulimit -n $MAX_FD
|
warn "Could not query maximum file descriptor limit"
|
||||||
if [ $? -ne 0 ] ; then
|
esac
|
||||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
case $MAX_FD in #(
|
||||||
fi
|
'' | soft) :;; #(
|
||||||
else
|
*)
|
||||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||||
fi
|
# shellcheck disable=SC2039,SC3045
|
||||||
fi
|
ulimit -n "$MAX_FD" ||
|
||||||
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
# For Darwin, add options to specify how the application appears in the dock
|
|
||||||
if $darwin; then
|
|
||||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
|
||||||
fi
|
|
||||||
|
|
||||||
# For Cygwin, switch paths to Windows format before running java
|
|
||||||
if $cygwin ; then
|
|
||||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
|
||||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
|
||||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
|
||||||
|
|
||||||
# We build the pattern for arguments to be converted via cygpath
|
|
||||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
|
||||||
SEP=""
|
|
||||||
for dir in $ROOTDIRSRAW ; do
|
|
||||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
|
||||||
SEP="|"
|
|
||||||
done
|
|
||||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
|
||||||
# Add a user-defined pattern to the cygpath arguments
|
|
||||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
|
||||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
|
||||||
fi
|
|
||||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
|
||||||
i=0
|
|
||||||
for arg in "$@" ; do
|
|
||||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
|
||||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
|
||||||
|
|
||||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
|
||||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
|
||||||
else
|
|
||||||
eval `echo args$i`="\"$arg\""
|
|
||||||
fi
|
|
||||||
i=$((i+1))
|
|
||||||
done
|
|
||||||
case $i in
|
|
||||||
(0) set -- ;;
|
|
||||||
(1) set -- "$args0" ;;
|
|
||||||
(2) set -- "$args0" "$args1" ;;
|
|
||||||
(3) set -- "$args0" "$args1" "$args2" ;;
|
|
||||||
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
|
||||||
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
|
||||||
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
|
||||||
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
|
||||||
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
|
||||||
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
|
||||||
esac
|
esac
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Escape application args
|
# Collect all arguments for the java command, stacking in reverse order:
|
||||||
save () {
|
# * args from the command line
|
||||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
# * the main class name
|
||||||
echo " "
|
# * -classpath
|
||||||
}
|
# * -D...appname settings
|
||||||
APP_ARGS=$(save "$@")
|
# * --module-path (only if needed)
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||||
|
|
||||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
if "$cygwin" || "$msys" ; then
|
||||||
|
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||||
|
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||||
|
|
||||||
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
|
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||||
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
|
|
||||||
cd "$(dirname "$0")"
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
for arg do
|
||||||
|
if
|
||||||
|
case $arg in #(
|
||||||
|
-*) false ;; # don't mess with options #(
|
||||||
|
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||||
|
[ -e "$t" ] ;; #(
|
||||||
|
*) false ;;
|
||||||
|
esac
|
||||||
|
then
|
||||||
|
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||||
|
fi
|
||||||
|
# Roll the args list around exactly as many times as the number of
|
||||||
|
# args, so each arg winds up back in the position where it started, but
|
||||||
|
# possibly modified.
|
||||||
|
#
|
||||||
|
# NB: a `for` loop captures its iteration list before it begins, so
|
||||||
|
# changing the positional parameters here affects neither the number of
|
||||||
|
# iterations, nor the values presented in `arg`.
|
||||||
|
shift # remove old arg
|
||||||
|
set -- "$@" "$arg" # push replacement arg
|
||||||
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
|
# Collect all arguments for the java command:
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||||
|
# and any embedded shellness will be escaped.
|
||||||
|
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||||
|
# treated as '${Hostname}' itself on the command line.
|
||||||
|
|
||||||
|
set -- \
|
||||||
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
|
-classpath "$CLASSPATH" \
|
||||||
|
org.gradle.wrapper.GradleWrapperMain \
|
||||||
|
"$@"
|
||||||
|
|
||||||
|
# Stop when "xargs" is not available.
|
||||||
|
if ! command -v xargs >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "xargs is not available"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use "xargs" to parse quoted args.
|
||||||
|
#
|
||||||
|
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||||
|
#
|
||||||
|
# In Bash we could simply go:
|
||||||
|
#
|
||||||
|
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||||
|
# set -- "${ARGS[@]}" "$@"
|
||||||
|
#
|
||||||
|
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||||
|
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||||
|
# character that might be a shell metacharacter, then use eval to reverse
|
||||||
|
# that process (while maintaining the separation between arguments), and wrap
|
||||||
|
# the whole thing up as a single "set" statement.
|
||||||
|
#
|
||||||
|
# This will of course break if any of these variables contains a newline or
|
||||||
|
# an unmatched quote.
|
||||||
|
#
|
||||||
|
|
||||||
|
eval "set -- $(
|
||||||
|
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||||
|
xargs -n1 |
|
||||||
|
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||||
|
tr '\n' ' '
|
||||||
|
)" '"$@"'
|
||||||
|
|
||||||
exec "$JAVACMD" "$@"
|
exec "$JAVACMD" "$@"
|
||||||
|
52
gradlew.bat
vendored
52
gradlew.bat
vendored
@ -1,3 +1,19 @@
|
|||||||
|
@rem
|
||||||
|
@rem Copyright 2015 the original author or authors.
|
||||||
|
@rem
|
||||||
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@rem you may not use this file except in compliance with the License.
|
||||||
|
@rem You may obtain a copy of the License at
|
||||||
|
@rem
|
||||||
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@rem
|
||||||
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@rem See the License for the specific language governing permissions and
|
||||||
|
@rem limitations under the License.
|
||||||
|
@rem
|
||||||
|
|
||||||
@if "%DEBUG%"=="" @echo off
|
@if "%DEBUG%"=="" @echo off
|
||||||
@rem ##########################################################################
|
@rem ##########################################################################
|
||||||
@rem
|
@rem
|
||||||
@ -10,18 +26,22 @@ if "%OS%"=="Windows_NT" setlocal
|
|||||||
|
|
||||||
set DIRNAME=%~dp0
|
set DIRNAME=%~dp0
|
||||||
if "%DIRNAME%"=="" set DIRNAME=.
|
if "%DIRNAME%"=="" set DIRNAME=.
|
||||||
|
@rem This is normally unused
|
||||||
set APP_BASE_NAME=%~n0
|
set APP_BASE_NAME=%~n0
|
||||||
set APP_HOME=%DIRNAME%
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
set DEFAULT_JVM_OPTS=
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
@rem Find java.exe
|
@rem Find java.exe
|
||||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
set JAVA_EXE=java.exe
|
set JAVA_EXE=java.exe
|
||||||
%JAVA_EXE% -version >NUL 2>&1
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
if "%ERRORLEVEL%" == "0" goto init
|
if %ERRORLEVEL% equ 0 goto execute
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
@ -35,7 +55,7 @@ goto fail
|
|||||||
set JAVA_HOME=%JAVA_HOME:"=%
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
if exist "%JAVA_EXE%" goto init
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
@ -45,38 +65,26 @@ echo location of your Java installation.
|
|||||||
|
|
||||||
goto fail
|
goto fail
|
||||||
|
|
||||||
:init
|
|
||||||
@rem Get command-line arguments, handling Windows variants
|
|
||||||
|
|
||||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
|
||||||
|
|
||||||
:win9xME_args
|
|
||||||
@rem Slurp the command line arguments.
|
|
||||||
set CMD_LINE_ARGS=
|
|
||||||
set _SKIP=2
|
|
||||||
|
|
||||||
:win9xME_args_slurp
|
|
||||||
if "x%~1" == "x" goto execute
|
|
||||||
|
|
||||||
set CMD_LINE_ARGS=%*
|
|
||||||
|
|
||||||
:execute
|
:execute
|
||||||
@rem Setup the command line
|
@rem Setup the command line
|
||||||
|
|
||||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
@rem Execute Gradle
|
@rem Execute Gradle
|
||||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||||
|
|
||||||
:end
|
:end
|
||||||
@rem End local scope for the variables with windows NT shell
|
@rem End local scope for the variables with windows NT shell
|
||||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||||
|
|
||||||
:fail
|
:fail
|
||||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
rem the _cmd.exe /c_ return code!
|
rem the _cmd.exe /c_ return code!
|
||||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
set EXIT_CODE=%ERRORLEVEL%
|
||||||
exit /b 1
|
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||||
|
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||||
|
exit /b %EXIT_CODE%
|
||||||
|
|
||||||
:mainEnd
|
:mainEnd
|
||||||
if "%OS%"=="Windows_NT" endlocal
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
1
lombok.config
Normal file
1
lombok.config
Normal file
@ -0,0 +1 @@
|
|||||||
|
lombok.addLombokGeneratedAnnotation = true
|
18180
package-lock.json
generated
18180
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
126
package.json
126
package.json
@ -1,126 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "hsadmin-ng",
|
|
||||||
"version": "0.0.0",
|
|
||||||
"description": "Description for hsadminNg",
|
|
||||||
"private": true,
|
|
||||||
"license": "UNLICENSED",
|
|
||||||
"cacheDirectories": [
|
|
||||||
"node_modules"
|
|
||||||
],
|
|
||||||
"dependencies": {
|
|
||||||
"@angular/common": "7.2.4",
|
|
||||||
"@angular/compiler": "7.2.4",
|
|
||||||
"@angular/core": "7.2.4",
|
|
||||||
"@angular/forms": "7.2.4",
|
|
||||||
"@angular/platform-browser": "7.2.4",
|
|
||||||
"@angular/platform-browser-dynamic": "7.2.4",
|
|
||||||
"@angular/router": "7.2.4",
|
|
||||||
"@fortawesome/angular-fontawesome": "0.3.0",
|
|
||||||
"@fortawesome/fontawesome-svg-core": "1.2.14",
|
|
||||||
"@fortawesome/free-solid-svg-icons": "5.7.1",
|
|
||||||
"@ng-bootstrap/ng-bootstrap": "4.0.2",
|
|
||||||
"@ngx-translate/core": "11.0.1",
|
|
||||||
"@ngx-translate/http-loader": "4.0.0",
|
|
||||||
"bootstrap": "4.2.1",
|
|
||||||
"core-js": "2.6.4",
|
|
||||||
"moment": "2.24.0",
|
|
||||||
"ng-jhipster": "0.9.1",
|
|
||||||
"ngx-cookie": "2.0.1",
|
|
||||||
"ngx-infinite-scroll": "7.0.1",
|
|
||||||
"ngx-webstorage": "2.0.1",
|
|
||||||
"rxjs": "6.4.0",
|
|
||||||
"swagger-ui": "2.2.10",
|
|
||||||
"tslib": "1.9.3",
|
|
||||||
"zone.js": "0.8.29"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@angular/cli": "7.3.1",
|
|
||||||
"@angular/compiler-cli": "7.2.4",
|
|
||||||
"@ngtools/webpack": "7.3.1",
|
|
||||||
"@types/jest": "24.0.0",
|
|
||||||
"@types/node": "10.12.24",
|
|
||||||
"angular-router-loader": "0.8.5",
|
|
||||||
"angular2-template-loader": "0.6.2",
|
|
||||||
"autoprefixer": "9.4.7",
|
|
||||||
"browser-sync": "2.26.3",
|
|
||||||
"browser-sync-webpack-plugin": "2.2.2",
|
|
||||||
"cache-loader": "2.0.1",
|
|
||||||
"codelyzer": "4.5.0",
|
|
||||||
"copy-webpack-plugin": "4.6.0",
|
|
||||||
"css-loader": "2.1.0",
|
|
||||||
"file-loader": "3.0.1",
|
|
||||||
"fork-ts-checker-webpack-plugin": "0.5.2",
|
|
||||||
"friendly-errors-webpack-plugin": "1.7.0",
|
|
||||||
"generator-jhipster": "5.8.2",
|
|
||||||
"html-loader": "0.5.5",
|
|
||||||
"html-webpack-plugin": "3.2.0",
|
|
||||||
"husky": "1.3.1",
|
|
||||||
"jest": "24.1.0",
|
|
||||||
"jest-junit": "6.2.1",
|
|
||||||
"jest-preset-angular": "6.0.2",
|
|
||||||
"jest-sonar-reporter": "2.0.0",
|
|
||||||
"lint-staged": "8.1.3",
|
|
||||||
"merge-jsons-webpack-plugin": "1.0.18",
|
|
||||||
"mini-css-extract-plugin": "0.5.0",
|
|
||||||
"moment-locales-webpack-plugin": "1.0.7",
|
|
||||||
"optimize-css-assets-webpack-plugin": "5.0.1",
|
|
||||||
"prettier": "1.16.4",
|
|
||||||
"reflect-metadata": "0.1.13",
|
|
||||||
"rimraf": "2.6.3",
|
|
||||||
"simple-progress-webpack-plugin": "1.1.2",
|
|
||||||
"style-loader": "0.23.1",
|
|
||||||
"terser-webpack-plugin": "1.2.2",
|
|
||||||
"thread-loader": "2.1.2",
|
|
||||||
"to-string-loader": "1.1.5",
|
|
||||||
"ts-loader": "5.3.3",
|
|
||||||
"tslint": "5.12.1",
|
|
||||||
"tslint-config-prettier": "1.18.0",
|
|
||||||
"tslint-loader": "3.6.0",
|
|
||||||
"typescript": "3.2.4",
|
|
||||||
"postcss-loader": "3.0.0",
|
|
||||||
"webpack": "4.29.3",
|
|
||||||
"webpack-cli": "3.2.3",
|
|
||||||
"webpack-dev-server": "3.1.14",
|
|
||||||
"webpack-merge": "4.2.1",
|
|
||||||
"webpack-notifier": "1.7.0",
|
|
||||||
"webpack-visualizer-plugin": "0.1.11",
|
|
||||||
"workbox-webpack-plugin": "3.6.3",
|
|
||||||
"write-file-webpack-plugin": "4.5.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8.9.0"
|
|
||||||
},
|
|
||||||
"lint-staged": {
|
|
||||||
"{,src/**/}*.{md,json,ts,css,scss}": [
|
|
||||||
"prettier --write",
|
|
||||||
"git add"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"prettier:format": "prettier --write \"{,src/**/}*.{md,json,ts,css,scss}\"",
|
|
||||||
"lint": "tslint --project tsconfig.json -e 'node_modules/**'",
|
|
||||||
"lint:fix": "npm run lint -- --fix",
|
|
||||||
"ngc": "ngc -p tsconfig-aot.json",
|
|
||||||
"cleanup": "rimraf build/{aot,www}",
|
|
||||||
"clean-www": "rimraf build//www/app/{src,build/}",
|
|
||||||
"start": "npm run webpack:dev",
|
|
||||||
"start-tls": "npm run webpack:dev -- --env.tls",
|
|
||||||
"serve": "npm run start",
|
|
||||||
"build": "npm run webpack:prod",
|
|
||||||
"test": "npm run lint && jest --coverage --logHeapUsage -w=2 --config src/test/javascript/jest.conf.js",
|
|
||||||
"test:watch": "npm run test -- --watch",
|
|
||||||
"webpack:dev": "npm run webpack-dev-server -- --config webpack/webpack.dev.js --inline --hot --port=9060 --watch-content-base --env.stats=minimal",
|
|
||||||
"webpack:dev-verbose": "npm run webpack-dev-server -- --config webpack/webpack.dev.js --inline --hot --port=9060 --watch-content-base --profile --progress --env.stats=normal",
|
|
||||||
"webpack:build:main": "npm run webpack -- --config webpack/webpack.dev.js --env.stats=minimal",
|
|
||||||
"webpack:build": "npm run cleanup && npm run webpack:build:main",
|
|
||||||
"webpack:prod:main": "npm run webpack -- --config webpack/webpack.prod.js --profile",
|
|
||||||
"webpack:prod": "npm run cleanup && npm run webpack:prod:main && npm run clean-www",
|
|
||||||
"webpack:test": "npm run test",
|
|
||||||
"webpack-dev-server": "node --max_old_space_size=4096 node_modules/webpack-dev-server/bin/webpack-dev-server.js",
|
|
||||||
"webpack": "node --max_old_space_size=4096 node_modules/webpack/bin/webpack.js"
|
|
||||||
},
|
|
||||||
"jestSonar": {
|
|
||||||
"reportPath": "build/test-results/jest",
|
|
||||||
"reportFile": "TESTS-results-sonar.xml"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
plugins: [
|
|
||||||
require('autoprefixer')
|
|
||||||
]
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"*": {
|
|
||||||
"target": "http://localhost:8080",
|
|
||||||
"secure": false,
|
|
||||||
"loglevel": "debug"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1 +1,14 @@
|
|||||||
|
pluginManagement {
|
||||||
|
repositories {
|
||||||
|
maven { url 'https://repo.spring.io/milestone' }
|
||||||
|
maven { url 'https://repo.spring.io/snapshot' }
|
||||||
|
gradlePluginPortal()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id 'org.gradle.toolchains.foojay-resolver-convention' version '0.7.0'
|
||||||
|
}
|
||||||
|
|
||||||
rootProject.name = 'hsadmin-ng'
|
rootProject.name = 'hsadmin-ng'
|
||||||
|
28
sql/29-hs-statistics.sql
Normal file
28
sql/29-hs-statistics.sql
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
-- ========================================================
|
||||||
|
-- Some Business Table Statistics
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
|
drop view if exists "BusinessTableStatisticsV";
|
||||||
|
create view "BusinessTableStatisticsV" as
|
||||||
|
select no,
|
||||||
|
to_char("count", '999 999 999') as "count",
|
||||||
|
to_char("required", '999 999 999') as "required",
|
||||||
|
to_char("count"::float / "required"::float, '990.999') as "factor",
|
||||||
|
"table"
|
||||||
|
from (select 1 as no, count(*) as "count", 7000 as "required", 'customers' as "table"
|
||||||
|
from customer
|
||||||
|
union
|
||||||
|
select 2 as no, count(*) as "count", 15000 as "required", 'packages' as "table"
|
||||||
|
from package
|
||||||
|
union
|
||||||
|
select 3 as no, count(*) as "count", 150000 as "required", 'domain' as "table"
|
||||||
|
from domain
|
||||||
|
union
|
||||||
|
select 4 as no, count(*) as "count", 100000 as "required", 'domain' as "table"
|
||||||
|
from domain
|
||||||
|
union
|
||||||
|
select 5 as no, count(*) as "count", 500000 as "required", 'emailaddress' as "table"
|
||||||
|
from emailaddress) totals
|
||||||
|
order by totals.no;
|
||||||
|
|
||||||
|
select * from "BusinessTableStatisticsV";
|
39
sql/historization.sql
Normal file
39
sql/historization.sql
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
|
||||||
|
-- ========================================================
|
||||||
|
-- Historization twiddle
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
|
rollback;
|
||||||
|
begin transaction;
|
||||||
|
call defineContext('historization testing', null, 'superuser-alex@hostsharing.net',
|
||||||
|
-- 'hs_booking.project#D-1000000-hshdefaultproject:ADMIN'); -- prod+test
|
||||||
|
'hs_booking.project#D-1000313-D-1000313defaultproject:ADMIN'); -- prod+test
|
||||||
|
-- 'hs_booking.project#D-1000300-mihdefaultproject:ADMIN'); -- prod
|
||||||
|
-- 'hs_booking.project#D-1000300-mimdefaultproject:ADMIN'); -- test
|
||||||
|
-- update hs_hosting.asset set caption='lug00 b' where identifier = 'lug00' and type = 'MANAGED_WEBSPACE'; -- prod
|
||||||
|
-- update hs_hosting.asset set caption='hsh00 A ' || now()::text where identifier = 'hsh00' and type = 'MANAGED_WEBSPACE'; -- test
|
||||||
|
-- update hs_hosting.asset set caption='hsh00 B ' || now()::text where identifier = 'hsh00' and type = 'MANAGED_WEBSPACE'; -- test
|
||||||
|
|
||||||
|
-- insert into hs_hosting.asset
|
||||||
|
-- (uuid, bookingitemuuid, type, parentassetuuid, assignedtoassetuuid, identifier, caption, config, alarmcontactuuid)
|
||||||
|
-- values
|
||||||
|
-- (uuid_generate_v4(), null, 'EMAIL_ADDRESS', 'bbda5895-0569-4e20-bb4c-34f3a38f3f63'::uuid, null,
|
||||||
|
-- 'new@thi.example.org', 'some new E-Mail-Address', '{}'::jsonb, null);
|
||||||
|
|
||||||
|
delete from hs_hosting.asset where uuid='5aea68d2-3b55-464f-8362-b05c76c5a681'::uuid;
|
||||||
|
commit;
|
||||||
|
|
||||||
|
-- single version at point in time
|
||||||
|
-- set hsadminng.tx_history_txid to (select max(txid) from base.tx_context where txtimestamp<='2024-08-27 12:13:13.450821');
|
||||||
|
set hsadminng.tx_history_txid to '';
|
||||||
|
set hsadminng.tx_history_timestamp to '2024-08-29 12:42';
|
||||||
|
-- all versions
|
||||||
|
select base.tx_history_txid(), txc.txtimestamp, txc.currentSubject, txc.currentTask, haex.*
|
||||||
|
from hs_hosting.asset_ex haex
|
||||||
|
join base.tx_context txc on haex.txid=txc.txid
|
||||||
|
where haex.identifier = 'test@thi.example.org';
|
||||||
|
|
||||||
|
select uuid, version, type, identifier, caption from hs_hosting.asset_hv p where identifier = 'test@thi.example.org';
|
||||||
|
|
||||||
|
select pg_current_xact_id();
|
||||||
|
|
50
sql/rbac-tests.sql
Normal file
50
sql/rbac-tests.sql
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
-- ========================================================
|
||||||
|
-- Some Tests
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
select rbac.isGranted(rbac.findRoleId('administrators'), rbac.findRoleId('rbactest.package#aaa00:OWNER'));
|
||||||
|
select rbac.isGranted(rbac.findRoleId('rbactest.package#aaa00:OWNER'), rbac.findRoleId('administrators'));
|
||||||
|
-- call rbac.grantRoleToRole(findRoleId('rbactest.package#aaa00:OWNER'), findRoleId('administrators'));
|
||||||
|
-- call rbac.grantRoleToRole(findRoleId('administrators'), findRoleId('rbactest.package#aaa00:OWNER'));
|
||||||
|
|
||||||
|
select count(*)
|
||||||
|
FROM rbac.queryAllPermissionsOfSubjectIdForObjectUuids(rbac.findRbacSubject('superuser-fran@hostsharing.net'),
|
||||||
|
ARRAY(select uuid from rbactest.customer where reference < 1100000));
|
||||||
|
select count(*)
|
||||||
|
FROM rbac.queryAllPermissionsOfSubjectId(findRbacSubject('superuser-fran@hostsharing.net'));
|
||||||
|
select *
|
||||||
|
FROM rbac.queryAllPermissionsOfSubjectId(findRbacSubject('alex@example.com'));
|
||||||
|
select *
|
||||||
|
FROM rbac.queryAllPermissionsOfSubjectId(findRbacSubject('rosa@example.com'));
|
||||||
|
|
||||||
|
select *
|
||||||
|
FROM rbac.queryAllRbacSubjectsWithPermissionsFor(rbac.findEffectivePermissionId('customer',
|
||||||
|
(SELECT uuid FROM rbac.RbacObject WHERE objectTable = 'customer' LIMIT 1),
|
||||||
|
'add-package'));
|
||||||
|
select *
|
||||||
|
FROM rbac.queryAllRbacSubjectsWithPermissionsFor(rbac.findEffectivePermissionId('package',
|
||||||
|
(SELECT uuid FROM rbac.RbacObject WHERE objectTable = 'package' LIMIT 1),
|
||||||
|
'DELETE'));
|
||||||
|
|
||||||
|
DO LANGUAGE plpgsql
|
||||||
|
$$
|
||||||
|
DECLARE
|
||||||
|
userId uuid;
|
||||||
|
result bool;
|
||||||
|
BEGIN
|
||||||
|
userId = rbac.findRbacSubject('superuser-alex@hostsharing.net');
|
||||||
|
result = (SELECT * FROM rbac.isPermissionGrantedToSubject(rbac.findPermissionId('package', 94928, 'add-package'), userId));
|
||||||
|
IF (result) THEN
|
||||||
|
RAISE EXCEPTION 'expected permission NOT to be granted, but it is';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
result = (SELECT * FROM rbac.isPermissionGrantedToSubject(rbac.findPermissionId('package', 94928, 'SELECT'), userId));
|
||||||
|
IF (NOT result) THEN
|
||||||
|
RAISE EXCEPTION 'expected permission to be granted, but it is NOT';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
RAISE LOG 'isPermissionGrantedToSubjectId test passed';
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
89
sql/rbac-view-option-experiments.sql
Normal file
89
sql/rbac-view-option-experiments.sql
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
|
||||||
|
-- ========================================================
|
||||||
|
-- Options for SELECT under RBAC rules
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
|
-- access control via view policy and isPermissionGrantedToSubject - way too slow (33 s 617ms for 1 million rows)
|
||||||
|
SET SESSION AUTHORIZATION DEFAULT;
|
||||||
|
CREATE ROLE admin;
|
||||||
|
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO admin;
|
||||||
|
CREATE ROLE restricted;
|
||||||
|
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO restricted;
|
||||||
|
|
||||||
|
SET SESSION AUTHORIZATION DEFAULT;
|
||||||
|
ALTER TABLE customer DISABLE ROW LEVEL SECURITY;
|
||||||
|
ALTER TABLE customer ENABLE ROW LEVEL SECURITY;
|
||||||
|
ALTER TABLE customer FORCE ROW LEVEL SECURITY;
|
||||||
|
DROP POLICY IF EXISTS customer_policy ON customer;
|
||||||
|
CREATE POLICY customer_policy ON customer
|
||||||
|
FOR SELECT
|
||||||
|
TO restricted
|
||||||
|
USING (
|
||||||
|
-- id=1000
|
||||||
|
rbac.isPermissionGrantedToSubject(rbac.findEffectivePermissionId('rbactest.customer', id, 'SELECT'), rbac.currentSubjectUuid())
|
||||||
|
);
|
||||||
|
|
||||||
|
SET SESSION AUTHORIZATION restricted;
|
||||||
|
SET hsadminng.currentSubject TO 'alex@example.com';
|
||||||
|
SELECT * from customer;
|
||||||
|
|
||||||
|
-- access control via view-rule and isPermissionGrantedToSubject - way too slow (35 s 580 ms for 1 million rows)
|
||||||
|
SET SESSION SESSION AUTHORIZATION DEFAULT;
|
||||||
|
DROP VIEW cust_view;
|
||||||
|
CREATE VIEW cust_view AS
|
||||||
|
SELECT * FROM rbactest.customer;
|
||||||
|
CREATE OR REPLACE RULE "_RETURN" AS
|
||||||
|
ON SELECT TO cust_view
|
||||||
|
DO INSTEAD
|
||||||
|
SELECT * FROM rbactest.customer WHERE rbac.isPermissionGrantedToSubject(rbac.findEffectivePermissionId('rbactest.customer', id, 'SELECT'), rbac.currentSubjectUuid());
|
||||||
|
SELECT * from cust_view LIMIT 10;
|
||||||
|
|
||||||
|
select rbac.queryAllPermissionsOfSubjectId(findRbacSubject('superuser-alex@hostsharing.net'));
|
||||||
|
|
||||||
|
-- access control via view-rule with join to recursive permissions - really fast (38ms for 1 million rows)
|
||||||
|
SET SESSION SESSION AUTHORIZATION DEFAULT;
|
||||||
|
ALTER TABLE rbactest.customer ENABLE ROW LEVEL SECURITY;
|
||||||
|
DROP VIEW IF EXISTS cust_view;
|
||||||
|
CREATE OR REPLACE VIEW cust_view AS
|
||||||
|
SELECT *
|
||||||
|
FROM rbactest.customer;
|
||||||
|
CREATE OR REPLACE RULE "_RETURN" AS
|
||||||
|
ON SELECT TO cust_view
|
||||||
|
DO INSTEAD
|
||||||
|
SELECT c.uuid, c.reference, c.prefix FROM rbactest.customer AS c
|
||||||
|
JOIN rbac.queryAllPermissionsOfSubjectId(rbac.currentSubjectUuid()) AS p
|
||||||
|
ON p.objectTable='rbactest.customer' AND p.objectUuid=c.uuid;
|
||||||
|
GRANT ALL PRIVILEGES ON cust_view TO restricted;
|
||||||
|
|
||||||
|
SET SESSION SESSION AUTHORIZATION restricted;
|
||||||
|
SET hsadminng.currentSubject TO 'alex@example.com';
|
||||||
|
SELECT * from cust_view;
|
||||||
|
|
||||||
|
|
||||||
|
-- access control via view with join to recursive permissions - really fast (38ms for 1 million rows)
|
||||||
|
SET SESSION SESSION AUTHORIZATION DEFAULT;
|
||||||
|
ALTER TABLE customer ENABLE ROW LEVEL SECURITY;
|
||||||
|
DROP VIEW IF EXISTS cust_view;
|
||||||
|
CREATE OR REPLACE VIEW cust_view AS
|
||||||
|
SELECT c.uuid, c.reference, c.prefix
|
||||||
|
FROM customer AS c
|
||||||
|
JOIN queryAllPermissionsOfSubjectId(rbac.currentSubjectUuid()) AS p
|
||||||
|
ON p.objectUuid=c.uuid;
|
||||||
|
GRANT ALL PRIVILEGES ON cust_view TO restricted;
|
||||||
|
|
||||||
|
SET SESSION SESSION AUTHORIZATION restricted;
|
||||||
|
-- SET hsadminng.currentSubject TO 'alex@example.com';
|
||||||
|
SET hsadminng.currentSubject TO 'superuser-alex@hostsharing.net';
|
||||||
|
-- SET hsadminng.currentSubject TO 'aaaaouq@example.com';
|
||||||
|
SELECT * from cust_view where reference=1144150;
|
||||||
|
|
||||||
|
select rr.uuid, rr.type from rbac.RbacGrants g
|
||||||
|
join rbac.RbacReference RR on g.ascendantUuid = RR.uuid
|
||||||
|
where g.descendantUuid in (
|
||||||
|
select uuid from rbac.queryAllPermissionsOfSubjectId(findRbacSubject('alex@example.com'))
|
||||||
|
where objectTable='rbactest.customer');
|
||||||
|
|
||||||
|
call rbac.grantRoleToUser(rbac.findRoleId('rbactest.customer#aaa:ADMIN'), rbac.findRbacSubject('aaaaouq@example.com'));
|
||||||
|
|
||||||
|
select rbac.queryAllPermissionsOfSubjectId(findRbacSubject('aaaaouq@example.com'));
|
||||||
|
|
2
sql/rbac.sql
Normal file
2
sql/rbac.sql
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
|
||||||
|
|
175
sql/recursive-cte-experiments-for-accessible-uuids.sql
Normal file
175
sql/recursive-cte-experiments-for-accessible-uuids.sql
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
-- just a permanent playground to explore optimization of the central recursive CTE query for RBAC
|
||||||
|
|
||||||
|
select * from hs_statistics_v;
|
||||||
|
|
||||||
|
-- ========================================================
|
||||||
|
|
||||||
|
-- This is the extracted recursive CTE query to determine the visible object UUIDs of a single table
|
||||||
|
-- (and optionally the hosting-asset-type) as a separate VIEW.
|
||||||
|
-- In the generated code this is part of the hs_hosting.asset_rv VIEW.
|
||||||
|
|
||||||
|
drop view if exists hs_hosting.asset_example_gv;
|
||||||
|
create view hs_hosting.asset_example_gv as
|
||||||
|
with recursive
|
||||||
|
recursive_grants as (
|
||||||
|
select distinct rbacgrants.descendantuuid,
|
||||||
|
rbacgrants.ascendantuuid,
|
||||||
|
1 as level,
|
||||||
|
true
|
||||||
|
from rbacgrants
|
||||||
|
where (rbacgrants.ascendantuuid = any (rbac.currentSubjectOrAssumedRolesUuids()))
|
||||||
|
and rbacgrants.assumed
|
||||||
|
union all
|
||||||
|
select distinct g.descendantuuid,
|
||||||
|
g.ascendantuuid,
|
||||||
|
grants.level + 1 as level,
|
||||||
|
assertTrue(grants.level < 22, 'too many grant-levels: ' || grants.level)
|
||||||
|
from rbacgrants g
|
||||||
|
join recursive_grants grants on grants.descendantuuid = g.ascendantuuid
|
||||||
|
where g.assumed
|
||||||
|
),
|
||||||
|
grant_count as (
|
||||||
|
select count(*) as grant_count from recursive_grants
|
||||||
|
),
|
||||||
|
count_check as (
|
||||||
|
select assertTrue((select grant_count from grant_count) < 600000,
|
||||||
|
'too many grants for current subjects: ' || (select grant_count from grant_count)) as valid
|
||||||
|
)
|
||||||
|
select distinct perm.objectuuid
|
||||||
|
from recursive_grants
|
||||||
|
join rbacpermission perm on recursive_grants.descendantuuid = perm.uuid
|
||||||
|
join rbacobject obj on obj.uuid = perm.objectuuid
|
||||||
|
join count_check cc on cc.valid
|
||||||
|
where obj.objecttable::text = 'hs_hosting.asset'::text
|
||||||
|
-- with/without this type condition
|
||||||
|
-- and obj.type = 'EMAIL_ADDRESS'::hshostingassettype
|
||||||
|
and obj.type = 'EMAIL_ADDRESS'::hshostingassettype
|
||||||
|
;
|
||||||
|
|
||||||
|
-- -----------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- A query just on the above view, only determining visible objects, no JOIN with business data:
|
||||||
|
|
||||||
|
rollback transaction;
|
||||||
|
begin transaction;
|
||||||
|
CALL defineContext('performance testing', null, 'superuser-alex@hostsharing.net',
|
||||||
|
'hs_booking.project#D-1000000-hshdefaultproject:ADMIN');
|
||||||
|
-- 'hs_booking.project#D-1000300-mihdefaultproject:ADMIN');
|
||||||
|
SET TRANSACTION READ ONLY;
|
||||||
|
EXPLAIN ANALYZE select * from hs_hosting.asset_example_gv;
|
||||||
|
end transaction ;
|
||||||
|
|
||||||
|
-- ========================================================
|
||||||
|
|
||||||
|
-- An example for a restricted view (_rv) similar to the one generated by our RBAC system,
|
||||||
|
-- but using the above separate VIEW to determine the visible objects.
|
||||||
|
|
||||||
|
drop view if exists hs_hosting.asset_example_rv;
|
||||||
|
create view hs_hosting.asset_example_rv as
|
||||||
|
with accessible_hs_hosting.asset_uuids as (
|
||||||
|
select * from hs_hosting.asset_example_gv
|
||||||
|
)
|
||||||
|
select target.*
|
||||||
|
from hs_hosting.asset target
|
||||||
|
where (target.uuid in (select accessible_hs_hosting.asset_uuids.objectuuid
|
||||||
|
from accessible_hs_hosting.asset_uuids));
|
||||||
|
|
||||||
|
-- -------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- performing several queries on the above view to determine average performance:
|
||||||
|
|
||||||
|
rollback transaction;
|
||||||
|
DO language plpgsql $$
|
||||||
|
DECLARE
|
||||||
|
start_time timestamp;
|
||||||
|
end_time timestamp;
|
||||||
|
total_time interval;
|
||||||
|
letter char(1);
|
||||||
|
BEGIN
|
||||||
|
start_time := clock_timestamp();
|
||||||
|
|
||||||
|
CALL defineContext('performance testing', null, 'superuser-alex@hostsharing.net',
|
||||||
|
'hs_booking.project#D-1000000-hshdefaultproject:ADMIN');
|
||||||
|
-- 'hs_booking.project#D-1000300-mihdefaultproject:ADMIN');
|
||||||
|
SET TRANSACTION READ ONLY;
|
||||||
|
|
||||||
|
FOR i IN 0..25 LOOP
|
||||||
|
letter := chr(i+ascii('a'));
|
||||||
|
PERFORM count(*) from (
|
||||||
|
|
||||||
|
-- An example for a business query based on the view:
|
||||||
|
select type, uuid, identifier, caption
|
||||||
|
from hs_hosting.asset_example_rv
|
||||||
|
where type = 'EMAIL_ADDRESS'
|
||||||
|
and identifier like letter || '%'
|
||||||
|
-- end of the business query example.
|
||||||
|
|
||||||
|
) AS timed;
|
||||||
|
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
end_time := clock_timestamp();
|
||||||
|
total_time := end_time - start_time;
|
||||||
|
|
||||||
|
RAISE NOTICE 'average execution time: %', total_time/26;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- average seconds per recursive CTE select as role 'hs_hosting.asset:<DEBITOR>defaultproject:ADMIN'
|
||||||
|
-- joined with business query for all 'EMAIL_ADDRESSES':
|
||||||
|
-- D-1000000-hsh D-1000300-mih
|
||||||
|
-- - without type comparison in rbacobject: ~3.30 - ~3.49 ~0.23
|
||||||
|
-- - with type comparison in rbacobject: ~2.99 - ~3.08 ~0.21
|
||||||
|
|
||||||
|
-- -------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- and a single query, so EXPLAIN can be used
|
||||||
|
|
||||||
|
rollback transaction;
|
||||||
|
begin transaction;
|
||||||
|
CALL defineContext('performance testing', null, 'superuser-alex@hostsharing.net',
|
||||||
|
'hs_booking.project#D-1000000-hshdefaultproject:ADMIN');
|
||||||
|
-- 'hs_booking.project#D-1000300-mihdefaultproject:ADMIN');
|
||||||
|
SET TRANSACTION READ ONLY;
|
||||||
|
|
||||||
|
EXPLAIN SELECT * from (
|
||||||
|
|
||||||
|
-- An example for a business query based on the view:
|
||||||
|
select type, uuid, identifier, caption
|
||||||
|
from hs_hosting.asset_example_rv
|
||||||
|
where type = 'EMAIL_ADDRESS'
|
||||||
|
-- and identifier like 'b%'
|
||||||
|
-- end of the business query example.
|
||||||
|
|
||||||
|
) ha;
|
||||||
|
|
||||||
|
end transaction;
|
||||||
|
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
-- extending the rbacobject table:
|
||||||
|
|
||||||
|
alter table rbacobject
|
||||||
|
-- just for performance testing, we would need a joined enum or a varchar(16) which would make it slow
|
||||||
|
add column type hs_hosting.AssetType;
|
||||||
|
|
||||||
|
-- and fill the type column with hs_hosting.asset types:
|
||||||
|
|
||||||
|
rollback transaction;
|
||||||
|
begin transaction;
|
||||||
|
call defineContext('setting rbacobject.type from hs_hosting.asset.type', null, 'superuser-alex@hostsharing.net');
|
||||||
|
|
||||||
|
UPDATE rbacobject
|
||||||
|
SET type = hs.type
|
||||||
|
FROM hs_hosting.asset hs
|
||||||
|
WHERE rbacobject.uuid = hs.uuid;
|
||||||
|
|
||||||
|
end transaction;
|
||||||
|
|
||||||
|
-- check the result:
|
||||||
|
|
||||||
|
select
|
||||||
|
(select count(*) as "total" from rbacobject),
|
||||||
|
(select count(*) as "not null" from rbacobject where type is not null),
|
||||||
|
(select count(*) as "null" from rbacobject where type is null);
|
||||||
|
|
@ -1,14 +0,0 @@
|
|||||||
# https://docs.docker.com/engine/reference/builder/#dockerignore-file
|
|
||||||
classes/
|
|
||||||
generated-sources/
|
|
||||||
generated-test-sources/
|
|
||||||
h2db/
|
|
||||||
maven-archiver/
|
|
||||||
maven-status/
|
|
||||||
reports/
|
|
||||||
surefire-reports/
|
|
||||||
test-classes/
|
|
||||||
test-results/
|
|
||||||
www/
|
|
||||||
!*.jar
|
|
||||||
!*.war
|
|
@ -1,20 +0,0 @@
|
|||||||
FROM openjdk:8-jre-alpine
|
|
||||||
|
|
||||||
ENV SPRING_OUTPUT_ANSI_ENABLED=ALWAYS \
|
|
||||||
JHIPSTER_SLEEP=0 \
|
|
||||||
JAVA_OPTS=""
|
|
||||||
|
|
||||||
# Add a jhipster user to run our application so that it doesn't need to run as root
|
|
||||||
RUN adduser -D -s /bin/sh jhipster
|
|
||||||
WORKDIR /home/jhipster
|
|
||||||
|
|
||||||
ADD entrypoint.sh entrypoint.sh
|
|
||||||
RUN chmod 755 entrypoint.sh && chown jhipster:jhipster entrypoint.sh
|
|
||||||
USER jhipster
|
|
||||||
|
|
||||||
ENTRYPOINT ["./entrypoint.sh"]
|
|
||||||
|
|
||||||
EXPOSE 8080
|
|
||||||
|
|
||||||
ADD *.war app.war
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
|||||||
version: '2'
|
|
||||||
services:
|
|
||||||
hsadminng-app:
|
|
||||||
image: hsadminng
|
|
||||||
environment:
|
|
||||||
- _JAVA_OPTIONS=-Xmx512m -Xms256m
|
|
||||||
- SPRING_PROFILES_ACTIVE=prod,swagger
|
|
||||||
- SPRING_DATASOURCE_URL=jdbc:postgresql://hsadminng-postgresql:5432/hsadminNg
|
|
||||||
- JHIPSTER_SLEEP=10 # gives time for the database to boot before the application
|
|
||||||
ports:
|
|
||||||
- 8080:8080
|
|
||||||
hsadminng-postgresql:
|
|
||||||
extends:
|
|
||||||
file: postgresql.yml
|
|
||||||
service: hsadminng-postgresql
|
|
@ -1,4 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
echo "The application will start in ${JHIPSTER_SLEEP}s..." && sleep ${JHIPSTER_SLEEP}
|
|
||||||
exec java ${JAVA_OPTS} -Djava.security.egd=file:/dev/./urandom -jar "${HOME}/app.war" "$@"
|
|
@ -1,15 +0,0 @@
|
|||||||
version: '2'
|
|
||||||
services:
|
|
||||||
jenkins:
|
|
||||||
image: jenkins:latest
|
|
||||||
ports:
|
|
||||||
- 49001:8080
|
|
||||||
- 50000:50000
|
|
||||||
# uncomment for docker in docker
|
|
||||||
#privileged: true
|
|
||||||
#volumes:
|
|
||||||
# enable persistent volume (warning: make sure that the local jenkins_home folder is created)
|
|
||||||
#- ~/volumes/jenkins_home:/var/jenkins_home
|
|
||||||
# mount docker sock and binary for docker in docker (only works on linux)
|
|
||||||
#- /var/run/docker.sock:/var/run/docker.sock
|
|
||||||
#- /usr/bin/docker:/usr/bin/docker
|
|
@ -1,11 +0,0 @@
|
|||||||
version: '2'
|
|
||||||
services:
|
|
||||||
hsadminng-postgresql:
|
|
||||||
image: postgres:10.4
|
|
||||||
# volumes:
|
|
||||||
# - ~/volumes/jhipster/hsadminNg/postgresql/:/var/lib/postgresql/data/
|
|
||||||
environment:
|
|
||||||
- POSTGRES_USER=hsadminNg
|
|
||||||
- POSTGRES_PASSWORD=
|
|
||||||
ports:
|
|
||||||
- 5432:5432
|
|
@ -1,7 +0,0 @@
|
|||||||
version: '2'
|
|
||||||
services:
|
|
||||||
hsadminng-sonar:
|
|
||||||
image: sonarqube:7.1
|
|
||||||
ports:
|
|
||||||
- 9001:9000
|
|
||||||
- 9092:9092
|
|
@ -1,6 +0,0 @@
|
|||||||
version: '2'
|
|
||||||
services:
|
|
||||||
swagger-editor:
|
|
||||||
image: swaggerapi/swagger-editor:latest
|
|
||||||
ports:
|
|
||||||
- 7742:8080
|
|
@ -0,0 +1,12 @@
|
|||||||
|
package net.hostsharing.hsadminng;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
|
@SpringBootApplication
|
||||||
|
public class HsadminNgApplication {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(HsadminNgApplication.class, args);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
package net.hostsharing.hsadminng.config;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonParser;
|
||||||
|
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||||
|
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||||
|
import org.openapitools.jackson.nullable.JsonNullableModule;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.annotation.Primary;
|
||||||
|
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class JsonObjectMapperConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@Primary
|
||||||
|
public Jackson2ObjectMapperBuilder customObjectMapper() {
|
||||||
|
return new Jackson2ObjectMapperBuilder()
|
||||||
|
.modules(new JsonNullableModule(), new JavaTimeModule())
|
||||||
|
.featuresToEnable(JsonParser.Feature.ALLOW_COMMENTS, JsonParser.Feature.ALLOW_COMMENTS)
|
||||||
|
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
package net.hostsharing.hsadminng.config;
|
||||||
|
|
||||||
|
import org.hibernate.dialect.PostgreSQLDialect;
|
||||||
|
|
||||||
|
import static org.hibernate.dialect.DatabaseVersion.make;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused") // configured in application.yml
|
||||||
|
public class PostgresCustomDialect extends PostgreSQLDialect {
|
||||||
|
|
||||||
|
public PostgresCustomDialect() {
|
||||||
|
super(make(15, 5));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
161
src/main/java/net/hostsharing/hsadminng/context/Context.java
Normal file
161
src/main/java/net/hostsharing/hsadminng/context/Context.java
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
package net.hostsharing.hsadminng.context;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.SneakyThrows;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.web.context.request.RequestContextHolder;
|
||||||
|
|
||||||
|
import jakarta.persistence.EntityManager;
|
||||||
|
import jakarta.persistence.PersistenceContext;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import static java.util.function.Predicate.not;
|
||||||
|
import static org.springframework.transaction.annotation.Propagation.MANDATORY;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class Context {
|
||||||
|
|
||||||
|
private static final Set<String> HEADERS_TO_IGNORE = Set.of(
|
||||||
|
"accept-encoding",
|
||||||
|
"connection",
|
||||||
|
"content-length",
|
||||||
|
"host",
|
||||||
|
"user-agent");
|
||||||
|
|
||||||
|
@PersistenceContext
|
||||||
|
private EntityManager em;
|
||||||
|
|
||||||
|
@Autowired(required = false)
|
||||||
|
private HttpServletRequest request;
|
||||||
|
|
||||||
|
@Transactional(propagation = MANDATORY)
|
||||||
|
public void define(final String currentSubject) {
|
||||||
|
define(currentSubject, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(propagation = MANDATORY)
|
||||||
|
public void define(final String currentSubject, final String assumedRoles) {
|
||||||
|
define(toTask(request), toCurl(request), currentSubject, assumedRoles);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(propagation = MANDATORY)
|
||||||
|
public void define(
|
||||||
|
final String currentTask,
|
||||||
|
final String currentRequest,
|
||||||
|
final String currentSubject,
|
||||||
|
final String assumedRoles) {
|
||||||
|
final var query = em.createNativeQuery("""
|
||||||
|
call base.defineContext(
|
||||||
|
cast(:currentTask as varchar(127)),
|
||||||
|
cast(:currentRequest as text),
|
||||||
|
cast(:currentSubject as varchar(63)),
|
||||||
|
cast(:assumedRoles as varchar(1023)));
|
||||||
|
""");
|
||||||
|
query.setParameter("currentTask", shortenToMaxLength(currentTask, 127));
|
||||||
|
query.setParameter("currentRequest", currentRequest);
|
||||||
|
query.setParameter("currentSubject", currentSubject);
|
||||||
|
query.setParameter("assumedRoles", assumedRoles != null ? assumedRoles : "");
|
||||||
|
query.executeUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String fetchCurrentTask() {
|
||||||
|
return (String) em.createNativeQuery("select current_setting('hsadminng.currentTask');").getSingleResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String fetchCurrentSubject() {
|
||||||
|
return String.valueOf(em.createNativeQuery("select base.currentSubject()").getSingleResult());
|
||||||
|
}
|
||||||
|
|
||||||
|
public UUID fetchCurrentSubjectUuid() {
|
||||||
|
return (UUID) em.createNativeQuery("select rbac.currentSubjectUuid()", UUID.class).getSingleResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String[] fetchAssumedRoles() {
|
||||||
|
return (String[]) em.createNativeQuery("select base.assumedRoles() as roles", String[].class).getSingleResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
public UUID[] fetchCurrentSubjectOrAssumedRolesUuids() {
|
||||||
|
return (UUID[]) em.createNativeQuery("select rbac.currentSubjectOrAssumedRolesUuids() as uuids", UUID[].class).getSingleResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getCallerMethodNameFromStackFrame(final int skipFrames) {
|
||||||
|
final Optional<StackWalker.StackFrame> caller =
|
||||||
|
StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
|
||||||
|
.walk(frames -> frames
|
||||||
|
.skip(skipFrames)
|
||||||
|
.filter(c -> c.getDeclaringClass() != Context.class)
|
||||||
|
.filter(c -> c.getDeclaringClass()
|
||||||
|
.getPackageName()
|
||||||
|
.startsWith("net.hostsharing.hsadminng"))
|
||||||
|
.filter(c -> !c.getDeclaringClass().getName().contains("$$SpringCGLIB$$"))
|
||||||
|
.findFirst());
|
||||||
|
return caller.map(
|
||||||
|
c -> c.getDeclaringClass().getSimpleName() + "." + c.getMethodName())
|
||||||
|
.orElse("unknown");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String toTask(final HttpServletRequest request) {
|
||||||
|
if (isRequestScopeAvailable()) {
|
||||||
|
return request.getMethod() + " " + request.getRequestURI();
|
||||||
|
} else {
|
||||||
|
return getCallerMethodNameFromStackFrame(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SneakyThrows
|
||||||
|
private String toCurl(final HttpServletRequest request) {
|
||||||
|
if (!isRequestScopeAvailable()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var curlCommand = "curl -0 -v";
|
||||||
|
|
||||||
|
// append method
|
||||||
|
curlCommand += " -X " + request.getMethod();
|
||||||
|
|
||||||
|
// append request url
|
||||||
|
curlCommand += " " + request.getRequestURI();
|
||||||
|
|
||||||
|
// append headers
|
||||||
|
final var headers = Collections.list(request.getHeaderNames()).stream()
|
||||||
|
.filter(not(HEADERS_TO_IGNORE::contains))
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
for (String headerName : headers) {
|
||||||
|
final var headerValue = request.getHeader(headerName);
|
||||||
|
curlCommand += " \\" + System.lineSeparator() + String.format("-H '%s:%s'", headerName, headerValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// body
|
||||||
|
final String body = request.getReader().lines().collect(Collectors.joining(System.lineSeparator()));
|
||||||
|
if (!StringUtils.isEmpty(body)) {
|
||||||
|
curlCommand += " \\" + System.lineSeparator() + "--data-binary @- ";
|
||||||
|
curlCommand +=
|
||||||
|
"<< EOF" + System.lineSeparator() + System.lineSeparator() + body + System.lineSeparator() + "EOF";
|
||||||
|
}
|
||||||
|
|
||||||
|
return curlCommand;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isRequestScopeAvailable() {
|
||||||
|
return RequestContextHolder.getRequestAttributes() != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String shortenToMaxLength(final String raw, final int maxLength) {
|
||||||
|
if (raw == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
if (raw.length() <= maxLength) {
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
return raw.substring(0, maxLength - 3) + "...";
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
package net.hostsharing.hsadminng.context;
|
||||||
|
|
||||||
|
import jakarta.servlet.ReadListener;
|
||||||
|
import jakarta.servlet.ServletInputStream;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
public class HttpServletRequestBodyCache extends ServletInputStream {
|
||||||
|
|
||||||
|
private InputStream inputStream;
|
||||||
|
|
||||||
|
public HttpServletRequestBodyCache(byte[] body) {
|
||||||
|
this.inputStream = new ByteArrayInputStream(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read() throws IOException {
|
||||||
|
return inputStream.read();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int available() throws IOException {
|
||||||
|
return inputStream.available();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isFinished() {
|
||||||
|
try {
|
||||||
|
return available() == 0;
|
||||||
|
} catch (IOException e) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isReady() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setReadListener(final ReadListener listener) {
|
||||||
|
throw new RuntimeException("Not implemented");
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
package net.hostsharing.hsadminng.context;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
|
||||||
|
import jakarta.servlet.FilterChain;
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class HttpServletRequestBodyCachingFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doFilterInternal(
|
||||||
|
final HttpServletRequest request,
|
||||||
|
final HttpServletResponse response,
|
||||||
|
final FilterChain filterChain)
|
||||||
|
throws ServletException, IOException {
|
||||||
|
filterChain.doFilter(new HttpServletRequestWithCachedBody(request), response);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
package net.hostsharing.hsadminng.context;
|
||||||
|
|
||||||
|
import org.springframework.util.StreamUtils;
|
||||||
|
|
||||||
|
import jakarta.servlet.ServletInputStream;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletRequestWrapper;
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
|
||||||
|
public class HttpServletRequestWithCachedBody extends HttpServletRequestWrapper {
|
||||||
|
|
||||||
|
private byte[] cachedBody;
|
||||||
|
|
||||||
|
public HttpServletRequestWithCachedBody(HttpServletRequest request) throws IOException {
|
||||||
|
super(request);
|
||||||
|
final var requestInputStream = request.getInputStream();
|
||||||
|
this.cachedBody = StreamUtils.copyToByteArray(requestInputStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ServletInputStream getInputStream() throws IOException {
|
||||||
|
return new HttpServletRequestBodyCache(this.cachedBody);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BufferedReader getReader() throws IOException {
|
||||||
|
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.cachedBody);
|
||||||
|
return new BufferedReader(new InputStreamReader(byteArrayInputStream));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
package net.hostsharing.hsadminng.errors;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import lombok.Getter;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.context.request.WebRequest;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public class CustomErrorResponse {
|
||||||
|
|
||||||
|
static ResponseEntity<CustomErrorResponse> errorResponse(
|
||||||
|
final WebRequest request,
|
||||||
|
final HttpStatus httpStatus,
|
||||||
|
final String message) {
|
||||||
|
return new ResponseEntity<>(
|
||||||
|
new CustomErrorResponse(request.getContextPath(), httpStatus, message), httpStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
static String firstMessageLine(final Throwable exception) {
|
||||||
|
if (exception.getMessage() != null) {
|
||||||
|
return line(exception.getMessage(), 0);
|
||||||
|
}
|
||||||
|
return "ERROR: [500] " + exception.getClass().getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
static String line(final String message, final int lineNo) {
|
||||||
|
return message.split("\\r|\\n|\\r\\n", 0)[lineNo];
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd hh:mm:ss")
|
||||||
|
private final LocalDateTime timestamp;
|
||||||
|
|
||||||
|
private final String path;
|
||||||
|
|
||||||
|
private final int statusCode;
|
||||||
|
|
||||||
|
private final String statusPhrase;
|
||||||
|
|
||||||
|
private final String message;
|
||||||
|
|
||||||
|
CustomErrorResponse(final String path, final HttpStatus status, final String message) {
|
||||||
|
this.timestamp = LocalDateTime.now();
|
||||||
|
this.path = path;
|
||||||
|
this.statusCode = status.value();
|
||||||
|
this.statusPhrase = status.getReasonPhrase();
|
||||||
|
this.message = message.startsWith("ERROR: [") ? message : "ERROR: [" + statusCode + "] " + message;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
package net.hostsharing.hsadminng.errors;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
@Target(ElementType.TYPE)
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
public @interface DisplayAs {
|
||||||
|
|
||||||
|
class DisplayName {
|
||||||
|
|
||||||
|
public static String of(final Class<?> clazz) {
|
||||||
|
final var displayNameAnnot = getDisplayNameAnnotation(clazz);
|
||||||
|
return displayNameAnnot != null ? displayNameAnnot.value() : clazz.getSimpleName();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String of(@NotNull final Object instance) {
|
||||||
|
return of(instance.getClass());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DisplayAs getDisplayNameAnnotation(final Class<?> clazz) {
|
||||||
|
if (clazz == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final var annot = clazz.getAnnotation(DisplayAs.class);
|
||||||
|
return annot != null ? annot : getDisplayNameAnnotation(clazz.getSuperclass());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String value() default "";
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
package net.hostsharing.hsadminng.errors;
|
||||||
|
|
||||||
|
import jakarta.validation.ValidationException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static java.lang.String.join;
|
||||||
|
|
||||||
|
public class MultiValidationException extends ValidationException {
|
||||||
|
|
||||||
|
private MultiValidationException(final List<String> violations) {
|
||||||
|
super(
|
||||||
|
violations.size() > 1
|
||||||
|
? "[\n" + join(",\n", violations) + "\n]"
|
||||||
|
: "[" + join(",\n", violations) + "]"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void throwIfNotEmpty(final List<String> violations) {
|
||||||
|
if (!violations.isEmpty()) {
|
||||||
|
throw new MultiValidationException(violations);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
package net.hostsharing.hsadminng.errors;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class ReferenceNotFoundException extends RuntimeException {
|
||||||
|
|
||||||
|
private final Class<?> entityClass;
|
||||||
|
private final UUID uuid;
|
||||||
|
public <E> ReferenceNotFoundException(final Class<E> entityClass, final UUID uuid, final Throwable exc) {
|
||||||
|
super(exc);
|
||||||
|
this.entityClass = entityClass;
|
||||||
|
this.uuid = uuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getMessage() {
|
||||||
|
return "Cannot resolve " + entityClass.getSimpleName() +" with uuid " + uuid;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,188 @@
|
|||||||
|
package net.hostsharing.hsadminng.errors;
|
||||||
|
|
||||||
|
import org.iban4j.Iban4jException;
|
||||||
|
import org.springframework.core.NestedExceptionUtils;
|
||||||
|
import org.springframework.dao.DataIntegrityViolationException;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.HttpStatusCode;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||||
|
import org.springframework.lang.Nullable;
|
||||||
|
import org.springframework.orm.jpa.JpaObjectRetrievalFailureException;
|
||||||
|
import org.springframework.orm.jpa.JpaSystemException;
|
||||||
|
import org.springframework.validation.FieldError;
|
||||||
|
import org.springframework.validation.method.ParameterValidationResult;
|
||||||
|
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||||
|
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
|
import org.springframework.web.context.request.WebRequest;
|
||||||
|
import org.springframework.web.method.annotation.HandlerMethodValidationException;
|
||||||
|
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
|
||||||
|
|
||||||
|
import jakarta.persistence.EntityNotFoundException;
|
||||||
|
import jakarta.validation.ValidationException;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import static net.hostsharing.hsadminng.errors.CustomErrorResponse.*;
|
||||||
|
|
||||||
|
@ControllerAdvice
|
||||||
|
public class RestResponseEntityExceptionHandler
|
||||||
|
extends ResponseEntityExceptionHandler {
|
||||||
|
|
||||||
|
@ExceptionHandler(DataIntegrityViolationException.class)
|
||||||
|
protected ResponseEntity<CustomErrorResponse> handleConflict(
|
||||||
|
final RuntimeException exc, final WebRequest request) {
|
||||||
|
|
||||||
|
final var rawMessage = NestedExceptionUtils.getMostSpecificCause(exc).getMessage();
|
||||||
|
var message = line(rawMessage, 0);
|
||||||
|
if (message.contains("violates foreign key constraint")) {
|
||||||
|
return errorResponse(request, HttpStatus.BAD_REQUEST, line(rawMessage, 1).replaceAll(" *Detail: *", ""));
|
||||||
|
}
|
||||||
|
return errorResponse(request, HttpStatus.CONFLICT, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(JpaSystemException.class)
|
||||||
|
protected ResponseEntity<CustomErrorResponse> handleJpaExceptions(
|
||||||
|
final RuntimeException exc, final WebRequest request) {
|
||||||
|
final var message = line(NestedExceptionUtils.getMostSpecificCause(exc).getMessage(), 0);
|
||||||
|
return errorResponse(request, httpStatus(exc, message).orElse(HttpStatus.INTERNAL_SERVER_ERROR), message);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(NoSuchElementException.class)
|
||||||
|
protected ResponseEntity<CustomErrorResponse> handleNoSuchElementException(
|
||||||
|
final RuntimeException exc, final WebRequest request) {
|
||||||
|
final var message = line(NestedExceptionUtils.getMostSpecificCause(exc).getMessage(), 0);
|
||||||
|
return errorResponse(request, HttpStatus.NOT_FOUND, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(ReferenceNotFoundException.class)
|
||||||
|
protected ResponseEntity<CustomErrorResponse> handleReferenceNotFoundException(
|
||||||
|
final ReferenceNotFoundException exc, final WebRequest request) {
|
||||||
|
return errorResponse(request, HttpStatus.BAD_REQUEST, exc.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler({ JpaObjectRetrievalFailureException.class, EntityNotFoundException.class })
|
||||||
|
protected ResponseEntity<CustomErrorResponse> handleJpaObjectRetrievalFailureException(
|
||||||
|
final RuntimeException exc, final WebRequest request) {
|
||||||
|
final var message =
|
||||||
|
userReadableEntityClassName(
|
||||||
|
line(NestedExceptionUtils.getMostSpecificCause(exc).getMessage(), 0));
|
||||||
|
return errorResponse(request, HttpStatus.BAD_REQUEST, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler({ Iban4jException.class, ValidationException.class })
|
||||||
|
protected ResponseEntity<CustomErrorResponse> handleValidationExceptions(
|
||||||
|
final Throwable exc, final WebRequest request) {
|
||||||
|
final String fullMessage = NestedExceptionUtils.getMostSpecificCause(exc).getMessage();
|
||||||
|
final var message = exc instanceof MultiValidationException ? fullMessage : line(fullMessage, 0);
|
||||||
|
return errorResponse(request, HttpStatus.BAD_REQUEST, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(Throwable.class)
|
||||||
|
protected ResponseEntity<CustomErrorResponse> handleOtherExceptions(
|
||||||
|
final Throwable exc, final WebRequest request) {
|
||||||
|
final var causingException = NestedExceptionUtils.getMostSpecificCause(exc);
|
||||||
|
final var message = firstMessageLine(causingException);
|
||||||
|
return errorResponse(request, httpStatus(causingException, message).orElse(HttpStatus.INTERNAL_SERVER_ERROR), message);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings("unchecked,rawtypes")
|
||||||
|
protected ResponseEntity handleExceptionInternal(
|
||||||
|
Exception exc, @Nullable Object body, HttpHeaders headers, HttpStatusCode statusCode, WebRequest request) {
|
||||||
|
|
||||||
|
final var response = super.handleExceptionInternal(exc, body, headers, statusCode, request);
|
||||||
|
return errorResponse(request, HttpStatus.valueOf(statusCode.value()),
|
||||||
|
Optional.ofNullable(response.getBody()).map(Object::toString).orElse(firstMessageLine(exc)));
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings("unchecked,rawtypes")
|
||||||
|
protected ResponseEntity handleHttpMessageNotReadable(
|
||||||
|
HttpMessageNotReadableException exc, HttpHeaders headers, HttpStatusCode status, WebRequest request) {
|
||||||
|
final var message = line(exc.getMessage(), 0);
|
||||||
|
return errorResponse(request, HttpStatus.BAD_REQUEST, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings("unchecked,rawtypes")
|
||||||
|
protected ResponseEntity handleMethodArgumentNotValid(
|
||||||
|
MethodArgumentNotValidException exc,
|
||||||
|
HttpHeaders headers,
|
||||||
|
HttpStatusCode statusCode,
|
||||||
|
WebRequest request) {
|
||||||
|
final var errorList = exc
|
||||||
|
.getBindingResult()
|
||||||
|
.getFieldErrors()
|
||||||
|
.stream()
|
||||||
|
.map(fieldError -> fieldError.getField() + " " + fieldError.getDefaultMessage() + " but is \""
|
||||||
|
+ fieldError.getRejectedValue() + "\"")
|
||||||
|
.toList();
|
||||||
|
return errorResponse(request, HttpStatus.BAD_REQUEST, errorList.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked,rawtypes")
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected ResponseEntity handleHandlerMethodValidationException(
|
||||||
|
final HandlerMethodValidationException exc,
|
||||||
|
final HttpHeaders headers,
|
||||||
|
final HttpStatusCode status,
|
||||||
|
final WebRequest request) {
|
||||||
|
final var errorList = exc
|
||||||
|
.getAllValidationResults()
|
||||||
|
.stream()
|
||||||
|
.map(ParameterValidationResult::getResolvableErrors)
|
||||||
|
.flatMap(Collection::stream)
|
||||||
|
.filter(FieldError.class::isInstance)
|
||||||
|
.map(FieldError.class::cast)
|
||||||
|
.map(fieldError -> fieldError.getField() + " " + fieldError.getDefaultMessage() + " but is \""
|
||||||
|
+ fieldError.getRejectedValue() + "\"")
|
||||||
|
.toList();
|
||||||
|
return errorResponse(request, HttpStatus.BAD_REQUEST, errorList.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private String userReadableEntityClassName(final String exceptionMessage) {
|
||||||
|
final var regex = "(net.hostsharing.hsadminng.[a-z0-9_.]*.[A-Za-z0-9_$]*Entity) ";
|
||||||
|
final var pattern = Pattern.compile(regex);
|
||||||
|
final var matcher = pattern.matcher(exceptionMessage);
|
||||||
|
if (matcher.find()) {
|
||||||
|
final var entityName = matcher.group(1);
|
||||||
|
final var entityClass = resolveClass(entityName);
|
||||||
|
if (entityClass.isPresent()) {
|
||||||
|
return (entityClass.get().isAnnotationPresent(DisplayAs.class)
|
||||||
|
? exceptionMessage.replace(entityName, entityClass.get().getAnnotation(DisplayAs.class).value())
|
||||||
|
: exceptionMessage.replace(entityName, entityClass.get().getSimpleName()))
|
||||||
|
.replace(" with id ", " with uuid ");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return exceptionMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Optional<Class<?>> resolveClass(final String entityName) {
|
||||||
|
try {
|
||||||
|
return Optional.of(ClassLoader.getSystemClassLoader().loadClass(entityName));
|
||||||
|
} catch (ClassNotFoundException e) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<HttpStatus> httpStatus(final Throwable causingException, final String message) {
|
||||||
|
if ( EntityNotFoundException.class.isInstance(causingException) ) {
|
||||||
|
return Optional.of(HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
if (message.startsWith("ERROR: [")) {
|
||||||
|
for (HttpStatus status : HttpStatus.values()) {
|
||||||
|
if (message.startsWith("ERROR: [" + status.value() + "]")) {
|
||||||
|
return Optional.of(status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Optional.of(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
@NonNullApi
|
||||||
|
@NonNullFields
|
||||||
|
package net.hostsharing.hsadminng.errors;
|
||||||
|
|
||||||
|
import org.springframework.lang.NonNullApi;
|
||||||
|
import org.springframework.lang.NonNullFields;
|
130
src/main/java/net/hostsharing/hsadminng/hash/HashGenerator.java
Normal file
130
src/main/java/net/hostsharing/hsadminng/hash/HashGenerator.java
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
package net.hostsharing.hsadminng.hash;
|
||||||
|
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.PriorityQueue;
|
||||||
|
import java.util.Queue;
|
||||||
|
import java.util.function.BiFunction;
|
||||||
|
import java.util.random.RandomGenerator;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Usage-example to generate hash:
|
||||||
|
* HashGenerator.using(LINUX_SHA512).withRandomSalt().hash("plaintext password");
|
||||||
|
*
|
||||||
|
* Usage-example to verify hash:
|
||||||
|
* HashGenerator.fromHash("hashed password).verify("plaintext password");
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public final class HashGenerator {
|
||||||
|
|
||||||
|
private static final RandomGenerator random = new SecureRandom();
|
||||||
|
private static final Queue<String> predefinedSalts = new PriorityQueue<>();
|
||||||
|
|
||||||
|
public static final int RANDOM_SALT_LENGTH = 16;
|
||||||
|
private static final String RANDOM_SALT_CHARACTERS =
|
||||||
|
"abcdefghijklmnopqrstuvwxyz" +
|
||||||
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
|
||||||
|
"0123456789/.";
|
||||||
|
private static boolean couldBeHashEnabled; // TODO.legacy: remove after legacy data is migrated
|
||||||
|
|
||||||
|
public enum Algorithm {
|
||||||
|
LINUX_SHA512(LinuxEtcShadowHashGenerator::hash, "6"),
|
||||||
|
LINUX_YESCRYPT(LinuxEtcShadowHashGenerator::hash, "y", "j9T$") {
|
||||||
|
@Override
|
||||||
|
String enrichedSalt(final String salt) {
|
||||||
|
return prefix + "$" + (salt.startsWith(optionalParam) ? salt : optionalParam + salt);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
MYSQL_NATIVE(MySQLNativePasswordHashGenerator::hash, "*"),
|
||||||
|
SCRAM_SHA256(PostgreSQLScramSHA256::hash, "SCRAM-SHA-256");
|
||||||
|
|
||||||
|
final BiFunction<HashGenerator, String, String> implementation;
|
||||||
|
final String prefix;
|
||||||
|
final String optionalParam;
|
||||||
|
|
||||||
|
Algorithm(BiFunction<HashGenerator, String, String> implementation, final String prefix, final String optionalParam) {
|
||||||
|
this.implementation = implementation;
|
||||||
|
this.prefix = prefix;
|
||||||
|
this.optionalParam = optionalParam;
|
||||||
|
}
|
||||||
|
|
||||||
|
Algorithm(BiFunction<HashGenerator, String, String> implementation, final String prefix) {
|
||||||
|
this(implementation, prefix, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Algorithm byPrefix(final String prefix) {
|
||||||
|
return Arrays.stream(Algorithm.values()).filter(a -> a.prefix.equals(prefix)).findAny()
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("unknown hash algorithm: '" + prefix + "'"));
|
||||||
|
}
|
||||||
|
|
||||||
|
String enrichedSalt(final String salt) {
|
||||||
|
return prefix + "$" + salt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Algorithm algorithm;
|
||||||
|
private String salt;
|
||||||
|
|
||||||
|
public static HashGenerator using(final Algorithm algorithm) {
|
||||||
|
return new HashGenerator(algorithm);
|
||||||
|
}
|
||||||
|
|
||||||
|
private HashGenerator(final Algorithm algorithm) {
|
||||||
|
this.algorithm = algorithm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void enableCouldBeHash(final boolean enable) {
|
||||||
|
couldBeHashEnabled = enable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean couldBeHash(final String value) {
|
||||||
|
return couldBeHashEnabled && value.startsWith(algorithm.prefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String hash(final String plaintextPassword) {
|
||||||
|
if (plaintextPassword == null) {
|
||||||
|
throw new IllegalStateException("no password given");
|
||||||
|
}
|
||||||
|
|
||||||
|
final var hash = algorithm.implementation.apply(this, plaintextPassword);
|
||||||
|
if (hash.length() < plaintextPassword.length()) {
|
||||||
|
throw new AssertionError("generated hash too short: " + hash);
|
||||||
|
}
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String hashIfNotYetHashed(final String plaintextPasswordOrHash) {
|
||||||
|
return couldBeHash(plaintextPasswordOrHash)
|
||||||
|
? plaintextPasswordOrHash
|
||||||
|
: hash(plaintextPasswordOrHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void nextSalt(final String salt) {
|
||||||
|
predefinedSalts.add(salt);
|
||||||
|
}
|
||||||
|
|
||||||
|
public HashGenerator withSalt(final String salt) {
|
||||||
|
this.salt = salt;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HashGenerator withRandomSalt() {
|
||||||
|
if (!predefinedSalts.isEmpty()) {
|
||||||
|
return withSalt(predefinedSalts.poll());
|
||||||
|
}
|
||||||
|
final var stringBuilder = new StringBuilder(RANDOM_SALT_LENGTH);
|
||||||
|
for (int i = 0; i < RANDOM_SALT_LENGTH; ++i) {
|
||||||
|
int randomIndex = random.nextInt(RANDOM_SALT_CHARACTERS.length());
|
||||||
|
stringBuilder.append(RANDOM_SALT_CHARACTERS.charAt(randomIndex));
|
||||||
|
}
|
||||||
|
return withSalt(stringBuilder.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
System.out.println(
|
||||||
|
HashGenerator.using(Algorithm.LINUX_YESCRYPT).withRandomSalt().hash("my plaintext domain transfer passphrase")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
package net.hostsharing.hsadminng.hash;
|
||||||
|
|
||||||
|
import com.sun.jna.Library;
|
||||||
|
import com.sun.jna.Native;
|
||||||
|
|
||||||
|
public class LinuxEtcShadowHashGenerator {
|
||||||
|
|
||||||
|
public static String hash(final HashGenerator generator, final String payload) {
|
||||||
|
if (generator.getSalt() == null) {
|
||||||
|
throw new IllegalStateException("no salt given");
|
||||||
|
}
|
||||||
|
|
||||||
|
return NativeCryptLibrary.INSTANCE.crypt(payload, "$" + generator.getAlgorithm().enrichedSalt(generator.getSalt()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void verify(final String givenHash, final String payload) {
|
||||||
|
|
||||||
|
final var parts = givenHash.split("\\$");
|
||||||
|
if (parts.length < 3 || parts.length > 5) {
|
||||||
|
throw new IllegalArgumentException("hash with unknown hash method: " + givenHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
final var algorithm = HashGenerator.Algorithm.byPrefix(parts[1]);
|
||||||
|
final var salt = parts.length == 4 ? parts[2] : parts[2] + "$" + parts[3];
|
||||||
|
final var calculatedHash = HashGenerator.using(algorithm).withSalt(salt).hash(payload);
|
||||||
|
if (!calculatedHash.equals(givenHash)) {
|
||||||
|
throw new IllegalArgumentException("invalid password");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface NativeCryptLibrary extends Library {
|
||||||
|
NativeCryptLibrary INSTANCE = Native.load("crypt", NativeCryptLibrary.class);
|
||||||
|
|
||||||
|
String crypt(String password, String salt);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
package net.hostsharing.hsadminng.hash;
|
||||||
|
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
|
||||||
|
public class MySQLNativePasswordHashGenerator {
|
||||||
|
|
||||||
|
public static String hash(final HashGenerator generator, final String password) {
|
||||||
|
// TODO.impl: if a random salt is generated or not should be part of the algorithm definition
|
||||||
|
// if (generator.getSalt() != null) {
|
||||||
|
// throw new IllegalStateException("salt not supported");
|
||||||
|
// }
|
||||||
|
|
||||||
|
try {
|
||||||
|
final var sha1 = MessageDigest.getInstance("SHA-1");
|
||||||
|
final var firstHash = sha1.digest(password.getBytes());
|
||||||
|
final var secondHash = sha1.digest(firstHash);
|
||||||
|
return "*" + bytesToHex(secondHash).toUpperCase();
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
throw new RuntimeException("SHA-1 algorithm not found", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String bytesToHex(byte[] bytes) {
|
||||||
|
final var hexString = new StringBuilder();
|
||||||
|
for (byte b : bytes) {
|
||||||
|
final var hex = Integer.toHexString(0xff & b);
|
||||||
|
if (hex.length() == 1) {
|
||||||
|
hexString.append('0');
|
||||||
|
}
|
||||||
|
hexString.append(hex);
|
||||||
|
}
|
||||||
|
return hexString.toString();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
package net.hostsharing.hsadminng.hash;
|
||||||
|
|
||||||
|
import lombok.SneakyThrows;
|
||||||
|
|
||||||
|
import javax.crypto.Mac;
|
||||||
|
import javax.crypto.SecretKeyFactory;
|
||||||
|
import javax.crypto.spec.PBEKeySpec;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.spec.InvalidKeySpecException;
|
||||||
|
import java.util.Base64;
|
||||||
|
|
||||||
|
public class PostgreSQLScramSHA256 {
|
||||||
|
|
||||||
|
private static final String PBKDF_2_WITH_HMAC_SHA256 = "PBKDF2WithHmacSHA256";
|
||||||
|
private static final String HMAC_SHA256 = "HmacSHA256";
|
||||||
|
private static final String SHA256 = "SHA-256";
|
||||||
|
private static final int ITERATIONS = 4096;
|
||||||
|
public static final int KEY_LENGTH_IN_BITS = 256;
|
||||||
|
|
||||||
|
private static final PostgreSQLScramSHA256 scram = new PostgreSQLScramSHA256();
|
||||||
|
|
||||||
|
@SneakyThrows
|
||||||
|
public static String hash(final HashGenerator generator, final String password) {
|
||||||
|
if (generator.getSalt() == null) {
|
||||||
|
throw new IllegalStateException("no salt given");
|
||||||
|
}
|
||||||
|
|
||||||
|
final byte[] salt = generator.getSalt().getBytes(Charset.forName("latin1")); // Base64.getEncoder().encode(generator.getSalt().getBytes());
|
||||||
|
final byte[] saltedPassword = scram.generateSaltedPassword(password, salt);
|
||||||
|
final byte[] clientKey = scram.hmacSHA256(saltedPassword, "Client Key".getBytes());
|
||||||
|
final byte[] storedKey = MessageDigest.getInstance(SHA256).digest(clientKey);
|
||||||
|
final byte[] serverKey = scram.hmacSHA256(saltedPassword, "Server Key".getBytes());
|
||||||
|
|
||||||
|
return "SCRAM-SHA-256${iterations}:{base64EncodedSalt}${base64EncodedStoredKey}:{base64EncodedServerKey}"
|
||||||
|
.replace("{iterations}", Integer.toString(ITERATIONS))
|
||||||
|
.replace("{base64EncodedSalt}", base64(salt))
|
||||||
|
.replace("{base64EncodedStoredKey}", base64(storedKey))
|
||||||
|
.replace("{base64EncodedServerKey}", base64(serverKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String base64(final byte[] salt) {
|
||||||
|
return Base64.getEncoder().encodeToString(salt);
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] generateSaltedPassword(String password, byte[] salt) throws NoSuchAlgorithmException, InvalidKeySpecException {
|
||||||
|
final var spec = new PBEKeySpec(password.toCharArray(), salt, ITERATIONS, KEY_LENGTH_IN_BITS);
|
||||||
|
return SecretKeyFactory.getInstance(PBKDF_2_WITH_HMAC_SHA256).generateSecret(spec).getEncoded();
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] hmacSHA256(byte[] key, byte[] message)
|
||||||
|
throws NoSuchAlgorithmException, InvalidKeyException {
|
||||||
|
final var mac = Mac.getInstance(HMAC_SHA256);
|
||||||
|
mac.init(new SecretKeySpec(key, HMAC_SHA256));
|
||||||
|
return mac.doFinal(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,55 @@
|
|||||||
|
package net.hostsharing.hsadminng.hs.booking.debitor;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import net.hostsharing.hsadminng.errors.DisplayAs;
|
||||||
|
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||||
|
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||||
|
|
||||||
|
// a partial HsOfficeDebitorEntity to reduce the number of SQL queries to load the entity
|
||||||
|
@Entity
|
||||||
|
@Table(schema = "hs_booking", name = "debitor_xv")
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@DisplayAs("BookingDebitor")
|
||||||
|
public class HsBookingDebitorEntity implements Stringifyable {
|
||||||
|
|
||||||
|
public static final String DEBITOR_NUMBER_TAG = "D-";
|
||||||
|
|
||||||
|
private static Stringify<HsBookingDebitorEntity> stringify =
|
||||||
|
stringify(HsBookingDebitorEntity.class, "booking-debitor")
|
||||||
|
.withIdProp(HsBookingDebitorEntity::toShortString)
|
||||||
|
.withProp(HsBookingDebitorEntity::getDefaultPrefix)
|
||||||
|
.quotedValues(false);
|
||||||
|
|
||||||
|
@Id
|
||||||
|
private UUID uuid;
|
||||||
|
|
||||||
|
@Column(name = "debitornumber")
|
||||||
|
private Integer debitorNumber;
|
||||||
|
|
||||||
|
@Column(name = "defaultprefix", columnDefinition = "char(3) not null")
|
||||||
|
private String defaultPrefix;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return stringify.apply(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toShortString() {
|
||||||
|
return DEBITOR_NUMBER_TAG + debitorNumber;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
package net.hostsharing.hsadminng.hs.booking.debitor;
|
||||||
|
|
||||||
|
import org.springframework.data.repository.Repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public interface HsBookingDebitorRepository extends Repository<HsBookingDebitorEntity, UUID> {
|
||||||
|
|
||||||
|
Optional<HsBookingDebitorEntity> findByUuid(UUID id);
|
||||||
|
|
||||||
|
List<HsBookingDebitorEntity> findByDebitorNumber(int debitorNumber);
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
package net.hostsharing.hsadminng.hs.booking.item;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import org.springframework.context.ApplicationEvent;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public class BookingItemCreatedAppEvent extends ApplicationEvent {
|
||||||
|
|
||||||
|
private BookingItemCreatedEventEntity entity;
|
||||||
|
|
||||||
|
public BookingItemCreatedAppEvent(
|
||||||
|
@NotNull final Object source,
|
||||||
|
@NotNull final HsBookingItemRealEntity newBookingItem,
|
||||||
|
final String assetJson) {
|
||||||
|
super(source);
|
||||||
|
this.entity = new BookingItemCreatedEventEntity(newBookingItem, assetJson);
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user