Compare commits
1027
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f348bb7d8 | ||
|
|
630597746e | ||
|
|
f98036296c | ||
|
|
bf1f867f23 | ||
|
|
8215a85930 | ||
|
|
10a5686955 | ||
|
|
2e3c7a3acf | ||
|
|
06713da67b | ||
|
|
46fb1b6582 | ||
|
|
1b3f98db1e | ||
|
|
a190cdd8f2 | ||
|
|
8e25369001 | ||
|
|
8d90ad816a | ||
|
|
da9e42cb21 | ||
|
|
cbab3ff0d0 | ||
|
|
210b570c89 | ||
|
|
a2203df6a3 | ||
|
|
6a2b369014 | ||
|
|
fda6a6657f | ||
|
|
04565dad00 | ||
|
|
ac0ae06463 | ||
|
|
a8775f757f | ||
|
|
e4f4ed923d | ||
|
|
c7915fa95d | ||
|
|
652b118634 | ||
|
|
f122ff1471 | ||
|
|
f76b90fab9 | ||
|
|
ccd8beae92 | ||
|
|
33f01009d2 | ||
|
|
a0e3a71109 | ||
|
|
f35ecba801 | ||
|
|
0d7f26bd14 | ||
|
|
2f7bdd2640 | ||
|
|
3125e4ddd2 | ||
|
|
c624cb27da | ||
|
|
06adf9a094 | ||
|
|
94ed55ee35 | ||
|
|
95c8603b69 | ||
|
|
9ad2a8272e | ||
|
|
6dc17da00a | ||
|
|
97aa1c6b2c | ||
|
|
084fed8377 | ||
|
|
68d86e4fa1 | ||
|
|
5203373047 | ||
|
|
86de481ba1 | ||
|
|
5d075a97dd | ||
|
|
e89499f5db | ||
|
|
a1f0208767 | ||
|
|
d1964c20ab | ||
|
|
1e035bf2e5 | ||
|
|
f3b87f9cf9 | ||
|
|
d2ea45e1eb | ||
|
|
58ad6b1a40 | ||
|
|
772ef3cd10 | ||
|
|
204dfd52c8 | ||
|
|
244d3452d0 | ||
|
|
e65f8a7150 | ||
|
|
90d6991b9d | ||
|
|
87ca3f203a | ||
|
|
3f3a20e178 | ||
|
|
ce1a19fdbe | ||
|
|
26ac92a6b1 | ||
|
|
1c7dd0a533 | ||
|
|
ceb29d76b2 | ||
|
|
6efb354cce | ||
|
|
8ae242a6ed | ||
|
|
5f0f92fd15 | ||
|
|
e521fa3554 | ||
|
|
b8f73cf21f | ||
|
|
d068cddbaf | ||
|
|
99422835ab | ||
|
|
68263360c7 | ||
|
|
34f5ddab0f | ||
|
|
0cc4630fc6 | ||
|
|
ee57c79784 | ||
|
|
7e41b8df27 | ||
|
|
79c1d7bafa | ||
|
|
652b2fee86 | ||
|
|
dfb871c26a | ||
|
|
99f74acb94 | ||
|
|
83f7682606 | ||
|
|
c4a07f3098 | ||
|
|
c9451f4f35 | ||
|
|
dff4922783 | ||
|
|
c05b666f3c | ||
|
|
9905024f08 | ||
|
|
b0cb457287 | ||
|
|
d4946c1c08 | ||
|
|
1d745b5cbd | ||
|
|
c99785f266 | ||
|
|
9ecdec3f45 | ||
|
|
866f45bd95 | ||
|
|
320a79dbff | ||
|
|
1e0b6f1aad | ||
|
|
251ad6727a | ||
|
|
1b1f56d183 | ||
|
|
7dc05a45ac | ||
|
|
7257bab289 | ||
|
|
48a76c2d0d | ||
|
|
d78c83b236 | ||
|
|
d9b0415047 | ||
|
|
af2e3de96d | ||
|
|
1c142cb170 | ||
|
|
d5dae7d444 | ||
|
|
85816be967 | ||
|
|
e66d658955 | ||
|
|
1916fded95 | ||
|
|
8cc8f4c49c | ||
|
|
1a3bf16729 | ||
|
|
5304b533fc | ||
|
|
0156d47d97 | ||
|
|
b04ac6cdba | ||
|
|
f43e4200da | ||
|
|
2ba7aef2b1 | ||
|
|
7a1932cab4 | ||
|
|
59f189229c | ||
|
|
29af7a1d29 | ||
|
|
3d9ddc53e6 | ||
|
|
1a34feda65 | ||
|
|
d8c1081578 | ||
|
|
1085a1da62 | ||
|
|
576db8e0e1 | ||
|
|
fe9dc4a28d | ||
|
|
d2aaae2340 | ||
|
|
84e40cbc2c | ||
|
|
42672a9ae2 | ||
|
|
2dd2173553 | ||
|
|
fcd5400cda | ||
|
|
23b69bb7a7 | ||
|
|
c31b0945d8 | ||
|
|
c71a4ac4e8 | ||
|
|
38c7ca64cb | ||
|
|
bc187fe7b3 | ||
|
|
548c0db0ae | ||
|
|
3ecd1307b0 | ||
|
|
b51661b790 | ||
|
|
02d6e41ce3 | ||
|
|
f884aed25e | ||
|
|
f2dee1a470 | ||
|
|
d5d806760f | ||
|
|
efe2a8f789 | ||
|
|
69bbed999f | ||
|
|
5b7d4184f0 | ||
|
|
422b7ed5ef | ||
|
|
66a2b916f2 | ||
|
|
27ac253098 | ||
|
|
1839b24b90 | ||
|
|
a60ebb6210 | ||
|
|
4dba4f3c02 | ||
|
|
6727af2f35 | ||
|
|
5bcf334bc5 | ||
|
|
ef49bd685e | ||
|
|
092562fb94 | ||
|
|
44e2983803 | ||
|
|
2e848da1a8 | ||
|
|
22ae4ecb23 | ||
|
|
1642ac2394 | ||
|
|
be8aa93cca | ||
|
|
fc7ba85639 | ||
|
|
11d1dd6419 | ||
|
|
e8bd7270e9 | ||
|
|
99e2afa81b | ||
|
|
676792d07d | ||
|
|
0bec2bcd1e | ||
|
|
25328048aa | ||
|
|
e51d2137cf | ||
|
|
5695677873 | ||
|
|
9d86f582ac | ||
|
|
97b0558f71 | ||
|
|
e825be1c79 | ||
|
|
88252c3dfc | ||
|
|
02997b593f | ||
|
|
9de81a0b14 | ||
|
|
ab13182091 | ||
|
|
e75e4580ae | ||
|
|
b7f031d199 | ||
|
|
808d27be2d | ||
|
|
dcf8751ff7 | ||
|
|
ec6e3d80ee | ||
|
|
5a9badf683 | ||
|
|
dd11dcef93 | ||
|
|
e9851957f0 | ||
|
|
57cfbb3770 | ||
|
|
da2590819d | ||
|
|
d74d93a522 | ||
|
|
b3eadcc990 | ||
|
|
f4d4405c20 | ||
|
|
1ebec0e2e5 | ||
|
|
4433347ff3 | ||
|
|
08c539ba51 | ||
|
|
4e538dd138 | ||
|
|
66373ccab2 | ||
|
|
9c82c0700d | ||
|
|
a480e50c16 | ||
|
|
4391bf4f70 | ||
|
|
18f46cb527 | ||
|
|
32b89adc04 | ||
|
|
5a213fcad9 | ||
|
|
d6f1d1bb3c | ||
|
|
c8c0d0e804 | ||
|
|
aeddf2a860 | ||
|
|
b876a5f7bd | ||
|
|
af8949c291 | ||
|
|
e3da38df7c | ||
|
|
a16079e38d | ||
|
|
e13b0fac63 | ||
|
|
26190350c4 | ||
|
|
7598cc62d3 | ||
|
|
b3211fa8c2 | ||
|
|
8dcc909b6e | ||
|
|
fafaf2c490 | ||
|
|
11e2f19503 | ||
|
|
d67bd0eff2 | ||
|
|
43bbea9a7c | ||
|
|
dc366c53a7 | ||
|
|
fe1ede26be | ||
|
|
aca799b28c | ||
|
|
1017a0af1d | ||
|
|
ff25484a34 | ||
|
|
af922920ab | ||
|
|
919a3f1b91 | ||
|
|
1ec8e63e9e | ||
|
|
96c76a059a | ||
|
|
608701aea7 | ||
|
|
142d44a01f | ||
|
|
83c0f537ce | ||
|
|
aeab25b7fe | ||
|
|
ef255c6a5c | ||
|
|
474ebed0ee | ||
|
|
6e55604dc3 | ||
|
|
2dfdfefb1a | ||
|
|
48f78cf80a | ||
|
|
69cc28dd4b | ||
|
|
adb8bcf791 | ||
|
|
bd22d30674 | ||
|
|
afbf1cc33f | ||
|
|
485df59c5b | ||
|
|
43308b326d | ||
|
|
6c9708c700 | ||
|
|
4a962aa75a | ||
|
|
e786158071 | ||
|
|
ea5a767948 | ||
|
|
bde65a6c57 | ||
|
|
94c0a436b5 | ||
|
|
bddf1adeba | ||
|
|
9406a577c2 | ||
|
|
40344726e1 | ||
|
|
08ffea23b7 | ||
|
|
f1ddaa8051 | ||
|
|
7ae62c1060 | ||
|
|
7c51046d8a | ||
|
|
df6d75e2c5 | ||
|
|
3c88d9666d | ||
|
|
ee74025f38 | ||
|
|
d49c7d47a2 | ||
|
|
8f18e9bbbc | ||
|
|
be96a3785b | ||
|
|
f3aeb5ec9f | ||
|
|
71ea61e7a6 | ||
|
|
c6c73e5e59 | ||
|
|
ae242f8480 | ||
|
|
351a17f68d | ||
|
|
1c4f7cb339 | ||
|
|
2713af189f | ||
|
|
be62d1a915 | ||
|
|
68d71a6767 | ||
|
|
46c88ad1bf | ||
|
|
917151123f | ||
|
|
b9e9d6a787 | ||
|
|
db20077c37 | ||
|
|
ec3d027f90 | ||
|
|
50678085c9 | ||
|
|
5af4785ba1 | ||
|
|
f59d42d5d3 | ||
|
|
fc7ae1ee96 | ||
|
|
2699bb9a44 | ||
|
|
212f5b4082 | ||
|
|
9591f48077 | ||
|
|
0a8494a0f2 | ||
|
|
39f2a4ab71 | ||
|
|
39aa09ca36 | ||
|
|
45c5a2ae78 | ||
|
|
a1c612f833 | ||
|
|
bc39bb32d4 | ||
|
|
884308ecb5 | ||
|
|
2d7f232c3d | ||
|
|
afa65d9ead | ||
|
|
605c88a029 | ||
|
|
09994d1730 | ||
|
|
bb9c99ebf6 | ||
|
|
d4819886a6 | ||
|
|
23445123a2 | ||
|
|
773bb43b41 | ||
|
|
493dddde02 | ||
|
|
2e5704c6b4 | ||
|
|
be6c843bba | ||
|
|
064d677fdc | ||
|
|
084ab782da | ||
|
|
1a10a1ef75 | ||
|
|
acc77e4e6c | ||
|
|
2c0ff9be60 | ||
|
|
b9f5946cf0 | ||
|
|
c403968fa8 | ||
|
|
14baef425e | ||
|
|
8da532da93 | ||
|
|
facf701897 | ||
|
|
f9fe9ea5d6 | ||
|
|
f5a80c3007 | ||
|
|
7cff37e707 | ||
|
|
4e8c6b3fc4 | ||
|
|
82282c1c75 | ||
|
|
d7379f29da | ||
|
|
000159226d | ||
|
|
4ebdb8d24b | ||
|
|
fc2df08e15 | ||
|
|
aaa83dc19a | ||
|
|
adff0d5da0 | ||
|
|
285d6931bc | ||
|
|
114b687c5c | ||
|
|
a26ade3988 | ||
|
|
94e2983310 | ||
|
|
fb2f5f6756 | ||
|
|
d7b818cba2 | ||
|
|
8d885d14ed | ||
|
|
39e0023a5f | ||
|
|
84faac0cd6 | ||
|
|
1bce639d35 | ||
|
|
f2061610b9 | ||
|
|
8716f9b41a | ||
|
|
a24c817bb7 | ||
|
|
47997e0056 | ||
|
|
70fd7bea99 | ||
|
|
52565375b3 | ||
|
|
2c327d9eee | ||
|
|
caeb710444 | ||
|
|
0b0b101184 | ||
|
|
f5b4a0788e | ||
|
|
1aa338fe3e | ||
|
|
5615d8d870 | ||
|
|
9711ccd914 | ||
|
|
fbc2b845ad | ||
|
|
39ecca0989 | ||
|
|
f19b6afd85 | ||
|
|
dca347398d | ||
|
|
de6341bd87 | ||
|
|
5cb9dbb87a | ||
|
|
dddc5181e7 | ||
|
|
3b78fa2d5d | ||
|
|
2a64f5e3ce | ||
|
|
8f5b53e3ce | ||
|
|
605f5decb3 | ||
|
|
d00e128920 | ||
|
|
226c1c53c0 | ||
|
|
001b874903 | ||
|
|
cc523ea356 | ||
|
|
5dc2b7b1d5 | ||
|
|
cd84e78b27 | ||
|
|
dedaa24972 | ||
|
|
1b0c4e5d1f | ||
|
|
b7e3fc5f73 | ||
|
|
9b9ec09788 | ||
|
|
bd7ceb0b73 | ||
|
|
9638c0dd83 | ||
|
|
60f40544bb | ||
|
|
066fb7178d | ||
|
|
cd762e5e8c | ||
|
|
f990d68cee | ||
|
|
88f14b39a7 | ||
|
|
eda64584d7 | ||
|
|
2f2a2dced6 | ||
|
|
38a2ea2fdf | ||
|
|
43bad9a608 | ||
|
|
be5f3086ce | ||
|
|
663f12d5e6 | ||
|
|
6e758d721d | ||
|
|
4c98b04ab7 | ||
|
|
f846645989 | ||
|
|
6bc79b590c | ||
|
|
2ad7e870d8 | ||
|
|
87636669ae | ||
|
|
8d627f1329 | ||
|
|
c51cc4703b | ||
|
|
a1124c9e1f | ||
|
|
20e245cb05 | ||
|
|
5c8e165080 | ||
|
|
afe088a9e8 | ||
|
|
162f31baa2 | ||
|
|
28a50df746 | ||
|
|
5a0d600322 | ||
|
|
38c1945ce4 | ||
|
|
939527bd98 | ||
|
|
cc2de52a1a | ||
|
|
4ccc37abbb | ||
|
|
47d1681e90 | ||
|
|
c89a412661 | ||
|
|
5bd53804f9 | ||
|
|
0d957311b4 | ||
|
|
c2495acca9 | ||
|
|
c297b29a13 | ||
|
|
112ebf77d5 | ||
|
|
5993d40fab | ||
|
|
2d82d15e5b | ||
|
|
656b157103 | ||
|
|
392fa21f1e | ||
|
|
fa99b5415d | ||
|
|
514417ac2d | ||
|
|
938cfe2555 | ||
|
|
d6e64a2c5e | ||
|
|
c52d16da6d | ||
|
|
3f62cd9271 | ||
|
|
df7cb4a9d0 | ||
|
|
9a37696d29 | ||
|
|
20fe7ec7b8 | ||
|
|
37cef49565 | ||
|
|
b5ee395aac | ||
|
|
7b89b80f71 | ||
|
|
08f2b0dea9 | ||
|
|
92e17f4b40 | ||
|
|
b8aedd5a3a | ||
|
|
d3e178d459 | ||
|
|
996d8e8cd3 | ||
|
|
0e1a059d8d | ||
|
|
30359b51fa | ||
|
|
d8880e5ff3 | ||
|
|
6819b8e3a5 | ||
|
|
03b521926a | ||
|
|
abfec23404 | ||
|
|
3dd282e11b | ||
|
|
92efe8441e | ||
|
|
95349e6e2e | ||
|
|
15f8e83d4a | ||
|
|
0095968a73 | ||
|
|
ad01d64803 | ||
|
|
30405804bc | ||
|
|
6093d00fdd | ||
|
|
1ba5ea411b | ||
|
|
9dd7a947e9 | ||
|
|
c51e6a916c | ||
|
|
e9b9da6d20 | ||
|
|
557edbec6d | ||
|
|
93650c442a | ||
|
|
d3e9b29d78 | ||
|
|
7cf1b002d2 | ||
|
|
e96ba202e9 | ||
|
|
9a5ca4d367 | ||
|
|
e6928c6456 | ||
|
|
245edcb5b9 | ||
|
|
e4cb97aa49 | ||
|
|
bc93066fe3 | ||
|
|
bf686bd791 | ||
|
|
01e232158b | ||
|
|
c6e0195a3c | ||
|
|
966d939c12 | ||
|
|
c61c05be51 | ||
|
|
7b099e3d6b | ||
|
|
6d257a0d4c | ||
|
|
b80a4a0aa9 | ||
|
|
aabb3428a8 | ||
|
|
80ff7b92db | ||
|
|
7badd3d5e5 | ||
|
|
e977a363ef | ||
|
|
91283878ac | ||
|
|
74ace4006c | ||
|
|
7be957f4f9 | ||
|
|
3643e76a06 | ||
|
|
07bc756785 | ||
|
|
0af68d28f1 | ||
|
|
75e1f50faf | ||
|
|
dcc40b0a63 | ||
|
|
dc0d9a8dce | ||
|
|
021b01f0bd | ||
|
|
b578122f19 | ||
|
|
e014dcce76 | ||
|
|
a1bdf2837e | ||
|
|
0092d9f74d | ||
|
|
e48aafd588 | ||
|
|
d958d85290 | ||
|
|
5b40031251 | ||
|
|
399a4a0511 | ||
|
|
bebe51c885 | ||
|
|
6812514683 | ||
|
|
62de481329 | ||
|
|
c3eb6df220 | ||
|
|
d875b9885d | ||
|
|
9ab023cc6c | ||
|
|
3510e17dd3 | ||
|
|
f50570e350 | ||
|
|
a52cdae1f7 | ||
|
|
826c60ae36 | ||
|
|
d2c9d1569c | ||
|
|
15dca61c59 | ||
|
|
4224496611 | ||
|
|
7467fbaa12 | ||
|
|
580e4fb0b2 | ||
|
|
90b15a070c | ||
|
|
b54061fc3f | ||
|
|
ce7de3b576 | ||
|
|
31d0365e85 | ||
|
|
084da8a03d | ||
|
|
e395ecb749 | ||
|
|
a34cb92fc1 | ||
|
|
c12fe28b2d | ||
|
|
0ac000adca | ||
|
|
1dd0b563b6 | ||
|
|
26acf6fa83 | ||
|
|
dcc19a39d7 | ||
|
|
496062ef0c | ||
|
|
b27f18c380 | ||
|
|
442d65bb5f | ||
|
|
35e4a5e56d | ||
|
|
fb60ca115f | ||
|
|
3d3871b0e6 | ||
|
|
060a59f982 | ||
|
|
c6c0abbedf | ||
|
|
df182c3301 | ||
|
|
56c858d917 | ||
|
|
00068c460a | ||
|
|
5d0b6400df | ||
|
|
413295e15e | ||
|
|
1c3d165e5d | ||
|
|
5fe0406ec9 | ||
|
|
17affe576f | ||
|
|
b86d091ad1 | ||
|
|
ea544c639e | ||
|
|
492e6cd856 | ||
|
|
5680be743c | ||
|
|
1fd22ee672 | ||
|
|
c3faea0428 | ||
|
|
483ac69c47 | ||
|
|
805ad9f250 | ||
|
|
43799fd315 | ||
|
|
91ec4cb5c7 | ||
|
|
634ce85f17 | ||
|
|
b6c52e8859 | ||
|
|
d042f6e2c9 | ||
|
|
cfe8c2956f | ||
|
|
235a8eab02 | ||
|
|
6baefa7490 | ||
|
|
319b29aa7d | ||
|
|
02428e4f0f | ||
|
|
3fe93eebb6 | ||
|
|
f5c7c14ce0 | ||
|
|
f57ab78916 | ||
|
|
087dab6f3d | ||
|
|
3df504b6b4 | ||
|
|
ea98b26766 | ||
|
|
6d6e598ce5 | ||
|
|
67ad7463f4 | ||
|
|
df6b481112 | ||
|
|
5301b556f0 | ||
|
|
2ff7345b08 | ||
|
|
bafcd3f036 | ||
|
|
5f787028df | ||
|
|
e1e65af8ea | ||
|
|
5bb71d396a | ||
|
|
058f44743c | ||
|
|
2077a2a3af | ||
|
|
5a3a0ae437 | ||
|
|
b4204fc806 | ||
|
|
e8c90c1327 | ||
|
|
136f2c8bb7 | ||
|
|
2b5e0e238d | ||
|
|
a6eface8bc | ||
|
|
7659b1aa18 | ||
|
|
a953b52eac | ||
|
|
6f164f0ee9 | ||
|
|
f61a9d7a70 | ||
|
|
4f270bffd0 | ||
|
|
fa90602aa0 | ||
|
|
eaf5034ec7 | ||
|
|
e83de896c9 | ||
|
|
bcd607a1ee | ||
|
|
e1adceeb85 | ||
|
|
106bdc3c52 | ||
|
|
5229807020 | ||
|
|
1e89467b31 | ||
|
|
f65b502a96 | ||
|
|
efe8342965 | ||
|
|
90cb3acd2f | ||
|
|
98dd87efbc | ||
|
|
ce6576ce84 | ||
|
|
5d4869f915 | ||
|
|
92567a070c | ||
|
|
30955c4e2c | ||
|
|
f9d51db182 | ||
|
|
852e799fea | ||
|
|
bbc3e99bb5 | ||
|
|
e8e22ee44c | ||
|
|
7a211beede | ||
|
|
24279b138b | ||
|
|
a2718a889d | ||
|
|
756de9f7f6 | ||
|
|
143d176dcd | ||
|
|
a1f7cfaf3a | ||
|
|
a549b3ac70 | ||
|
|
43c9179204 | ||
|
|
dfe232cf80 | ||
|
|
ba88cdce70 | ||
|
|
269f9fe860 | ||
|
|
4a670ab236 | ||
|
|
ab934f3d35 | ||
|
|
24eca809f1 | ||
|
|
57780b6520 | ||
|
|
f5e301fe0d | ||
|
|
68976f5b0c | ||
|
|
8e7e208265 | ||
|
|
0951c11aa6 | ||
|
|
3d4ca01242 | ||
|
|
319f86e91a | ||
|
|
490b3e3603 | ||
|
|
7476ee51e4 | ||
|
|
a9f1fda68a | ||
|
|
975337f225 | ||
|
|
1490f106f2 | ||
|
|
61c35265a6 | ||
|
|
0f4497e53e | ||
|
|
ca60c283fc | ||
|
|
37298bdbd6 | ||
|
|
d70fd86a82 | ||
|
|
ad606cbce6 | ||
|
|
4e0a32a37a | ||
|
|
7290040008 | ||
|
|
91bf50eb6c | ||
|
|
748ed30b47 | ||
|
|
b88d2f792d | ||
|
|
c8b19fa1ae | ||
|
|
c0795ce2c7 | ||
|
|
12c3aa83dd | ||
|
|
c193ba0c81 | ||
|
|
d8226c457f | ||
|
|
a507771f7c | ||
|
|
2f8d577e95 | ||
|
|
362babe5e2 | ||
|
|
d9297cee16 | ||
|
|
3851a2fe72 | ||
|
|
338300df32 | ||
|
|
0c7f63b2f3 | ||
|
|
9a87b7f196 | ||
|
|
abcd830af1 | ||
|
|
f807c999b6 | ||
|
|
ccd5deb85f | ||
|
|
dbf9525860 | ||
|
|
502efe42c9 | ||
|
|
f0787d98d1 | ||
|
|
d2ed8302a7 | ||
|
|
15310d1e97 | ||
|
|
38af401a16 | ||
|
|
ab1d39dfb3 | ||
|
|
d35786fe49 | ||
|
|
b9db84edf3 | ||
|
|
c3176cac6a | ||
|
|
6c98d64874 | ||
|
|
269dc26029 | ||
|
|
48d26fc0e9 | ||
|
|
797bf49ea6 | ||
|
|
fd96bacf70 | ||
|
|
7f9739cd4f | ||
|
|
698660c92a | ||
|
|
5aa90243a0 | ||
|
|
4d32ac8765 | ||
|
|
224f85b981 | ||
|
|
29c72c0744 | ||
|
|
36c162126b | ||
|
|
252dfe006b | ||
|
|
d1743f0608 | ||
|
|
9168cf811c | ||
|
|
9e50ae6a01 | ||
|
|
371c798116 | ||
|
|
d00e0021b6 | ||
|
|
bce3e61b7d | ||
|
|
ccbd4bace1 | ||
|
|
78999537f0 | ||
|
|
e3b0cc7093 | ||
|
|
50014f4398 | ||
|
|
833c276424 | ||
|
|
770525a385 | ||
|
|
e4204a56f5 | ||
|
|
d37a9eada3 | ||
|
|
a7bfae2f56 | ||
|
|
aaebcd074e | ||
|
|
58fc4520c9 | ||
|
|
53716697ca | ||
|
|
96668daefa | ||
|
|
4b033d3d93 | ||
|
|
8ca9a6a64a | ||
|
|
2ce3925ba9 | ||
|
|
3d9adbe991 | ||
|
|
f16828e129 | ||
|
|
91eb80beb0 | ||
|
|
30682968ff | ||
|
|
620e3b67e9 | ||
|
|
afe1ddd719 | ||
|
|
0aefacc88e | ||
|
|
366c707afa | ||
|
|
707e29adef | ||
|
|
66f2735224 | ||
|
|
86e19c274f | ||
|
|
46bb59e348 | ||
|
|
37cbba57e6 | ||
|
|
b05da425b9 | ||
|
|
c28f5cef7a | ||
|
|
f65e804836 | ||
|
|
01e9b3bcff | ||
|
|
4e58bed39e | ||
|
|
7d2942e1be | ||
|
|
943404689e | ||
|
|
89875aea02 | ||
|
|
70ce96caa0 | ||
|
|
625fb02dfe | ||
|
|
18966334ab | ||
|
|
a8f3756bbc | ||
|
|
f0dab97b80 | ||
|
|
3a7bba041f | ||
|
|
a50c0e61e4 | ||
|
|
87cb28eaab | ||
|
|
03fa58a271 | ||
|
|
666e610334 | ||
|
|
3c76a85674 | ||
|
|
c571ad9902 | ||
|
|
355c440c3d | ||
|
|
9a17658f84 | ||
|
|
69347aa7f6 | ||
|
|
bf259f4c98 | ||
|
|
082ebe8043 | ||
|
|
a13a02c995 | ||
|
|
3a9ac80786 | ||
|
|
d3cf177ebc | ||
|
|
7b49dbb74e | ||
|
|
4cc249bcd4 | ||
|
|
035529d7a2 | ||
|
|
fa91c0ed03 | ||
|
|
adbde56c1a | ||
|
|
a917939441 | ||
|
|
f793e883ef | ||
|
|
d2780c0b2e | ||
|
|
896e309866 | ||
|
|
a6dbfc7861 | ||
|
|
070ed88337 | ||
|
|
bd52908df4 | ||
|
|
5399c20a8c | ||
|
|
4a82ed73a4 | ||
|
|
f388c3d385 | ||
|
|
2de0748e95 | ||
|
|
997ede06fb | ||
|
|
68be5dd12a | ||
|
|
ad751e419c | ||
|
|
c862479dcd | ||
|
|
5cb11b9036 | ||
|
|
f63d52af04 | ||
|
|
3c0959640b | ||
|
|
dea4f4b931 | ||
|
|
3d26ab8817 | ||
|
|
3f3b21a75e | ||
|
|
b0002d6a09 | ||
|
|
c1708ea7d5 | ||
|
|
937ab313e5 | ||
|
|
523f746745 | ||
|
|
f5f5d934c8 | ||
|
|
d67804cd0b | ||
|
|
dcbdb59e00 | ||
|
|
92db89f9e8 | ||
|
|
aafc46657a | ||
|
|
667e014971 | ||
|
|
ed525f8ed9 | ||
|
|
e03140a9f2 | ||
|
|
6377cb71f7 | ||
|
|
a3c40f311c | ||
|
|
c736fab75e | ||
|
|
df442a6ab8 | ||
|
|
d355d53262 | ||
|
|
a7bebadf20 | ||
|
|
59d7ff3f0a | ||
|
|
7f02ac84ad | ||
|
|
70f53c6c06 | ||
|
|
b3723cf323 | ||
|
|
edfe7bd285 | ||
|
|
311fca5a72 | ||
|
|
98d82d2cae | ||
|
|
9372d23ad9 | ||
|
|
de2ce8a04e | ||
|
|
7eafb14622 | ||
|
|
fd91d46b5b | ||
|
|
3768c1f1ad | ||
|
|
dcbf91eff0 | ||
|
|
66242b4176 | ||
|
|
39627f0661 | ||
|
|
71f2bf77f8 | ||
|
|
99c79f9fd7 | ||
|
|
c942f44437 | ||
|
|
efaca72631 | ||
|
|
6fb98c547a | ||
|
|
25bddb8ee7 | ||
|
|
f1bff01999 | ||
|
|
74e5c62069 | ||
|
|
e52e13d611 | ||
|
|
8f6e7d275e | ||
|
|
ad42b4dde3 | ||
|
|
973fa0d4ec | ||
|
|
640e6efefd | ||
|
|
e026c8b206 | ||
|
|
761be66362 | ||
|
|
e54bf4a16f | ||
|
|
35c33f132e | ||
|
|
666827b7cb | ||
|
|
10998eb35b | ||
|
|
10d02e7442 | ||
|
|
8d493381a9 | ||
|
|
d353add130 | ||
|
|
77d17001bf | ||
|
|
df317b8368 | ||
|
|
ee358ccbc4 | ||
|
|
4450416905 | ||
|
|
0b821a71e3 | ||
|
|
fd620ef505 | ||
|
|
e05f60b66e | ||
|
|
947a2f0c20 | ||
|
|
063cefa18c | ||
|
|
8dfc7d9e8d | ||
|
|
d632d22d2a | ||
|
|
4a89fe93b5 | ||
|
|
08e1f455e2 | ||
|
|
e65c0d9893 | ||
|
|
be656b2dba | ||
|
|
bfcf595346 | ||
|
|
06a7938aa6 | ||
|
|
e51cb4df60 | ||
|
|
553f8a6f27 | ||
|
|
4d288f1ffc | ||
|
|
3e5854c651 | ||
|
|
53fc940952 | ||
|
|
c95e21a49a | ||
|
|
692898501e | ||
|
|
1fbbdb5f4a | ||
|
|
d4c69e98ce | ||
|
|
8224f3ba48 | ||
|
|
5e0f560fdc | ||
|
|
1d4bf4ddaa | ||
|
|
775d1f096c | ||
|
|
316053018b | ||
|
|
1d4ef83b81 | ||
|
|
074b6d261a | ||
|
|
bbe2532acb | ||
|
|
4166393396 | ||
|
|
9ce452443d | ||
|
|
215a3497ae | ||
|
|
f17863041a | ||
|
|
40510683a1 | ||
|
|
2b1ec74fba | ||
|
|
206b44d9c2 | ||
|
|
44787d83d2 | ||
|
|
23a3aa023c | ||
|
|
9151d2aebd | ||
|
|
5e1ac684a5 | ||
|
|
5fca18b3c0 | ||
|
|
7395d6585c | ||
|
|
05d0f1d2ee | ||
|
|
a338626d2b | ||
|
|
767d1fcc20 | ||
|
|
b8d23829aa | ||
|
|
ce747ad039 | ||
|
|
3a4670e8ff | ||
|
|
68c3652078 | ||
|
|
1f49ae8a2f | ||
|
|
ac49ba2f44 | ||
|
|
f0d3fa783a | ||
|
|
d007ee3630 | ||
|
|
84fd38316c | ||
|
|
104a9f9842 | ||
|
|
d49e0eda9e | ||
|
|
76f406e993 | ||
|
|
d1e6b7523e | ||
|
|
14b3d085a0 | ||
|
|
c942a8ac4f | ||
|
|
535023d800 | ||
|
|
dae219d32e | ||
|
|
a7d245b1fb | ||
|
|
1f4df58e6c | ||
|
|
d314198e40 | ||
|
|
35b6c88500 | ||
|
|
f74e1463df | ||
|
|
37243d058c | ||
|
|
927032997e | ||
|
|
e64a2d8de5 | ||
|
|
e697d5ebf5 | ||
|
|
9d5cd2091e | ||
|
|
870f29d797 | ||
|
|
f037aaa1af | ||
|
|
e17105cc8e | ||
|
|
cf75a97f08 | ||
|
|
8e0252ecb1 | ||
|
|
34e033e351 | ||
|
|
11035dbe1b | ||
|
|
0b782e9910 | ||
|
|
136fd37b1b | ||
|
|
b5f3efe272 | ||
|
|
955967f78a | ||
|
|
642fc996fb | ||
|
|
6ce0285315 | ||
|
|
aef61411e9 | ||
|
|
83a409bbf6 | ||
|
|
5ed864fbbc | ||
|
|
416e71fa97 | ||
|
|
f9b099d49d | ||
|
|
193aa23581 | ||
|
|
3a4750c6de | ||
|
|
63201c10f5 | ||
|
|
65f058a088 | ||
|
|
c95c9fa022 | ||
|
|
af736a29f1 | ||
|
|
22f26a6221 | ||
|
|
d30b81144e | ||
|
|
e32ef66e2a | ||
|
|
3ceb818ee5 | ||
|
|
6b1e4b2ab3 | ||
|
|
3a4141f5b5 | ||
|
|
5ea6169214 | ||
|
|
46c63d9169 | ||
|
|
db1d6e79d9 | ||
|
|
8eb2f35423 | ||
|
|
a7c563b0eb | ||
|
|
6a6ddc8a0c | ||
|
|
6965364813 | ||
|
|
9937be1cdf | ||
|
|
78830836b8 | ||
|
|
aea5055b30 | ||
|
|
a75d608f87 | ||
|
|
d52a365766 | ||
|
|
c071601660 | ||
|
|
fad0e80bcf | ||
|
|
f959332117 | ||
|
|
fb7ca4928d | ||
|
|
f5bc936e0d | ||
|
|
1ef5bf2abf | ||
|
|
14b2401aa8 | ||
|
|
6e5820ea11 | ||
|
|
115e533a71 | ||
|
|
495ab25301 | ||
|
|
afe4f84135 | ||
|
|
7d853dabe4 | ||
|
|
e32d7c52fc | ||
|
|
5addabe893 | ||
|
|
0daa3ee29f | ||
|
|
9c5e38a023 | ||
|
|
d42c917448 | ||
|
|
8fb60226c8 | ||
|
|
fda190de01 | ||
|
|
58dd353238 | ||
|
|
0027d22964 | ||
|
|
1a05f0f3b4 | ||
|
|
476da4e3e0 | ||
|
|
edb9f0cecf | ||
|
|
0addc4cf58 | ||
|
|
770d2d8a4c | ||
|
|
f4898884e4 | ||
|
|
5ef1ddb9f1 | ||
|
|
f3e5541b7d | ||
|
|
a19a3f4277 | ||
|
|
3f327326aa | ||
|
|
29ea24c8fb | ||
|
|
4fe328002a | ||
|
|
b1b149e0c3 | ||
|
|
47da227809 | ||
|
|
43d33260c9 | ||
|
|
ad715371a3 | ||
|
|
0ff4b6dd93 | ||
|
|
e3bb61248a | ||
|
|
e75e85cf80 | ||
|
|
375d061f58 | ||
|
|
78eb5acb8e | ||
|
|
656a5c4baa | ||
|
|
b8d05c452c | ||
|
|
f6631a6163 | ||
|
|
c0f6b6d8d0 | ||
|
|
d13acaa9ef | ||
|
|
8ebac35706 | ||
|
|
517e42f13d | ||
|
|
9c6e6c8284 | ||
|
|
a9ed193826 | ||
|
|
a94fd9a098 | ||
|
|
411ff56499 | ||
|
|
c70d12a411 | ||
|
|
d4f138cadc | ||
|
|
da70391677 | ||
|
|
f61fb4ba18 | ||
|
|
d2a5403527 | ||
|
|
06ac706057 | ||
|
|
4abd62661f | ||
|
|
f56f1c85b9 | ||
|
|
46e386cd43 | ||
|
|
600353899a | ||
|
|
4fbbf39c51 | ||
|
|
9a047eda36 | ||
|
|
716bf104db | ||
|
|
f435a8c6df | ||
|
|
a18715a774 | ||
|
|
f0f0aae88d | ||
|
|
4ae9310f83 | ||
|
|
1567982f0b | ||
|
|
875aae91fc | ||
|
|
bbef4e2be1 | ||
|
|
4955f36115 | ||
|
|
0606cb4a5e | ||
|
|
85e7cf5783 | ||
|
|
6fbc0975c0 | ||
|
|
b92a178142 | ||
|
|
eb5ae4aae9 | ||
|
|
dc23ef6188 | ||
|
|
b0b28a9f38 | ||
|
|
e6f6f92403 | ||
|
|
63f8d92a8b | ||
|
|
a42a750ebb | ||
|
|
335832d553 | ||
|
|
ac1e8e8389 | ||
|
|
448fb35a72 | ||
|
|
2ba2e7768d | ||
|
|
d0ead0f0a0 | ||
|
|
463310e1c3 | ||
|
|
5ca9af0b1d | ||
|
|
e795bdc16c | ||
|
|
37306e689d | ||
|
|
ca13e3c4ba | ||
|
|
3a5861c624 | ||
|
|
326818ea45 | ||
|
|
148b466ce4 | ||
|
|
18e4be3cc9 | ||
|
|
6a34ad36f5 | ||
|
|
56a9451128 |
No files matched your search
@@ -0,0 +1,20 @@
|
||||
* text=auto
|
||||
|
||||
*.html text eol=lf
|
||||
*.java text eol=lf
|
||||
*.js text eol=lf
|
||||
*.json text eol=lf
|
||||
*.jsp text eol=lf
|
||||
*.md text eol=lf
|
||||
*.properties text eol=lf
|
||||
*.svg text auto
|
||||
*.xml text eol=lf
|
||||
*.xsl text eol=lf
|
||||
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.gif binary
|
||||
*.ttf binary
|
||||
*.eot binary
|
||||
*.otf binary
|
||||
*.woff binary
|
||||
+13
-2
@@ -1,8 +1,19 @@
|
||||
language: java
|
||||
|
||||
jdk:
|
||||
- oraclejdk8
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.m2
|
||||
sudo: false
|
||||
|
||||
after_script: 'misc/.travis/after.sh'
|
||||
before_cache:
|
||||
- rm -rf $HOME/.m2/repository/org/keycloak
|
||||
|
||||
install: mvn install -Pdistribution -DskipTests=true -B -V
|
||||
|
||||
script:
|
||||
- mvn test -B
|
||||
- mvn -file testsuite/integration-arquillian test -B
|
||||
|
||||
sudo: false
|
||||
@@ -1,39 +1,72 @@
|
||||
keycloak
|
||||
Keycloak
|
||||
========
|
||||
|
||||
Please visit [http://keycloak.org](http://keycloak.org) for more information on Keycloak including how to download, documentation,
|
||||
and video tutorials.
|
||||
Keycloak is an SSO Service for web apps and REST services. For more information visit [http://keycloak.org](http://keycloak.org).
|
||||
|
||||
Keycloak is an SSO Service for web apps and REST services.
|
||||
It can be used for social applications as well as enterprise applications. It is based on OpenID Connect with support for SAML 2.0 as well.
|
||||
Here's some of the features:
|
||||
|
||||
* SSO and Single Log Out for browser applications
|
||||
* Social Broker. Enable Google, Facebook, Yahoo, Twitter, GitHub, LinkedIn social login with no code required.
|
||||
* Optional LDAP/Active Directory integration
|
||||
* Optional User Registration
|
||||
* Password and TOTP support (via Google Authenticator or FreeOTP). Client cert auth coming soon.
|
||||
* User session management from both admin and user perspective
|
||||
* Customizable themes for user facing pages: login, grant pages, account management, emails, and admin console all customizable!
|
||||
* OAuth Bearer token auth for REST Services
|
||||
* Integrated Browser App to REST Service token propagation
|
||||
* Admin REST API
|
||||
* OAuth 2.0 Grant requests
|
||||
* CORS Support
|
||||
* CORS Web Origin management and validation
|
||||
* Completely centrally managed user and role mapping metadata. Minimal configuration at the application side
|
||||
* Admin Console for managing users, roles, role mappings, applications, user sessions, allowed CORS web origins, and OAuth clients.
|
||||
* Deployable as a WAR, appliance, or an Openshift cloud service (SaaS).
|
||||
* Supports JBoss AS7, EAP 6.x, Wildfly, Tomcat, and Jetty applications. Plans to support Node.js, RAILS, GRAILS, and other non-Java applications.
|
||||
* Javascript/HTML 5 adapter for pure Javascript apps
|
||||
* Session management from admin console
|
||||
* Revocation policies
|
||||
* Password policies
|
||||
* OpenID Connect Support
|
||||
* SAML Support
|
||||
* Token claim and SAML assertion mappings, role name mappings, etc. Ability to configure exactly what information you want in your tokens and SAML documents
|
||||
* IDP brokering or chaining. You can set up Keycloak to be a child IDP to another SAML or OIDC IDP.
|
||||
* Kerberos bridging. Logged in Kerberos users can access Keycloak SAML or OIDC applications via our Kerberos bridge.
|
||||
Building
|
||||
--------
|
||||
|
||||
Please visit [http://keycloak.org](http://keycloak.org) for more information on Keycloak including how to download, documentation,
|
||||
and video tutorials.
|
||||
Ensure you have JDK 7 (or newer), Maven 3.2.1 (or newer) and Git installed
|
||||
|
||||
java -version
|
||||
mvn -version
|
||||
git --version
|
||||
|
||||
First clone the Keycloak repository:
|
||||
|
||||
git clone https://github.com/keycloak/keycloak.git
|
||||
cd keycloak
|
||||
|
||||
To build Keycloak run:
|
||||
|
||||
mvn install
|
||||
|
||||
This will build all modules and run the testsuite.
|
||||
|
||||
To build the distribution run:
|
||||
|
||||
mvn install -Pdistribution
|
||||
|
||||
Once completed you will find distribution archives in `distribution`.
|
||||
|
||||
|
||||
Starting Keycloak
|
||||
-----------------
|
||||
|
||||
To start Keycloak during development first build as specficied above, then run:
|
||||
|
||||
mvn -f testsuite/integration/pom.xml exec:java -Pkeycloak-server
|
||||
|
||||
|
||||
To start Keycloak from the appliance distribution first build the distribution it as specified above, then run:
|
||||
|
||||
tar xfz distribution/appliance-dist/target/keycloak-appliance-dist-all-<VERSION>.tar.gz
|
||||
cd keycloak-appliance-dist-all-<VERSION>/keycloak
|
||||
bin/standalone.sh
|
||||
|
||||
To stop the server press `Ctrl + C`.
|
||||
|
||||
|
||||
Contributing
|
||||
------------
|
||||
|
||||
* See [Hacking on Keycloak](misc/HackingOnKeycloak.md)
|
||||
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
* [User Guide, Admin REST API and Javadocs](http://keycloak.jboss.org/docs)
|
||||
* Developer documentation
|
||||
* [Hacking on Keycloak](misc/HackingOnKeycloak.md) - how to become a Keycloak contributor
|
||||
* [Testsuite](misc/Testsuite.md) - details about testsuite, but also how to quickly run Keycloak during development and a few test tools (OTP generation, LDAP server, Mail server)
|
||||
* [Database Testing](misc/DatabaseTesting.md) - how to do testing of Keycloak on different databases
|
||||
* [Updating Database](misc/UpdatingDatabaseSchema.md) - how to change the Keycloak database
|
||||
* [Release Process](misc/ReleaseProcess.md) - how to release Keycloak
|
||||
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
* [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0)
|
||||
+50
-53
@@ -1,53 +1,50 @@
|
||||
<?xml version="1.0"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>1.2.0.Beta1-SNAPSHOT</version>
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>keycloak-broker-core</artifactId>
|
||||
<name>Keycloak Broker Core</name>
|
||||
<description/>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-core</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-model-api</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-events-api</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jboss.resteasy</groupId>
|
||||
<artifactId>resteasy-jaxrs</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>log4j</groupId>
|
||||
<artifactId>log4j</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-simple</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
<?xml version="1.0"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>1.4.0.Final</version>
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>keycloak-broker-core</artifactId>
|
||||
<name>Keycloak Broker Core</name>
|
||||
<description/>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-model-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-events-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jboss.resteasy</groupId>
|
||||
<artifactId>resteasy-jaxrs</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>log4j</groupId>
|
||||
<artifactId>log4j</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-simple</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -18,8 +18,11 @@
|
||||
package org.keycloak.broker.provider;
|
||||
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.models.ClientSessionModel;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
@@ -55,8 +58,38 @@ public abstract class AbstractIdentityProvider<C extends IdentityProviderModel>
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response performLogin(AuthenticationRequest request) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response keycloakInitiatedBrowserLogout(UserSessionModel userSession, UriInfo uriInfo, RealmModel realm) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void backchannelLogout(UserSessionModel userSession, UriInfo uriInfo, RealmModel realm) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void attachUserSession(UserSessionModel userSession, ClientSessionModel clientSession, BrokeredIdentityContext context) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, BrokeredIdentityContext context) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, BrokeredIdentityContext context) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, BrokeredIdentityContext context) {
|
||||
|
||||
}
|
||||
}
|
||||
+2
@@ -18,8 +18,10 @@
|
||||
package org.keycloak.broker.provider;
|
||||
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.RealmModel;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.HashMap;
|
||||
|
||||
Executable
+38
@@ -0,0 +1,38 @@
|
||||
package org.keycloak.broker.provider;
|
||||
|
||||
import org.keycloak.broker.provider.IdentityProviderMapper;
|
||||
import org.keycloak.models.IdentityProviderMapperModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.RealmModel;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public abstract class AbstractIdentityProviderMapper implements IdentityProviderMapper {
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public IdentityProviderMapper create(KeycloakSession session) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(org.keycloak.Config.Scope config) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
|
||||
}
|
||||
}
|
||||
+98
-43
@@ -17,25 +17,34 @@
|
||||
*/
|
||||
package org.keycloak.broker.provider;
|
||||
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* <p>Represents all identity information obtained from an {@link IdentityProvider} after a
|
||||
* <p>Represents all identity information obtained from an {@link org.keycloak.broker.provider.IdentityProvider} after a
|
||||
* successful authentication.</p>
|
||||
*
|
||||
* @author Pedro Igor
|
||||
*/
|
||||
public class FederatedIdentity {
|
||||
public class BrokeredIdentityContext {
|
||||
|
||||
private String id;
|
||||
private String username;
|
||||
private String modelUsername;
|
||||
private String email;
|
||||
private String firstName;
|
||||
private String lastName;
|
||||
private String email;
|
||||
private String token;
|
||||
private String identityProviderId;
|
||||
private String brokerSessionId;
|
||||
private String brokerUserId;
|
||||
private String code;
|
||||
private String token;
|
||||
private IdentityProviderModel idpConfig;
|
||||
private IdentityProvider idp;
|
||||
private Map<String, Object> contextData = new HashMap<>();
|
||||
|
||||
public FederatedIdentity(String id) {
|
||||
public BrokeredIdentityContext(String id) {
|
||||
if (id == null) {
|
||||
throw new RuntimeException("No identifier provider for identity.");
|
||||
}
|
||||
@@ -51,6 +60,11 @@ public class FederatedIdentity {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Username in remote idp
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
@@ -59,24 +73,17 @@ public class FederatedIdentity {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getFirstName() {
|
||||
return firstName;
|
||||
/**
|
||||
* username to store in UserModel
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public String getModelUsername() {
|
||||
return modelUsername;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
if (name != null) {
|
||||
int i = name.lastIndexOf(' ');
|
||||
if (i != -1) {
|
||||
firstName = name.substring(0, i);
|
||||
lastName = name.substring(i + 1);
|
||||
} else {
|
||||
firstName = name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public String getLastName() {
|
||||
return lastName;
|
||||
public void setModelUsername(String modelUsername) {
|
||||
this.modelUsername = modelUsername;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
@@ -87,23 +94,6 @@ public class FederatedIdentity {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
|
||||
public void setToken(String token) {
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
public String getToken() {
|
||||
return this.token;
|
||||
}
|
||||
|
||||
public String getIdentityProviderId() {
|
||||
return this.identityProviderId;
|
||||
}
|
||||
|
||||
public void setIdentityProviderId(String identityProviderId) {
|
||||
this.identityProviderId = identityProviderId;
|
||||
}
|
||||
|
||||
public String getBrokerSessionId() {
|
||||
return brokerSessionId;
|
||||
}
|
||||
@@ -120,16 +110,81 @@ public class FederatedIdentity {
|
||||
this.brokerUserId = brokerUserId;
|
||||
}
|
||||
|
||||
public String getToken() {
|
||||
return token;
|
||||
}
|
||||
|
||||
public void setToken(String token) {
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public void setCode(String code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public IdentityProviderModel getIdpConfig() {
|
||||
return idpConfig;
|
||||
}
|
||||
|
||||
public void setIdpConfig(IdentityProviderModel idpConfig) {
|
||||
this.idpConfig = idpConfig;
|
||||
}
|
||||
|
||||
public IdentityProvider getIdp() {
|
||||
return idp;
|
||||
}
|
||||
|
||||
public void setIdp(IdentityProvider idp) {
|
||||
this.idp = idp;
|
||||
}
|
||||
|
||||
public Map<String, Object> getContextData() {
|
||||
return contextData;
|
||||
}
|
||||
|
||||
public void setContextData(Map<String, Object> contextData) {
|
||||
this.contextData = contextData;
|
||||
}
|
||||
|
||||
public String getFirstName() {
|
||||
return firstName;
|
||||
}
|
||||
|
||||
public void setFirstName(String firstName) {
|
||||
this.firstName = firstName;
|
||||
}
|
||||
|
||||
public String getLastName() {
|
||||
return lastName;
|
||||
}
|
||||
|
||||
public void setLastName(String lastName) {
|
||||
this.lastName = lastName;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
if (name != null) {
|
||||
int i = name.lastIndexOf(' ');
|
||||
if (i != -1) {
|
||||
firstName = name.substring(0, i);
|
||||
lastName = name.substring(i + 1);
|
||||
} else {
|
||||
firstName = name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "{" +
|
||||
"id='" + id + '\'' +
|
||||
", username='" + username + '\'' +
|
||||
", firstName='" + firstName + '\'' +
|
||||
", lastName='" + lastName + '\'' +
|
||||
", email='" + email + '\'' +
|
||||
", token='" + token + '\'' +
|
||||
", identityProviderId='" + identityProviderId + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
package org.keycloak.broker.provider;
|
||||
|
||||
import org.keycloak.broker.provider.AbstractIdentityProviderMapper;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.broker.provider.IdentityBrokerException;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.IdentityProviderMapperModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class HardcodedRoleMapper extends AbstractIdentityProviderMapper {
|
||||
public static final String ROLE = "role";
|
||||
protected static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
|
||||
|
||||
static {
|
||||
ProviderConfigProperty property;
|
||||
property = new ProviderConfigProperty();
|
||||
property.setName(ROLE);
|
||||
property.setLabel("Role");
|
||||
property.setHelpText("Role to grant to user. Click 'Select Role' button to browse roles, or just type it in the textbox. To reference an application role the syntax is appname.approle, i.e. myapp.myrole");
|
||||
property.setType(ProviderConfigProperty.ROLE_TYPE);
|
||||
configProperties.add(property);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static String[] parseRole(String role) {
|
||||
int scopeIndex = role.lastIndexOf('.');
|
||||
if (scopeIndex > -1) {
|
||||
String appName = role.substring(0, scopeIndex);
|
||||
role = role.substring(scopeIndex + 1);
|
||||
String[] rtn = {appName, role};
|
||||
return rtn;
|
||||
} else {
|
||||
String[] rtn = {null, role};
|
||||
return rtn;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public static RoleModel getRoleFromString(RealmModel realm, String roleName) {
|
||||
String[] parsedRole = parseRole(roleName);
|
||||
RoleModel role = null;
|
||||
if (parsedRole[0] == null) {
|
||||
role = realm.getRole(parsedRole[1]);
|
||||
} else {
|
||||
ClientModel client = realm.getClientByClientId(parsedRole[0]);
|
||||
role = client.getRole(parsedRole[1]);
|
||||
}
|
||||
return role;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return configProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayCategory() {
|
||||
return "Role Importer";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "Hardcoded Role";
|
||||
}
|
||||
|
||||
public static final String[] COMPATIBLE_PROVIDERS = {ANY_PROVIDER};
|
||||
|
||||
|
||||
public static final String PROVIDER_ID = "oidc-hardcoded-role-idp-mapper";
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getCompatibleProviders() {
|
||||
return COMPATIBLE_PROVIDERS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
String roleName = mapperModel.getConfig().get(ROLE);
|
||||
RoleModel role = getRoleFromString(realm, roleName);
|
||||
if (role == null) throw new IdentityBrokerException("Unable to find role: " + roleName);
|
||||
user.grantRole(role);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "When user is imported from provider, hardcode a role mapping for it.";
|
||||
}
|
||||
}
|
||||
@@ -18,35 +18,45 @@
|
||||
package org.keycloak.broker.provider;
|
||||
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.models.ClientSessionModel;
|
||||
import org.keycloak.models.FederatedIdentityModel;
|
||||
import org.keycloak.models.IdentityProviderMapperModel;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.provider.Provider;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author Pedro Igor
|
||||
*/
|
||||
public interface IdentityProvider<C extends IdentityProviderModel> extends Provider {
|
||||
|
||||
public interface AuthenticationCallback {
|
||||
interface AuthenticationCallback {
|
||||
/**
|
||||
* This method should be called by provider after the JAXRS callback endpoint has finished authentication
|
||||
* with the remote IDP
|
||||
*
|
||||
* @param userNotes notes to add to the UserSessionModel
|
||||
* @param identityProviderConfig provider config
|
||||
* @param federatedIdentity federated identity
|
||||
* @param code relayState or state parameter used to identity the client session
|
||||
* @param context
|
||||
* @return
|
||||
*/
|
||||
public Response authenticated(Map<String, String> userNotes, IdentityProviderModel identityProviderConfig, FederatedIdentity federatedIdentity, String code);
|
||||
Response authenticated(BrokeredIdentityContext context);
|
||||
|
||||
Response cancelled(String code);
|
||||
|
||||
Response error(String code, String message);
|
||||
}
|
||||
|
||||
|
||||
void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, BrokeredIdentityContext context);
|
||||
void attachUserSession(UserSessionModel userSession, ClientSessionModel clientSession, BrokeredIdentityContext context);
|
||||
void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, BrokeredIdentityContext context);
|
||||
void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, BrokeredIdentityContext context);
|
||||
|
||||
/**
|
||||
* JAXRS callback endpoint for when the remote IDP wants to callback to keycloak.
|
||||
*
|
||||
@@ -58,17 +68,11 @@ public interface IdentityProvider<C extends IdentityProviderModel> extends Provi
|
||||
* <p>Initiates the authentication process by sending an authentication request to an identity provider. This method is called
|
||||
* only once during the authentication.</p>
|
||||
*
|
||||
* <p>Depending on how the authentication is performed, this method may redirect the user to the identity provider for authentication.
|
||||
* In this case, the response would contain a {@link javax.ws.rs.core.Response} that will be used to redirect the user.</p>
|
||||
*
|
||||
* <p>However, if the authentication flow does not require a redirect to the identity provider (eg.: simple challenge/response mechanism), this method may return a response containing
|
||||
* a {@link FederatedIdentity} representing the identity information for an user. In this case, the authentication flow stops.</p>
|
||||
*
|
||||
* @param request The initial authentication request. Contains all the contextual information in order to build an authentication request to the
|
||||
* identity provider.
|
||||
* @return
|
||||
*/
|
||||
Response handleRequest(AuthenticationRequest request);
|
||||
Response performLogin(AuthenticationRequest request);
|
||||
|
||||
/**
|
||||
* <p>Returns a {@link javax.ws.rs.core.Response} containing the token previously stored during the authentication process for a
|
||||
@@ -79,6 +83,8 @@ public interface IdentityProvider<C extends IdentityProviderModel> extends Provi
|
||||
*/
|
||||
Response retrieveToken(FederatedIdentityModel identity);
|
||||
|
||||
void backchannelLogout(UserSessionModel userSession, UriInfo uriInfo, RealmModel realm);
|
||||
|
||||
/**
|
||||
* Called when a Keycloak application initiates a logout through the browser. This is expected to do a logout
|
||||
* with the IDP
|
||||
|
||||
Regular → Executable
+2
@@ -18,6 +18,8 @@
|
||||
package org.keycloak.broker.provider;
|
||||
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
package org.keycloak.broker.provider;
|
||||
|
||||
import org.keycloak.models.IdentityProviderMapperModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.provider.ConfiguredProvider;
|
||||
import org.keycloak.provider.Provider;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public interface IdentityProviderMapper extends Provider, ProviderFactory<IdentityProviderMapper>,ConfiguredProvider {
|
||||
public static final String ANY_PROVIDER = "*";
|
||||
|
||||
String[] getCompatibleProviders();
|
||||
String getDisplayCategory();
|
||||
String getDisplayType();
|
||||
|
||||
/**
|
||||
* Called to determine what keycloak username and email to use to process the login request from the external IDP
|
||||
* Usually used to map BrokeredIdentityContet.username or email.
|
||||
*
|
||||
* @param session
|
||||
* @param realm
|
||||
* @param mapperModel
|
||||
* @param context
|
||||
*/
|
||||
void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context);
|
||||
|
||||
/**
|
||||
* Called after UserModel is created for first time for this user.
|
||||
*
|
||||
* @param session
|
||||
* @param realm
|
||||
* @param user
|
||||
* @param mapperModel
|
||||
* @param context
|
||||
*/
|
||||
void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context);
|
||||
|
||||
/**
|
||||
* Called when this user has logged in before and has already been imported.
|
||||
*
|
||||
* @param session
|
||||
* @param realm
|
||||
* @param user
|
||||
* @param mapperModel
|
||||
* @param context
|
||||
*/
|
||||
void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context);
|
||||
|
||||
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package org.keycloak.broker.provider;
|
||||
|
||||
import org.keycloak.provider.Provider;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
import org.keycloak.provider.Spi;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public class IdentityProviderMapperSpi implements Spi {
|
||||
|
||||
@Override
|
||||
public boolean isInternal() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "identity-provider-mapper";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends Provider> getProviderClass() {
|
||||
return IdentityProviderMapper.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends ProviderFactory> getProviderFactoryClass() {
|
||||
return IdentityProviderMapper.class;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -28,6 +28,11 @@ public class IdentityProviderSpi implements Spi {
|
||||
|
||||
public static final String IDENTITY_PROVIDER_SPI_NAME = "identity_provider";
|
||||
|
||||
@Override
|
||||
public boolean isInternal() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return IDENTITY_PROVIDER_SPI_NAME;
|
||||
|
||||
Regular → Executable
+98
-11
@@ -1,7 +1,4 @@
|
||||
package org.keycloak.broker.oidc.util;
|
||||
|
||||
import org.codehaus.jackson.JsonNode;
|
||||
import org.codehaus.jackson.map.ObjectMapper;
|
||||
package org.keycloak.broker.provider.util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
@@ -21,14 +18,13 @@ import java.util.zip.GZIPInputStream;
|
||||
*/
|
||||
public class SimpleHttp {
|
||||
|
||||
private static ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
private String url;
|
||||
private String method;
|
||||
private Map<String, String> headers;
|
||||
private Map<String, String> params;
|
||||
|
||||
private SimpleHttp(String url, String method) {
|
||||
protected SimpleHttp(String url, String method) {
|
||||
this.url = url;
|
||||
this.method = method;
|
||||
}
|
||||
@@ -57,9 +53,6 @@ public class SimpleHttp {
|
||||
return this;
|
||||
}
|
||||
|
||||
public JsonNode asJson() throws IOException {
|
||||
return mapper.readTree(asString());
|
||||
}
|
||||
|
||||
public String asString() throws IOException {
|
||||
boolean get = method.equals("GET");
|
||||
@@ -126,11 +119,105 @@ public class SimpleHttp {
|
||||
return toString(is);
|
||||
} finally {
|
||||
if (os != null) {
|
||||
os.close();
|
||||
try {
|
||||
os.close();
|
||||
} catch (IOException e) {
|
||||
}
|
||||
}
|
||||
|
||||
if (is != null) {
|
||||
is.close();
|
||||
try {
|
||||
is.close();
|
||||
} catch (IOException e) {
|
||||
}
|
||||
}
|
||||
if (connection != null) {
|
||||
try {
|
||||
connection.disconnect();
|
||||
} catch (Exception e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int asStatus() throws IOException {
|
||||
boolean get = method.equals("GET");
|
||||
boolean post = method.equals("POST");
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (get) {
|
||||
sb.append(url);
|
||||
}
|
||||
|
||||
if (params != null) {
|
||||
boolean f = true;
|
||||
for (Map.Entry<String, String> p : params.entrySet()) {
|
||||
if (f) {
|
||||
f = false;
|
||||
if (get) {
|
||||
sb.append("?");
|
||||
}
|
||||
} else {
|
||||
sb.append("&");
|
||||
}
|
||||
sb.append(URLEncoder.encode(p.getKey(), "UTF-8"));
|
||||
sb.append("=");
|
||||
sb.append(URLEncoder.encode(p.getValue(), "UTF-8"));
|
||||
}
|
||||
}
|
||||
|
||||
if (get) {
|
||||
url = sb.toString();
|
||||
}
|
||||
|
||||
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
|
||||
OutputStream os = null;
|
||||
InputStream is = null;
|
||||
|
||||
try {
|
||||
connection.setRequestMethod(method);
|
||||
|
||||
if (headers != null) {
|
||||
for (Map.Entry<String, String> h : headers.entrySet()) {
|
||||
connection.setRequestProperty(h.getKey(), h.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
if (post) {
|
||||
String data = sb.toString();
|
||||
|
||||
connection.setDoOutput(true);
|
||||
connection.setRequestMethod("POST");
|
||||
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
|
||||
connection.setRequestProperty("Content-Length", String.valueOf(data.length()));
|
||||
|
||||
os = connection.getOutputStream();
|
||||
os.write(data.getBytes());
|
||||
} else {
|
||||
connection.setDoOutput(false);
|
||||
}
|
||||
|
||||
is = connection.getInputStream();
|
||||
return connection.getResponseCode();
|
||||
} finally {
|
||||
if (os != null) {
|
||||
try {
|
||||
os.close();
|
||||
} catch (IOException e) {
|
||||
}
|
||||
}
|
||||
|
||||
if (is != null) {
|
||||
try {
|
||||
is.close();
|
||||
} catch (IOException e) {
|
||||
}
|
||||
}
|
||||
if (connection != null) {
|
||||
try {
|
||||
connection.disconnect();
|
||||
} catch (Exception e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
broker/core/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderMapper
Executable
+1
@@ -0,0 +1 @@
|
||||
org.keycloak.broker.provider.HardcodedRoleMapper
|
||||
@@ -1 +1,2 @@
|
||||
org.keycloak.broker.provider.IdentityProviderSpi
|
||||
org.keycloak.broker.provider.IdentityProviderSpi
|
||||
org.keycloak.broker.provider.IdentityProviderMapperSpi
|
||||
+58
-57
@@ -1,57 +1,58 @@
|
||||
<?xml version="1.0"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>1.2.0.Beta1-SNAPSHOT</version>
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>keycloak-broker-oidc</artifactId>
|
||||
<name>Keycloak Broker - OpenID Connect Identity Provider</name>
|
||||
<description/>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-core</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-broker-core</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.codehaus.jackson</groupId>
|
||||
<artifactId>jackson-core-asl</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.codehaus.jackson</groupId>
|
||||
<artifactId>jackson-mapper-asl</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-events-api</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-services</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jboss.logging</groupId>
|
||||
<artifactId>jboss-logging</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
<?xml version="1.0"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>1.4.0.Final</version>
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>keycloak-broker-oidc</artifactId>
|
||||
<name>Keycloak Broker - OpenID Connect Identity Provider</name>
|
||||
<description/>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-broker-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.codehaus.jackson</groupId>
|
||||
<artifactId>jackson-core-asl</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.codehaus.jackson</groupId>
|
||||
<artifactId>jackson-mapper-asl</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-events-api</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-services</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jboss.logging</groupId>
|
||||
<artifactId>jboss-logging</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
+46
-21
@@ -22,10 +22,10 @@ import org.codehaus.jackson.map.ObjectMapper;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.ClientConnection;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.broker.oidc.util.SimpleHttp;
|
||||
import org.keycloak.broker.provider.util.SimpleHttp;
|
||||
import org.keycloak.broker.provider.AbstractIdentityProvider;
|
||||
import org.keycloak.broker.provider.AuthenticationRequest;
|
||||
import org.keycloak.broker.provider.FederatedIdentity;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.broker.provider.IdentityBrokerException;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
@@ -33,8 +33,8 @@ import org.keycloak.events.EventType;
|
||||
import org.keycloak.models.FederatedIdentityModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.services.ErrorPage;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.services.resources.flows.Flows;
|
||||
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.QueryParam;
|
||||
@@ -45,8 +45,6 @@ import javax.ws.rs.core.UriBuilder;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@@ -60,6 +58,7 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
|
||||
public static final String FEDERATED_ACCESS_TOKEN = "FEDERATED_ACCESS_TOKEN";
|
||||
public static final String FEDERATED_REFRESH_TOKEN = "FEDERATED_REFRESH_TOKEN";
|
||||
public static final String FEDERATED_TOKEN_EXPIRATION = "FEDERATED_TOKEN_EXPIRATION";
|
||||
public static final String ACCESS_DENIED = "access_denied";
|
||||
protected static ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
public static final String OAUTH2_PARAMETER_ACCESS_TOKEN = "access_token";
|
||||
@@ -87,7 +86,7 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response handleRequest(AuthenticationRequest request) {
|
||||
public Response performLogin(AuthenticationRequest request) {
|
||||
try {
|
||||
URI authorizationUrl = createAuthorizationUrl(request).build();
|
||||
|
||||
@@ -108,11 +107,22 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
|
||||
}
|
||||
|
||||
protected String extractTokenFromResponse(String response, String tokenName) {
|
||||
if(response == null)
|
||||
return null;
|
||||
|
||||
if (response.startsWith("{")) {
|
||||
try {
|
||||
return mapper.readTree(response).get(tokenName).getTextValue();
|
||||
JsonNode node = mapper.readTree(response);
|
||||
if(node.has(tokenName)){
|
||||
String s = node.get(tokenName).getTextValue();
|
||||
if(s == null || s.trim().isEmpty())
|
||||
return null;
|
||||
return s;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new IdentityBrokerException("Could not extract token [" + tokenName + "] from response [" + response + "].", e);
|
||||
throw new IdentityBrokerException("Could not extract token [" + tokenName + "] from response [" + response + "] due: " + e.getMessage(), e);
|
||||
}
|
||||
} else {
|
||||
Matcher matcher = Pattern.compile(tokenName + "=([^&]+)").matcher(response);
|
||||
@@ -125,22 +135,21 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
|
||||
return null;
|
||||
}
|
||||
|
||||
protected FederatedIdentity getFederatedIdentity(Map<String, String> notes, String response) {
|
||||
protected BrokeredIdentityContext getFederatedIdentity(String response) {
|
||||
String accessToken = extractTokenFromResponse(response, OAUTH2_PARAMETER_ACCESS_TOKEN);
|
||||
|
||||
if (accessToken == null) {
|
||||
throw new IdentityBrokerException("No access token from server.");
|
||||
throw new IdentityBrokerException("No access token available in OAuth server response: " + response);
|
||||
}
|
||||
|
||||
return doGetFederatedIdentity(accessToken);
|
||||
}
|
||||
|
||||
|
||||
protected FederatedIdentity doGetFederatedIdentity(String accessToken) {
|
||||
protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) {
|
||||
return null;
|
||||
}
|
||||
|
||||
;
|
||||
|
||||
protected UriBuilder createAuthorizationUrl(AuthenticationRequest request) {
|
||||
return UriBuilder.fromUri(getConfig().getAuthorizationUrl())
|
||||
@@ -151,9 +160,20 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
|
||||
.queryParam(OAUTH2_PARAMETER_REDIRECT_URI, request.getRedirectUri());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get JSON property as text. JSON numbers and booleans are converted to text. Empty string is converted to null.
|
||||
*
|
||||
* @param jsonNode to get property from
|
||||
* @param name of property to get
|
||||
* @return string value of the property or null.
|
||||
*/
|
||||
protected String getJsonProperty(JsonNode jsonNode, String name) {
|
||||
if (jsonNode.has(name)) {
|
||||
return jsonNode.get(name).asText();
|
||||
if (jsonNode.has(name) && !jsonNode.get(name).isNull()) {
|
||||
String s = jsonNode.get(name).asText();
|
||||
if(s != null && !s.isEmpty())
|
||||
return s;
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -194,9 +214,11 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
|
||||
@QueryParam(OAuth2Constants.ERROR) String error) {
|
||||
if (error != null) {
|
||||
//logger.error("Failed " + getConfig().getAlias() + " broker login: " + error);
|
||||
event.event(EventType.LOGIN);
|
||||
event.error(error);
|
||||
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR);
|
||||
if (error.equals(ACCESS_DENIED)) {
|
||||
return callback.cancelled(state);
|
||||
} else {
|
||||
return callback.error(state, Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -204,21 +226,24 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
|
||||
if (authorizationCode != null) {
|
||||
String response = generateTokenRequest(authorizationCode).asString();
|
||||
|
||||
HashMap<String, String> userNotes = new HashMap<String, String>();
|
||||
FederatedIdentity federatedIdentity = getFederatedIdentity(userNotes, response);
|
||||
BrokeredIdentityContext federatedIdentity = getFederatedIdentity(response);
|
||||
|
||||
if (getConfig().isStoreToken()) {
|
||||
federatedIdentity.setToken(response);
|
||||
}
|
||||
|
||||
return callback.authenticated(userNotes, getConfig(), federatedIdentity, state);
|
||||
federatedIdentity.setCode(state);
|
||||
federatedIdentity.setIdpConfig(getConfig());
|
||||
federatedIdentity.setIdp(AbstractOAuth2IdentityProvider.this);
|
||||
|
||||
return callback.authenticated(federatedIdentity);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to make identity provider oauth callback", e);
|
||||
}
|
||||
event.event(EventType.LOGIN);
|
||||
event.error(Errors.IDENTITY_PROVIDER_LOGIN_FAILURE);
|
||||
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR);
|
||||
return ErrorPage.error(session, Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR);
|
||||
}
|
||||
|
||||
public SimpleHttp generateTokenRequest(String authorizationCode) {
|
||||
|
||||
+114
-100
@@ -1,100 +1,114 @@
|
||||
package org.keycloak.broker.oidc;
|
||||
|
||||
import org.keycloak.broker.oidc.util.SimpleHttp;
|
||||
import org.keycloak.constants.AdapterConstants;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.jose.jws.JWSInput;
|
||||
import org.keycloak.jose.jws.crypto.RSAProvider;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.representations.adapters.action.AdminAction;
|
||||
import org.keycloak.representations.adapters.action.LogoutAction;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
import org.keycloak.util.PemUtils;
|
||||
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.io.IOException;
|
||||
import java.security.PublicKey;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class KeycloakOIDCIdentityProvider extends OIDCIdentityProvider {
|
||||
|
||||
public KeycloakOIDCIdentityProvider(OIDCIdentityProviderConfig config) {
|
||||
super(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object callback(RealmModel realm, AuthenticationCallback callback, EventBuilder event) {
|
||||
return new KeycloakEndpoint(callback, realm, event);
|
||||
}
|
||||
|
||||
protected class KeycloakEndpoint extends OIDCEndpoint {
|
||||
public KeycloakEndpoint(AuthenticationCallback callback, RealmModel realm, EventBuilder event) {
|
||||
super(callback, realm, event);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path(AdapterConstants.K_LOGOUT)
|
||||
public Response backchannelLogout(String input) {
|
||||
JWSInput token = new JWSInput(input);
|
||||
PublicKey key = getExternalIdpKey();
|
||||
if (key != null) {
|
||||
if (!verify(token, key)) {
|
||||
logger.warn("Failed to verify logout request");
|
||||
return Response.status(400).build();
|
||||
}
|
||||
}
|
||||
LogoutAction action = null;
|
||||
try {
|
||||
action = JsonSerialization.readValue(token.getContent(), LogoutAction.class);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
if (!validateAction(action)) return Response.status(400).build();
|
||||
if (action.getKeycloakSessionIds() != null) {
|
||||
for (String sessionId : action.getKeycloakSessionIds()) {
|
||||
String brokerSessionId = getConfig().getAlias() + "." + sessionId;
|
||||
UserSessionModel userSession = session.sessions().getUserSessionByBrokerSessionId(realm, brokerSessionId);
|
||||
if (userSession != null) {
|
||||
AuthenticationManager.backchannelLogout(session, realm, userSession, uriInfo, clientConnection, headers);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
protected boolean validateAction(AdminAction action) {
|
||||
if (!action.validate()) {
|
||||
logger.warn("admin request failed, not validated" + action.getAction());
|
||||
return false;
|
||||
}
|
||||
if (action.isExpired()) {
|
||||
logger.warn("admin request failed, expired token");
|
||||
return false;
|
||||
}
|
||||
if (!getConfig().getClientId().equals(action.getResource())) {
|
||||
logger.warn("Resource name does not match");
|
||||
return false;
|
||||
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SimpleHttp generateTokenRequest(String authorizationCode) {
|
||||
return super.generateTokenRequest(authorizationCode)
|
||||
.param(AdapterConstants.APPLICATION_SESSION_STATE, "n/a"); // hack to get backchannel logout to work
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
package org.keycloak.broker.oidc;
|
||||
|
||||
import org.keycloak.broker.provider.util.SimpleHttp;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.constants.AdapterConstants;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.jose.jws.JWSInput;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.representations.AccessTokenResponse;
|
||||
import org.keycloak.representations.JsonWebToken;
|
||||
import org.keycloak.representations.adapters.action.AdminAction;
|
||||
import org.keycloak.representations.adapters.action.LogoutAction;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.io.IOException;
|
||||
import java.security.PublicKey;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class KeycloakOIDCIdentityProvider extends OIDCIdentityProvider {
|
||||
|
||||
public static final String VALIDATED_ACCESS_TOKEN = "VALIDATED_ACCESS_TOKEN";
|
||||
|
||||
public KeycloakOIDCIdentityProvider(OIDCIdentityProviderConfig config) {
|
||||
super(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object callback(RealmModel realm, AuthenticationCallback callback, EventBuilder event) {
|
||||
return new KeycloakEndpoint(callback, realm, event);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processAccessTokenResponse(BrokeredIdentityContext context, PublicKey idpKey, AccessTokenResponse response) {
|
||||
JsonWebToken access = validateToken(idpKey, response.getToken());
|
||||
context.getContextData().put(VALIDATED_ACCESS_TOKEN, access);
|
||||
}
|
||||
|
||||
protected class KeycloakEndpoint extends OIDCEndpoint {
|
||||
public KeycloakEndpoint(AuthenticationCallback callback, RealmModel realm, EventBuilder event) {
|
||||
super(callback, realm, event);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path(AdapterConstants.K_LOGOUT)
|
||||
public Response backchannelLogout(String input) {
|
||||
JWSInput token = new JWSInput(input);
|
||||
PublicKey key = getExternalIdpKey();
|
||||
if (key != null) {
|
||||
if (!verify(token, key)) {
|
||||
logger.warn("Failed to verify logout request");
|
||||
return Response.status(400).build();
|
||||
}
|
||||
}
|
||||
LogoutAction action = null;
|
||||
try {
|
||||
action = JsonSerialization.readValue(token.getContent(), LogoutAction.class);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
if (!validateAction(action)) return Response.status(400).build();
|
||||
if (action.getKeycloakSessionIds() != null) {
|
||||
for (String sessionId : action.getKeycloakSessionIds()) {
|
||||
String brokerSessionId = getConfig().getAlias() + "." + sessionId;
|
||||
UserSessionModel userSession = session.sessions().getUserSessionByBrokerSessionId(realm, brokerSessionId);
|
||||
if (userSession != null
|
||||
&& userSession.getState() != UserSessionModel.State.LOGGING_OUT
|
||||
&& userSession.getState() != UserSessionModel.State.LOGGED_OUT
|
||||
) {
|
||||
AuthenticationManager.backchannelLogout(session, realm, userSession, uriInfo, clientConnection, headers, false);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
protected boolean validateAction(AdminAction action) {
|
||||
if (!action.validate()) {
|
||||
logger.warn("admin request failed, not validated" + action.getAction());
|
||||
return false;
|
||||
}
|
||||
if (action.isExpired()) {
|
||||
logger.warn("admin request failed, expired token");
|
||||
return false;
|
||||
}
|
||||
if (!getConfig().getClientId().equals(action.getResource())) {
|
||||
logger.warn("Resource name does not match");
|
||||
return false;
|
||||
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SimpleHttp generateTokenRequest(String authorizationCode) {
|
||||
return super.generateTokenRequest(authorizationCode)
|
||||
.param(AdapterConstants.CLIENT_SESSION_STATE, "n/a"); // hack to get backchannel logout to work
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
+7
-4
@@ -17,13 +17,15 @@
|
||||
*/
|
||||
package org.keycloak.broker.oidc;
|
||||
|
||||
import org.keycloak.broker.oidc.mappers.UsernameTemplateMapper;
|
||||
import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
|
||||
import org.keycloak.models.IdentityProviderMapperModel;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
@@ -53,4 +55,5 @@ public class KeycloakOIDCIdentityProviderFactory extends AbstractIdentityProvide
|
||||
return OIDCIdentityProviderFactory.parseOIDCConfig(inputStream);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -19,25 +19,28 @@ package org.keycloak.broker.oidc;
|
||||
|
||||
import org.codehaus.jackson.JsonNode;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.RSATokenVerifier;
|
||||
import org.keycloak.broker.oidc.util.SimpleHttp;
|
||||
import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper;
|
||||
import org.keycloak.broker.oidc.util.JsonSimpleHttp;
|
||||
import org.keycloak.broker.provider.util.SimpleHttp;
|
||||
import org.keycloak.broker.provider.AuthenticationRequest;
|
||||
import org.keycloak.broker.provider.FederatedIdentity;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.broker.provider.IdentityBrokerException;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.jose.jws.JWSInput;
|
||||
import org.keycloak.jose.jws.crypto.RSAProvider;
|
||||
import org.keycloak.models.ClientSessionModel;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.representations.AccessTokenResponse;
|
||||
import org.keycloak.representations.IDToken;
|
||||
import org.keycloak.representations.JsonWebToken;
|
||||
import org.keycloak.services.ErrorPage;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.services.resources.IdentityBrokerService;
|
||||
import org.keycloak.services.resources.RealmsResource;
|
||||
import org.keycloak.services.resources.flows.Flows;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
import org.keycloak.util.PemUtils;
|
||||
|
||||
@@ -48,9 +51,9 @@ import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriBuilder;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.PublicKey;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author Pedro Igor
|
||||
@@ -61,6 +64,9 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
||||
public static final String OAUTH2_PARAMETER_PROMPT = "prompt";
|
||||
public static final String SCOPE_OPENID = "openid";
|
||||
public static final String FEDERATED_ID_TOKEN = "FEDERATED_ID_TOKEN";
|
||||
public static final String USER_INFO = "UserInfo";
|
||||
public static final String FEDERATED_ACCESS_TOKEN_RESPONSE = "FEDERATED_ACCESS_TOKEN_RESPONSE";
|
||||
public static final String VALIDATED_ID_TOKEN = "VALIDATED_ID_TOKEN";
|
||||
|
||||
public OIDCIdentityProvider(OIDCIdentityProviderConfig config) {
|
||||
super(config);
|
||||
@@ -92,13 +98,6 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
||||
|
||||
}
|
||||
|
||||
protected boolean verify(JWSInput jws, PublicKey key) {
|
||||
if (key == null) return true;
|
||||
if (!getConfig().isValidateSignature()) return true;
|
||||
return RSAProvider.verify(jws, key);
|
||||
|
||||
}
|
||||
|
||||
protected class OIDCEndpoint extends Endpoint {
|
||||
public OIDCEndpoint(AuthenticationCallback callback, RealmModel realm, EventBuilder event) {
|
||||
super(callback, realm, event);
|
||||
@@ -116,33 +115,66 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
||||
EventBuilder event = new EventBuilder(realm, session, clientConnection);
|
||||
event.event(EventType.LOGOUT);
|
||||
event.error(Errors.USER_SESSION_NOT_FOUND);
|
||||
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR);
|
||||
return ErrorPage.error(session, Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR);
|
||||
}
|
||||
if (userSession.getState() != UserSessionModel.State.LOGGING_OUT) {
|
||||
logger.error("usersession in different state");
|
||||
EventBuilder event = new EventBuilder(realm, session, clientConnection);
|
||||
event.event(EventType.LOGOUT);
|
||||
event.error(Errors.USER_SESSION_NOT_FOUND);
|
||||
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.SESSION_NOT_ACTIVE);
|
||||
return ErrorPage.error(session, Messages.SESSION_NOT_ACTIVE);
|
||||
}
|
||||
return AuthenticationManager.finishBrowserLogout(session, realm, userSession, uriInfo, clientConnection, headers);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void backchannelLogout(UserSessionModel userSession, UriInfo uriInfo, RealmModel realm) {
|
||||
if (getConfig().getLogoutUrl() == null || getConfig().getLogoutUrl().trim().equals("") || !getConfig().isBackchannelSupported()) return;
|
||||
String idToken = userSession.getNote(FEDERATED_ID_TOKEN);
|
||||
if (idToken == null) return;
|
||||
backchannelLogout(userSession, idToken);
|
||||
}
|
||||
|
||||
protected void backchannelLogout(UserSessionModel userSession, String idToken) {
|
||||
String sessionId = userSession.getId();
|
||||
UriBuilder logoutUri = UriBuilder.fromUri(getConfig().getLogoutUrl())
|
||||
.queryParam("state", sessionId);
|
||||
logoutUri.queryParam("id_token_hint", idToken);
|
||||
String url = logoutUri.build().toString();
|
||||
try {
|
||||
int status = JsonSimpleHttp.doGet(url).asStatus();
|
||||
boolean success = status >=200 && status < 400;
|
||||
if (!success) {
|
||||
logger.warn("Failed backchannel broker logout to: " + url);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("Failed backchannel broker logout to: " + url, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Response keycloakInitiatedBrowserLogout(UserSessionModel userSession, UriInfo uriInfo, RealmModel realm) {
|
||||
if (getConfig().getLogoutUrl() == null || getConfig().getLogoutUrl().trim().equals("")) return null;
|
||||
UriBuilder logoutUri = UriBuilder.fromUri(getConfig().getLogoutUrl())
|
||||
.queryParam("state", userSession.getId());
|
||||
String idToken = userSession.getNote(FEDERATED_ID_TOKEN);
|
||||
if (idToken != null) logoutUri.queryParam("id_token_hint", idToken);
|
||||
String redirect = RealmsResource.brokerUrl(uriInfo)
|
||||
.path(IdentityBrokerService.class, "getEndpoint")
|
||||
.path(OIDCEndpoint.class, "logoutResponse")
|
||||
.build(realm.getName(), getConfig().getAlias()).toString();
|
||||
logoutUri.queryParam("post_logout_redirect_uri", redirect);
|
||||
return Response.status(302).location(logoutUri.build()).build();
|
||||
if (idToken != null && getConfig().isBackchannelSupported()) {
|
||||
backchannelLogout(userSession, idToken);
|
||||
return null;
|
||||
} else {
|
||||
String sessionId = userSession.getId();
|
||||
UriBuilder logoutUri = UriBuilder.fromUri(getConfig().getLogoutUrl())
|
||||
.queryParam("state", sessionId);
|
||||
if (idToken != null) logoutUri.queryParam("id_token_hint", idToken);
|
||||
String redirect = RealmsResource.brokerUrl(uriInfo)
|
||||
.path(IdentityBrokerService.class, "getEndpoint")
|
||||
.path(OIDCEndpoint.class, "logoutResponse")
|
||||
.build(realm.getName(), getConfig().getAlias()).toString();
|
||||
logoutUri.queryParam("post_logout_redirect_uri", redirect);
|
||||
Response response = Response.status(302).location(logoutUri.build()).build();
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -157,8 +189,12 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
||||
return authorizationUrl;
|
||||
}
|
||||
|
||||
protected void processAccessTokenResponse(BrokeredIdentityContext context, PublicKey idpKey, AccessTokenResponse response) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected FederatedIdentity getFederatedIdentity(Map<String, String> notes, String response) {
|
||||
protected BrokeredIdentityContext getFederatedIdentity(String response) {
|
||||
AccessTokenResponse tokenResponse = null;
|
||||
try {
|
||||
tokenResponse = JsonSerialization.readValue(response, AccessTokenResponse.class);
|
||||
@@ -170,32 +206,31 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
||||
|
||||
String encodedIdToken = tokenResponse.getIdToken();
|
||||
|
||||
notes.put(FEDERATED_ACCESS_TOKEN, accessToken);
|
||||
notes.put(FEDERATED_ID_TOKEN, encodedIdToken);
|
||||
notes.put(FEDERATED_REFRESH_TOKEN, tokenResponse.getRefreshToken());
|
||||
notes.put(FEDERATED_TOKEN_EXPIRATION, Long.toString(tokenResponse.getExpiresIn()));
|
||||
|
||||
|
||||
IDToken idToken = validateIdToken(key, encodedIdToken);
|
||||
JsonWebToken idToken = validateToken(key, encodedIdToken);
|
||||
|
||||
try {
|
||||
String id = idToken.getSubject();
|
||||
String name = idToken.getName();
|
||||
String preferredUsername = idToken.getPreferredUsername();
|
||||
String email = idToken.getEmail();
|
||||
BrokeredIdentityContext identity = new BrokeredIdentityContext(id);
|
||||
String name = (String)idToken.getOtherClaims().get(IDToken.NAME);
|
||||
String preferredUsername = (String)idToken.getOtherClaims().get(IDToken.PREFERRED_USERNAME);
|
||||
String email = (String)idToken.getOtherClaims().get(IDToken.EMAIL);
|
||||
|
||||
if (id == null || name == null || preferredUsername == null || email == null && getConfig().getUserInfoUrl() != null) {
|
||||
JsonNode userInfo = SimpleHttp.doGet(getConfig().getUserInfoUrl())
|
||||
.header("Authorization", "Bearer " + accessToken)
|
||||
.asJson();
|
||||
if (getConfig().getUserInfoUrl() != null && (id == null || name == null || preferredUsername == null || email == null) ) {
|
||||
SimpleHttp request = JsonSimpleHttp.doGet(getConfig().getUserInfoUrl())
|
||||
.header("Authorization", "Bearer " + accessToken);
|
||||
JsonNode userInfo = JsonSimpleHttp.asJson(request);
|
||||
|
||||
id = getJsonProperty(userInfo, "sub");
|
||||
name = getJsonProperty(userInfo, "name");
|
||||
preferredUsername = getJsonProperty(userInfo, "preferred_username");
|
||||
email = getJsonProperty(userInfo, "email");
|
||||
AbstractJsonUserAttributeMapper.storeUserProfileForMapper(identity, userInfo, getConfig().getAlias());
|
||||
}
|
||||
|
||||
FederatedIdentity identity = new FederatedIdentity(id);
|
||||
identity.getContextData().put(FEDERATED_ACCESS_TOKEN_RESPONSE, tokenResponse);
|
||||
identity.getContextData().put(VALIDATED_ID_TOKEN, idToken);
|
||||
processAccessTokenResponse(identity, key, tokenResponse);
|
||||
|
||||
identity.setId(id);
|
||||
identity.setName(name);
|
||||
@@ -235,23 +270,34 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
private IDToken validateIdToken(PublicKey key, String encodedToken) {
|
||||
protected boolean verify(JWSInput jws, PublicKey key) {
|
||||
if (key == null) return true;
|
||||
if (!getConfig().isValidateSignature()) return true;
|
||||
return RSAProvider.verify(jws, key);
|
||||
|
||||
}
|
||||
|
||||
protected JsonWebToken validateToken(PublicKey key, String encodedToken) {
|
||||
if (encodedToken == null) {
|
||||
throw new IdentityBrokerException("No id_token from server.");
|
||||
throw new IdentityBrokerException("No token from server.");
|
||||
}
|
||||
|
||||
try {
|
||||
JWSInput jws = new JWSInput(encodedToken);
|
||||
if (!verify(jws, key)) {
|
||||
throw new IdentityBrokerException("IDToken signature validation failed");
|
||||
throw new IdentityBrokerException("token signature validation failed");
|
||||
}
|
||||
IDToken idToken = jws.readJsonContent(IDToken.class);
|
||||
JsonWebToken token = jws.readJsonContent(JsonWebToken.class);
|
||||
|
||||
String aud = idToken.getAudience();
|
||||
String iss = idToken.getIssuer();
|
||||
String aud = token.getAudience();
|
||||
String iss = token.getIssuer();
|
||||
|
||||
if (aud != null && !aud.equals(getConfig().getClientId())) {
|
||||
throw new IdentityBrokerException("Wrong audience from id_token..");
|
||||
throw new IdentityBrokerException("Wrong audience from token.");
|
||||
}
|
||||
|
||||
if (!token.isActive()) {
|
||||
throw new IdentityBrokerException("Token is no longer valid");
|
||||
}
|
||||
|
||||
String trustedIssuers = getConfig().getIssuer();
|
||||
@@ -261,18 +307,25 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
||||
|
||||
for (String trustedIssuer : issuers) {
|
||||
if (iss != null && iss.equals(trustedIssuer.trim())) {
|
||||
return idToken;
|
||||
return token;
|
||||
}
|
||||
}
|
||||
|
||||
throw new IdentityBrokerException("Wrong issuer from id_token. Got: " + iss + " expected: " + getConfig().getIssuer());
|
||||
throw new IdentityBrokerException("Wrong issuer from token. Got: " + iss + " expected: " + getConfig().getIssuer());
|
||||
}
|
||||
return idToken;
|
||||
return token;
|
||||
} catch (IOException e) {
|
||||
throw new IdentityBrokerException("Could not decode id token.", e);
|
||||
throw new IdentityBrokerException("Could not decode token.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void attachUserSession(UserSessionModel userSession, ClientSessionModel clientSession, BrokeredIdentityContext context) {
|
||||
AccessTokenResponse tokenResponse = (AccessTokenResponse)context.getContextData().get(FEDERATED_ACCESS_TOKEN_RESPONSE);
|
||||
userSession.setNote(FEDERATED_ACCESS_TOKEN, tokenResponse.getToken());
|
||||
userSession.setNote(FEDERATED_ID_TOKEN, tokenResponse.getIdToken());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getDefaultScopes() {
|
||||
return "openid";
|
||||
|
||||
@@ -70,6 +70,14 @@ public class OIDCIdentityProviderConfig extends OAuth2IdentityProviderConfig {
|
||||
getConfig().put("validateSignature", String.valueOf(validateSignature));
|
||||
}
|
||||
|
||||
public boolean isBackchannelSupported() {
|
||||
return Boolean.valueOf(getConfig().get("backchannelSupported"));
|
||||
}
|
||||
|
||||
public void setBackchannelSupported(boolean backchannel) {
|
||||
getConfig().put("backchannelSupported", String.valueOf(backchannel));
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
+13
-5
@@ -17,21 +17,24 @@
|
||||
*/
|
||||
package org.keycloak.broker.oidc;
|
||||
|
||||
import org.codehaus.jackson.annotate.JsonProperty;
|
||||
import org.keycloak.broker.oidc.util.SimpleHttp;
|
||||
import org.keycloak.broker.oidc.mappers.UsernameTemplateMapper;
|
||||
import org.keycloak.broker.provider.util.SimpleHttp;
|
||||
import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
|
||||
import org.keycloak.jose.jwk.JWK;
|
||||
import org.keycloak.jose.jwk.JWKParser;
|
||||
import org.keycloak.models.IdentityProviderMapperModel;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.protocol.oidc.representations.JSONWebKeySet;
|
||||
import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
import org.keycloak.util.PemUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.PublicKey;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
@@ -82,7 +85,7 @@ public class OIDCIdentityProviderFactory extends AbstractIdentityProviderFactory
|
||||
JSONWebKeySet keySet = JsonSerialization.readValue(keySetString, JSONWebKeySet.class);
|
||||
for (JWK jwk : keySet.getKeys()) {
|
||||
JWKParser parse = JWKParser.create(jwk);
|
||||
if (parse.getJwk().getPublicKeyUse().equals(JWK.SIG_USE)) {
|
||||
if (parse.getJwk().getPublicKeyUse().equals(JWK.SIG_USE) && keyTypeSupported(jwk.getKeyType())) {
|
||||
PublicKey key = parse.toPublicKey();
|
||||
config.setPublicKeySignatureVerifier(KeycloakModelUtils.getPemFromKey(key));
|
||||
config.setValidateSignature(true);
|
||||
@@ -91,10 +94,15 @@ public class OIDCIdentityProviderFactory extends AbstractIdentityProviderFactory
|
||||
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("F ailed to query JWKSet from: " + uri, e);
|
||||
throw new RuntimeException("Failed to query JWKSet from: " + uri, e);
|
||||
}
|
||||
|
||||
}
|
||||
return config.getConfig();
|
||||
}
|
||||
|
||||
protected static boolean keyTypeSupported(String type) {
|
||||
return type != null && type.equals("RSA");
|
||||
}
|
||||
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
package org.keycloak.broker.oidc.mappers;
|
||||
|
||||
import org.keycloak.broker.oidc.KeycloakOIDCIdentityProvider;
|
||||
import org.keycloak.broker.provider.AbstractIdentityProviderMapper;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.models.IdentityProviderMapperModel;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.representations.JsonWebToken;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public abstract class AbstractClaimMapper extends AbstractIdentityProviderMapper {
|
||||
public static final String CLAIM = "claim";
|
||||
public static final String CLAIM_VALUE = "claim.value";
|
||||
|
||||
public static Object getClaimValue(JsonWebToken token, String claim) {
|
||||
String[] split = claim.split("\\.");
|
||||
Map<String, Object> jsonObject = token.getOtherClaims();
|
||||
for (int i = 0; i < split.length; i++) {
|
||||
if (i == split.length - 1) {
|
||||
return jsonObject.get(split[i]);
|
||||
} else {
|
||||
Object val = jsonObject.get(split[i]);
|
||||
if (!(val instanceof Map)) return null;
|
||||
jsonObject = (Map<String, Object>)val;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Object getClaimValue(IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
String claim = mapperModel.getConfig().get(CLAIM);
|
||||
return getClaimValue(context, claim);
|
||||
}
|
||||
|
||||
public static Object getClaimValue(BrokeredIdentityContext context, String claim) {
|
||||
{ // search access token
|
||||
JsonWebToken token = (JsonWebToken)context.getContextData().get(KeycloakOIDCIdentityProvider.VALIDATED_ACCESS_TOKEN);
|
||||
if (token != null) {
|
||||
Object value = getClaimValue(token, claim);
|
||||
if (value != null) return value;
|
||||
}
|
||||
|
||||
}
|
||||
{ // search ID Token
|
||||
JsonWebToken token = (JsonWebToken)context.getContextData().get(KeycloakOIDCIdentityProvider.VALIDATED_ID_TOKEN);
|
||||
if (token != null) {
|
||||
Object value = getClaimValue(token, claim);
|
||||
if (value != null) return value;
|
||||
}
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
protected boolean hasClaimValue(IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
Object value = getClaimValue(mapperModel, context);
|
||||
String desiredValue = mapperModel.getConfig().get(CLAIM_VALUE);
|
||||
return valueEquals(desiredValue, value);
|
||||
}
|
||||
|
||||
public boolean valueEquals(String desiredValue, Object value) {
|
||||
if (value instanceof String) {
|
||||
if (desiredValue.equals(value)) return true;
|
||||
} else if (value instanceof Double) {
|
||||
try {
|
||||
if (Double.valueOf(desiredValue).equals(value)) return true;
|
||||
} catch (Exception e) {
|
||||
|
||||
}
|
||||
} else if (value instanceof Integer) {
|
||||
try {
|
||||
if (Integer.valueOf(desiredValue).equals(value)) return true;
|
||||
} catch (Exception e) {
|
||||
|
||||
}
|
||||
} else if (value instanceof Boolean) {
|
||||
try {
|
||||
if (Boolean.valueOf(desiredValue).equals(value)) return true;
|
||||
} catch (Exception e) {
|
||||
|
||||
}
|
||||
} else if (value instanceof List) {
|
||||
List list = (List)value;
|
||||
for (Object val : list) {
|
||||
return valueEquals(desiredValue, val);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Executable
+206
@@ -0,0 +1,206 @@
|
||||
package org.keycloak.broker.oidc.mappers;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.codehaus.jackson.JsonNode;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.broker.oidc.OIDCIdentityProvider;
|
||||
import org.keycloak.broker.provider.AbstractIdentityProviderMapper;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.models.IdentityProviderMapperModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
/**
|
||||
* Abstract class for Social Provider mappers which allow mapping of JSON user profile field into Keycloak user
|
||||
* attribute. Concrete mapper classes with own ID and provider mapping must be implemented for each social provider who
|
||||
* uses {@link JsonNode} user profile.
|
||||
*
|
||||
* @author Vlastimil Elias (velias at redhat dot com)
|
||||
*/
|
||||
public abstract class AbstractJsonUserAttributeMapper extends AbstractIdentityProviderMapper {
|
||||
|
||||
|
||||
protected static final Logger logger = Logger.getLogger(AbstractJsonUserAttributeMapper.class);
|
||||
|
||||
protected static final Logger LOGGER_DUMP_USER_PROFILE = Logger.getLogger("org.keycloak.social.user_profile_dump");
|
||||
|
||||
private static final String JSON_PATH_DELIMITER = ".";
|
||||
|
||||
/**
|
||||
* Config param where name of mapping source JSON User Profile field is stored.
|
||||
*/
|
||||
public static final String CONF_JSON_FIELD = "jsonField";
|
||||
/**
|
||||
* Config param where name of mapping target USer attribute is stored.
|
||||
*/
|
||||
public static final String CONF_USER_ATTRIBUTE = "userAttribute";
|
||||
|
||||
/**
|
||||
* Key in {@link BrokeredIdentityContext#getContextData()} where {@link JsonNode} with user profile is stored.
|
||||
*/
|
||||
public static final String CONTEXT_JSON_NODE = OIDCIdentityProvider.USER_INFO;
|
||||
|
||||
private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
|
||||
|
||||
static {
|
||||
ProviderConfigProperty property;
|
||||
ProviderConfigProperty property1;
|
||||
property1 = new ProviderConfigProperty();
|
||||
property1.setName(CONF_JSON_FIELD);
|
||||
property1.setLabel("Social Profile JSON Field Path");
|
||||
property1.setHelpText("Path of field in Social provider User Profile JSON data to get value from. You can use dot notation for nesting and square brackets for array index. Eg. 'contact.address[0].country'.");
|
||||
property1.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
configProperties.add(property1);
|
||||
property = new ProviderConfigProperty();
|
||||
property.setName(CONF_USER_ATTRIBUTE);
|
||||
property.setLabel("User Attribute Name");
|
||||
property.setHelpText("User attribute name to store information into.");
|
||||
property.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
configProperties.add(property);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store used profile JsonNode into user context for later use by this mapper. Profile data are dumped into special logger if enabled also to allow investigation of the structure.
|
||||
*
|
||||
* @param user context to store profile data into
|
||||
* @param profile to store into context
|
||||
* @param provider identification of social provider to be used in log dump
|
||||
*
|
||||
* @see #importNewUser(KeycloakSession, RealmModel, UserModel, IdentityProviderMapperModel, BrokeredIdentityContext)
|
||||
* @see BrokeredIdentityContext#getContextData()
|
||||
*/
|
||||
public static void storeUserProfileForMapper(BrokeredIdentityContext user, JsonNode profile, String provider) {
|
||||
user.getContextData().put(AbstractJsonUserAttributeMapper.CONTEXT_JSON_NODE, profile);
|
||||
if (LOGGER_DUMP_USER_PROFILE.isDebugEnabled())
|
||||
LOGGER_DUMP_USER_PROFILE.debug("User Profile JSON Data for provider "+provider+": " + profile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return configProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayCategory() {
|
||||
return "Attribute Importer";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "Attribute Importer";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Import user profile information if it exists in Social provider JSON data into the specified user attribute.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
String attribute = mapperModel.getConfig().get(CONF_USER_ATTRIBUTE);
|
||||
if (attribute == null || attribute.trim().isEmpty()) {
|
||||
logger.debug("Attribute is not configured");
|
||||
return;
|
||||
}
|
||||
attribute = attribute.trim();
|
||||
|
||||
String value = getJsonValue(mapperModel, context);
|
||||
if (value != null) {
|
||||
user.setSingleAttribute(attribute, value);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
// we do not update user profile from social provider
|
||||
}
|
||||
|
||||
protected static String getJsonValue(IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
|
||||
String jsonField = mapperModel.getConfig().get(CONF_JSON_FIELD);
|
||||
if (jsonField == null || jsonField.trim().isEmpty()) {
|
||||
logger.debug("JSON field path is not configured");
|
||||
return null;
|
||||
}
|
||||
jsonField = jsonField.trim();
|
||||
|
||||
if (jsonField.startsWith(JSON_PATH_DELIMITER) || jsonField.endsWith(JSON_PATH_DELIMITER) || jsonField.startsWith("[")) {
|
||||
logger.debug("JSON field path is invalid " + jsonField);
|
||||
return null;
|
||||
}
|
||||
|
||||
JsonNode profileJsonNode = (JsonNode) context.getContextData().get(CONTEXT_JSON_NODE);
|
||||
|
||||
String value = getJsonValue(profileJsonNode, jsonField);
|
||||
|
||||
if (value == null) {
|
||||
logger.debug("User profile JSON value '" + jsonField + "' is not available.");
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
protected static String getJsonValue(JsonNode baseNode, String fieldPath) {
|
||||
logger.debug("Going to process JsonNode path " + fieldPath + " on data " + baseNode);
|
||||
if (baseNode != null) {
|
||||
|
||||
int idx = fieldPath.indexOf(JSON_PATH_DELIMITER);
|
||||
|
||||
String currentFieldName = fieldPath;
|
||||
if (idx > 0) {
|
||||
currentFieldName = fieldPath.substring(0, idx).trim();
|
||||
if (currentFieldName.isEmpty()) {
|
||||
logger.debug("JSON path is invalid " + fieldPath);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
String currentNodeName = currentFieldName;
|
||||
int arrayIndex = -1;
|
||||
if (currentFieldName.endsWith("]")) {
|
||||
int bi = currentFieldName.indexOf("[");
|
||||
if (bi == -1) {
|
||||
logger.debug("Invalid array index construct in " + currentFieldName);
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
String is = currentFieldName.substring(bi+1, currentFieldName.length() - 1).trim();
|
||||
arrayIndex = Integer.parseInt(is);
|
||||
} catch (Exception e) {
|
||||
logger.debug("Invalid array index construct in " + currentFieldName);
|
||||
return null;
|
||||
}
|
||||
currentNodeName = currentFieldName.substring(0,bi).trim();
|
||||
}
|
||||
|
||||
JsonNode currentNode = baseNode.get(currentNodeName);
|
||||
if (arrayIndex > -1 && currentNode.isArray()) {
|
||||
logger.debug("Going to take array node at index " + arrayIndex);
|
||||
currentNode = currentNode.get(arrayIndex);
|
||||
}
|
||||
|
||||
if (currentNode == null) {
|
||||
logger.debug("JsonNode not found for name " + currentFieldName);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (idx < 0) {
|
||||
if (!currentNode.isValueNode()) {
|
||||
logger.debug("JsonNode is not value node for name " + currentFieldName);
|
||||
return null;
|
||||
}
|
||||
String ret = currentNode.asText();
|
||||
if (ret != null && !ret.trim().isEmpty())
|
||||
return ret.trim();
|
||||
} else {
|
||||
return getJsonValue(currentNode, fieldPath.substring(idx + 1));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
package org.keycloak.broker.oidc.mappers;
|
||||
|
||||
import org.keycloak.broker.oidc.KeycloakOIDCIdentityProviderFactory;
|
||||
import org.keycloak.broker.oidc.OIDCIdentityProviderFactory;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.broker.provider.HardcodedRoleMapper;
|
||||
import org.keycloak.broker.provider.IdentityBrokerException;
|
||||
import org.keycloak.models.IdentityProviderMapperModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class ClaimToRoleMapper extends AbstractClaimMapper {
|
||||
|
||||
public static final String[] COMPATIBLE_PROVIDERS = {KeycloakOIDCIdentityProviderFactory.PROVIDER_ID, OIDCIdentityProviderFactory.PROVIDER_ID};
|
||||
|
||||
private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
|
||||
|
||||
static {
|
||||
ProviderConfigProperty property;
|
||||
ProviderConfigProperty property1;
|
||||
property1 = new ProviderConfigProperty();
|
||||
property1.setName(CLAIM);
|
||||
property1.setLabel("Claim");
|
||||
property1.setHelpText("Name of claim to search for in token. You can reference nested claims using a '.', i.e. 'address.locality'.");
|
||||
property1.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
configProperties.add(property1);
|
||||
property1 = new ProviderConfigProperty();
|
||||
property1.setName(CLAIM_VALUE);
|
||||
property1.setLabel("Claim Value");
|
||||
property1.setHelpText("Value the claim must have. If the claim is an array, then the value must be contained in the array.");
|
||||
property1.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
configProperties.add(property1);
|
||||
property = new ProviderConfigProperty();
|
||||
property.setName(HardcodedRoleMapper.ROLE);
|
||||
property.setLabel("Role");
|
||||
property.setHelpText("Role to grant to user if claim is present. Click 'Select Role' button to browse roles, or just type it in the textbox. To reference an application role the syntax is appname.approle, i.e. myapp.myrole");
|
||||
property.setType(ProviderConfigProperty.ROLE_TYPE);
|
||||
configProperties.add(property);
|
||||
}
|
||||
|
||||
public static final String PROVIDER_ID = "oidc-role-idp-mapper";
|
||||
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return configProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getCompatibleProviders() {
|
||||
return COMPATIBLE_PROVIDERS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayCategory() {
|
||||
return "Role Importer";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "Claim to Role";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
String roleName = mapperModel.getConfig().get(HardcodedRoleMapper.ROLE);
|
||||
if (hasClaimValue(mapperModel, context)) {
|
||||
RoleModel role = HardcodedRoleMapper.getRoleFromString(realm, roleName);
|
||||
if (role == null) throw new IdentityBrokerException("Unable to find role: " + roleName);
|
||||
user.grantRole(role);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
String roleName = mapperModel.getConfig().get(HardcodedRoleMapper.ROLE);
|
||||
if (!hasClaimValue(mapperModel, context)) {
|
||||
RoleModel role = HardcodedRoleMapper.getRoleFromString(realm, roleName);
|
||||
if (role == null) throw new IdentityBrokerException("Unable to find role: " + roleName);
|
||||
user.deleteRoleMapping(role);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "If a claim exists, grant the user the specified realm or application role.";
|
||||
}
|
||||
|
||||
}
|
||||
Executable
+118
@@ -0,0 +1,118 @@
|
||||
package org.keycloak.broker.oidc.mappers;
|
||||
|
||||
import org.keycloak.broker.oidc.KeycloakOIDCIdentityProvider;
|
||||
import org.keycloak.broker.oidc.KeycloakOIDCIdentityProviderFactory;
|
||||
import org.keycloak.broker.oidc.OIDCIdentityProviderFactory;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.broker.provider.HardcodedRoleMapper;
|
||||
import org.keycloak.broker.provider.IdentityBrokerException;
|
||||
import org.keycloak.models.IdentityProviderMapperModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.representations.JsonWebToken;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class ExternalKeycloakRoleToRoleMapper extends AbstractClaimMapper {
|
||||
|
||||
public static final String[] COMPATIBLE_PROVIDERS = {KeycloakOIDCIdentityProviderFactory.PROVIDER_ID};
|
||||
|
||||
private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
|
||||
private static final String EXTERNAL_ROLE = "external.role";
|
||||
|
||||
static {
|
||||
ProviderConfigProperty property;
|
||||
ProviderConfigProperty property1;
|
||||
property1 = new ProviderConfigProperty();
|
||||
property1.setName(EXTERNAL_ROLE);
|
||||
property1.setLabel("External role");
|
||||
property1.setHelpText("External role to check for. To reference an application role the syntax is appname.approle, i.e. myapp.myrole.");
|
||||
property1.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
configProperties.add(property1);
|
||||
property = new ProviderConfigProperty();
|
||||
property.setName(HardcodedRoleMapper.ROLE);
|
||||
property.setLabel("Role");
|
||||
property.setHelpText("Role to grant to user if external role is present. Click 'Select Role' button to browse roles, or just type it in the textbox. To reference an application role the syntax is appname.approle, i.e. myapp.myrole");
|
||||
property.setType(ProviderConfigProperty.ROLE_TYPE);
|
||||
configProperties.add(property);
|
||||
}
|
||||
|
||||
public static final String PROVIDER_ID = "keycloak-oidc-role-to-role-idp-mapper";
|
||||
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return configProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getCompatibleProviders() {
|
||||
return COMPATIBLE_PROVIDERS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayCategory() {
|
||||
return "Role Importer";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "External Role to Role";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
RoleModel role = hasRole(realm, mapperModel, context);
|
||||
if (role != null) {
|
||||
user.grantRole(role);
|
||||
}
|
||||
}
|
||||
|
||||
private RoleModel hasRole(RealmModel realm,IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
JsonWebToken token = (JsonWebToken)context.getContextData().get(KeycloakOIDCIdentityProvider.VALIDATED_ACCESS_TOKEN);
|
||||
//if (token == null) return;
|
||||
String roleName = mapperModel.getConfig().get(HardcodedRoleMapper.ROLE);
|
||||
String[] parseRole = HardcodedRoleMapper.parseRole(mapperModel.getConfig().get(EXTERNAL_ROLE));
|
||||
String externalRoleName = parseRole[1];
|
||||
String claimName = null;
|
||||
if (parseRole[0] == null) {
|
||||
claimName = "realm_access.roles";
|
||||
} else {
|
||||
claimName = "resource_access." + parseRole[0] + ".roles";
|
||||
}
|
||||
Object claim = getClaimValue(token, claimName);
|
||||
if (valueEquals(externalRoleName, claim)) {
|
||||
RoleModel role = HardcodedRoleMapper.getRoleFromString(realm, roleName);
|
||||
if (role == null) throw new IdentityBrokerException("Unable to find role: " + roleName);
|
||||
return role;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
RoleModel role = hasRole(realm, mapperModel, context);
|
||||
if (role == null) {
|
||||
user.deleteRoleMapping(role);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Looks for an external role in a keycloak access token. If external role exists, grant the user the specified realm or application role.";
|
||||
}
|
||||
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
package org.keycloak.broker.oidc.mappers;
|
||||
|
||||
import org.keycloak.broker.oidc.KeycloakOIDCIdentityProviderFactory;
|
||||
import org.keycloak.broker.oidc.OIDCIdentityProviderFactory;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.models.IdentityProviderMapperModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class UserAttributeMapper extends AbstractClaimMapper {
|
||||
|
||||
public static final String[] COMPATIBLE_PROVIDERS = {KeycloakOIDCIdentityProviderFactory.PROVIDER_ID, OIDCIdentityProviderFactory.PROVIDER_ID};
|
||||
|
||||
private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
|
||||
|
||||
public static final String USER_ATTRIBUTE = "user.attribute";
|
||||
|
||||
static {
|
||||
ProviderConfigProperty property;
|
||||
ProviderConfigProperty property1;
|
||||
property1 = new ProviderConfigProperty();
|
||||
property1.setName(CLAIM);
|
||||
property1.setLabel("Claim");
|
||||
property1.setHelpText("Name of claim to search for in token. You can reference nested claims using a '.', i.e. 'address.locality'.");
|
||||
property1.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
configProperties.add(property1);
|
||||
property = new ProviderConfigProperty();
|
||||
property.setName(USER_ATTRIBUTE);
|
||||
property.setLabel("User Attribute Name");
|
||||
property.setHelpText("User attribute name to store claim.");
|
||||
property.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
configProperties.add(property);
|
||||
}
|
||||
|
||||
public static final String PROVIDER_ID = "oidc-user-attribute-idp-mapper";
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return configProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getCompatibleProviders() {
|
||||
return COMPATIBLE_PROVIDERS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayCategory() {
|
||||
return "Attribute Importer";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "Attribute Importer";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
String attribute = mapperModel.getConfig().get(USER_ATTRIBUTE);
|
||||
Object value = getClaimValue(mapperModel, context);
|
||||
if (value != null) {
|
||||
user.setSingleAttribute(attribute, value.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
String attribute = mapperModel.getConfig().get(USER_ATTRIBUTE);
|
||||
Object value = getClaimValue(mapperModel, context);
|
||||
String current = user.getFirstAttribute(attribute);
|
||||
if (value != null && !value.equals(current)) {
|
||||
user.setSingleAttribute(attribute, value.toString());
|
||||
} else if (value == null) {
|
||||
user.removeAttribute(attribute);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Import declared claim if it exists in ID or access token into the specified user attribute.";
|
||||
}
|
||||
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
package org.keycloak.broker.oidc.mappers;
|
||||
|
||||
import org.keycloak.broker.oidc.KeycloakOIDCIdentityProviderFactory;
|
||||
import org.keycloak.broker.oidc.OIDCIdentityProviderFactory;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.models.IdentityProviderMapperModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class UsernameTemplateMapper extends AbstractClaimMapper {
|
||||
|
||||
public static final String[] COMPATIBLE_PROVIDERS = {KeycloakOIDCIdentityProviderFactory.PROVIDER_ID, OIDCIdentityProviderFactory.PROVIDER_ID};
|
||||
|
||||
private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
|
||||
|
||||
public static final String TEMPLATE = "template";
|
||||
|
||||
static {
|
||||
ProviderConfigProperty property;
|
||||
property = new ProviderConfigProperty();
|
||||
property.setName(TEMPLATE);
|
||||
property.setLabel("Template");
|
||||
property.setHelpText("Template to use to format the username to import. Substitutions are enclosed in ${}. For example: '${ALIAS}.${CLAIM.sub}'. ALIAS is the provider alias. CLAIM.<NAME> references an ID or Access token claim.");
|
||||
property.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
property.setDefaultValue("${ALIAS}.${CLAIM.preferred_username}");
|
||||
configProperties.add(property);
|
||||
}
|
||||
|
||||
public static final String PROVIDER_ID = "oidc-username-idp-mapper";
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return configProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getCompatibleProviders() {
|
||||
return COMPATIBLE_PROVIDERS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayCategory() {
|
||||
return "Preprocessor";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "Username Template Importer";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
}
|
||||
|
||||
static Pattern substitution = Pattern.compile("\\$\\{([^}]+)\\}");
|
||||
|
||||
@Override
|
||||
public void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
String template = mapperModel.getConfig().get(TEMPLATE);
|
||||
Matcher m = substitution.matcher(template);
|
||||
StringBuffer sb = new StringBuffer();
|
||||
while (m.find()) {
|
||||
String variable = m.group(1);
|
||||
if (variable.equals("ALIAS")) {
|
||||
m.appendReplacement(sb, context.getIdpConfig().getAlias());
|
||||
} else if (variable.equals("UUID")) {
|
||||
m.appendReplacement(sb, KeycloakModelUtils.generateId());
|
||||
} else if (variable.startsWith("CLAIM.")) {
|
||||
String name = variable.substring("CLAIM.".length());
|
||||
Object value = AbstractClaimMapper.getClaimValue(context, name);
|
||||
if (value == null) value = "";
|
||||
m.appendReplacement(sb, value.toString());
|
||||
} else {
|
||||
m.appendReplacement(sb, m.group(1));
|
||||
}
|
||||
|
||||
}
|
||||
m.appendTail(sb);
|
||||
String username = sb.toString();
|
||||
context.setModelUsername(username);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Format the username to import.";
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package org.keycloak.broker.oidc.util;
|
||||
|
||||
import org.codehaus.jackson.JsonNode;
|
||||
import org.codehaus.jackson.map.ObjectMapper;
|
||||
import org.keycloak.broker.provider.util.SimpleHttp;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class JsonSimpleHttp extends SimpleHttp {
|
||||
public JsonSimpleHttp(String url, String method) {
|
||||
super(url, method);
|
||||
}
|
||||
|
||||
public static JsonSimpleHttp doGet(String url) {
|
||||
return new JsonSimpleHttp(url, "GET");
|
||||
}
|
||||
|
||||
public static JsonSimpleHttp doPost(String url) {
|
||||
return new JsonSimpleHttp(url, "POST");
|
||||
}
|
||||
|
||||
private static ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
public static JsonNode asJson(SimpleHttp request) throws IOException {
|
||||
return mapper.readTree(request.asString());
|
||||
}
|
||||
|
||||
}
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
org.keycloak.broker.oidc.OIDCIdentityProviderFactory
|
||||
org.keycloak.broker.oidc.OIDCIdentityProviderFactory
|
||||
org.keycloak.broker.oidc.KeycloakOIDCIdentityProviderFactory
|
||||
broker/oidc/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderMapper
Executable
+4
@@ -0,0 +1,4 @@
|
||||
org.keycloak.broker.oidc.mappers.ClaimToRoleMapper
|
||||
org.keycloak.broker.oidc.mappers.ExternalKeycloakRoleToRoleMapper
|
||||
org.keycloak.broker.oidc.mappers.UserAttributeMapper
|
||||
org.keycloak.broker.oidc.mappers.UsernameTemplateMapper
|
||||
Executable
+133
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* JBoss, Home of Professional Open Source
|
||||
* Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors
|
||||
* as indicated by the @authors tag. All rights reserved.
|
||||
*/
|
||||
package org.keycloak.broker.oidc;
|
||||
|
||||
import org.codehaus.jackson.JsonNode;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.broker.provider.IdentityBrokerException;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Unit test for {@link AbstractOAuth2IdentityProvider}
|
||||
*
|
||||
* @author Vlastimil Elias (velias at redhat dot com)
|
||||
*/
|
||||
public class AbstractOAuth2IdentityProviderTest {
|
||||
|
||||
@Test
|
||||
public void constructor_defaultScopeHandling(){
|
||||
TestProvider tested = getTested();
|
||||
|
||||
//default scope is set from the provider if not provided in the configuration
|
||||
Assert.assertEquals( tested.getDefaultScopes(), tested.getConfig().getDefaultScope());
|
||||
|
||||
//default scope is preserved if provided in the configuration
|
||||
IdentityProviderModel model = new IdentityProviderModel();
|
||||
OAuth2IdentityProviderConfig config = new OAuth2IdentityProviderConfig(model);
|
||||
config.setDefaultScope("myscope");
|
||||
tested = new TestProvider(config);
|
||||
|
||||
Assert.assertEquals("myscope", tested.getConfig().getDefaultScope());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getJsonProperty_asJsonNode() throws IOException {
|
||||
TestProvider tested = getTested();
|
||||
|
||||
JsonNode jsonNode = tested
|
||||
.asJsonNode("{\"nullone\":null, \"emptyone\":\"\", \"blankone\": \" \", \"withvalue\" : \"my value\", \"withbooleanvalue\" : true, \"withnumbervalue\" : 10}");
|
||||
Assert.assertNull(tested.getJsonProperty(jsonNode, "nonexisting"));
|
||||
Assert.assertNull(tested.getJsonProperty(jsonNode, "nullone"));
|
||||
Assert.assertNull(tested.getJsonProperty(jsonNode, "emptyone"));
|
||||
Assert.assertEquals(" ", tested.getJsonProperty(jsonNode, "blankone"));
|
||||
Assert.assertEquals("my value", tested.getJsonProperty(jsonNode, "withvalue"));
|
||||
Assert.assertEquals("true", tested.getJsonProperty(jsonNode, "withbooleanvalue"));
|
||||
Assert.assertEquals("10", tested.getJsonProperty(jsonNode, "withnumbervalue"));
|
||||
}
|
||||
|
||||
@Test(expected = IdentityBrokerException.class)
|
||||
public void getFederatedIdentity_responseUrlLine_tokenNotFound() {
|
||||
TestProvider tested = getTested();
|
||||
tested.getFederatedIdentity("cosi=sss");
|
||||
}
|
||||
|
||||
@Test(expected = IdentityBrokerException.class)
|
||||
public void getFederatedIdentity_responseJSON_tokenNotFound() {
|
||||
TestProvider tested = getTested();
|
||||
Map<String, String> notes = new HashMap<>();
|
||||
tested.getFederatedIdentity("{\"cosi\":\"sss\"}");
|
||||
}
|
||||
|
||||
@Test(expected = IdentityBrokerException.class)
|
||||
public void getFederatedIdentity_responseJSON_invalidFormat() {
|
||||
TestProvider tested = getTested();
|
||||
Map<String, String> notes = new HashMap<>();
|
||||
tested.getFederatedIdentity("{\"cosi\":\"sss\"");
|
||||
}
|
||||
|
||||
@Test(expected = IdentityBrokerException.class)
|
||||
public void getFederatedIdentity_responseJSON_emptyTokenField() {
|
||||
TestProvider tested = getTested();
|
||||
tested.getFederatedIdentity("{\""
|
||||
+ AbstractOAuth2IdentityProvider.OAUTH2_PARAMETER_ACCESS_TOKEN + "\" : \"\"}");
|
||||
}
|
||||
|
||||
@Test(expected = IdentityBrokerException.class)
|
||||
public void getFederatedIdentity_responseJSON_nullTokenField() {
|
||||
TestProvider tested = getTested();
|
||||
tested.getFederatedIdentity("{\""
|
||||
+ AbstractOAuth2IdentityProvider.OAUTH2_PARAMETER_ACCESS_TOKEN + "\" : null}");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getFederatedIdentity_responseJSON() {
|
||||
TestProvider tested = getTested();
|
||||
BrokeredIdentityContext fi = tested.getFederatedIdentity("{\""
|
||||
+ AbstractOAuth2IdentityProvider.OAUTH2_PARAMETER_ACCESS_TOKEN + "\" : \"458rt\"}");
|
||||
Assert.assertNotNull(fi);
|
||||
Assert.assertEquals("458rt", fi.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getFederatedIdentity_responseUrlLine() {
|
||||
TestProvider tested = getTested();
|
||||
BrokeredIdentityContext fi = tested.getFederatedIdentity("cosi=sss&"
|
||||
+ AbstractOAuth2IdentityProvider.OAUTH2_PARAMETER_ACCESS_TOKEN + "=458rtf&kdesi=ss}");
|
||||
Assert.assertNotNull(fi);
|
||||
Assert.assertEquals("458rtf", fi.getId());
|
||||
}
|
||||
|
||||
private TestProvider getTested() {
|
||||
IdentityProviderModel model = new IdentityProviderModel();
|
||||
OAuth2IdentityProviderConfig config = new OAuth2IdentityProviderConfig(model);
|
||||
return new TestProvider(config);
|
||||
}
|
||||
|
||||
private static class TestProvider extends AbstractOAuth2IdentityProvider<OAuth2IdentityProviderConfig> {
|
||||
|
||||
public TestProvider(OAuth2IdentityProviderConfig config) {
|
||||
super(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getDefaultScopes() {
|
||||
return "default";
|
||||
}
|
||||
|
||||
protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) {
|
||||
return new BrokeredIdentityContext(accessToken);
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* JBoss, Home of Professional Open Source
|
||||
* Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors
|
||||
* as indicated by the @authors tag. All rights reserved.
|
||||
*/
|
||||
package org.keycloak.broker.oidc.mappers;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.codehaus.jackson.JsonNode;
|
||||
import org.codehaus.jackson.JsonProcessingException;
|
||||
import org.codehaus.jackson.map.ObjectMapper;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* Unit test for {@link AbstractJsonUserAttributeMapper}
|
||||
*
|
||||
* @author Vlastimil Elias (velias at redhat dot com)
|
||||
*/
|
||||
public class AbstractJsonUserAttributeMapperTest {
|
||||
|
||||
private static ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
private static JsonNode baseNode;
|
||||
|
||||
private JsonNode getJsonNode() throws JsonProcessingException, IOException {
|
||||
if (baseNode == null)
|
||||
baseNode = mapper.readTree("{ \"value1\" : \"v1 \",\"value_empty\" : \"\", \"value_b\" : true, \"value_i\" : 454, " + " \"value_array\":[\"a1\",\"a2\"], " +" \"nest1\": {\"value1\": \" fgh \",\"value_empty\" : \"\", \"nest2\":{\"value_b\" : false, \"value_i\" : 43}}, "+ " \"nesta\": { \"a\":[{\"av1\": \"vala1\"},{\"av1\": \"vala2\"}]}"+" }");
|
||||
return baseNode;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getJsonValue_invalidPath() throws JsonProcessingException, IOException {
|
||||
|
||||
Assert.assertNull(AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "."));
|
||||
Assert.assertNull(AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), ".."));
|
||||
Assert.assertNull(AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "...value1"));
|
||||
Assert.assertNull(AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), ".value1"));
|
||||
Assert.assertNull(AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "value1."));
|
||||
Assert.assertNull(AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "[]"));
|
||||
Assert.assertNull(AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "[value1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getJsonValue_simpleValues() throws JsonProcessingException, IOException {
|
||||
|
||||
//unknown field returns null
|
||||
Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "value_unknown"));
|
||||
|
||||
// we check value is trimmed also!
|
||||
Assert.assertEquals("v1", AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "value1"));
|
||||
Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "value_empty"));
|
||||
|
||||
Assert.assertEquals("true", AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "value_b"));
|
||||
Assert.assertEquals("454", AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "value_i"));
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getJsonValue_nestedSimpleValues() throws JsonProcessingException, IOException {
|
||||
|
||||
// null if path points to JSON object
|
||||
Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nest1"));
|
||||
Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nest1.nest2"));
|
||||
|
||||
//unknown field returns null
|
||||
Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nest1.value_unknown"));
|
||||
Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nest1.nest2.value_unknown"));
|
||||
|
||||
// we check value is trimmed also!
|
||||
Assert.assertEquals("fgh", AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nest1.value1"));
|
||||
Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nest1.value_empty"));
|
||||
|
||||
Assert.assertEquals("false", AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nest1.nest2.value_b"));
|
||||
Assert.assertEquals("43", AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nest1.nest2.value_i"));
|
||||
|
||||
// null if invalid nested path
|
||||
Assert.assertNull(AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nest1."));
|
||||
Assert.assertNull(AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nest1.nest2."));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getJsonValue_simpleArray() throws JsonProcessingException, IOException {
|
||||
|
||||
// array field itself returns null if no index is provided
|
||||
Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "value_array"));
|
||||
// outside index returns null
|
||||
Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "value_array[2]"));
|
||||
|
||||
//corect index
|
||||
Assert.assertEquals("a1", AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "value_array[0]"));
|
||||
Assert.assertEquals("a2", AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "value_array[1]"));
|
||||
|
||||
//incorrect array constructs
|
||||
Assert.assertNull(AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "value_array[]"));
|
||||
Assert.assertNull(AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "value_array]"));
|
||||
Assert.assertNull(AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "value_array["));
|
||||
Assert.assertNull(AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "value_array[a]"));
|
||||
Assert.assertNull(AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "value_array[-2]"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getJsonValue_nestedArrayWithObjects() throws JsonProcessingException, IOException {
|
||||
Assert.assertEquals("vala1", AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nesta.a[0].av1"));
|
||||
Assert.assertEquals("vala2", AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nesta.a[1].av1"));
|
||||
|
||||
//different path erros or nonexisting indexes or fields return null
|
||||
Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nesta.a[2].av1"));
|
||||
Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nesta.a[0]"));
|
||||
Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nesta.a[0].av_unknown"));
|
||||
Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nesta.a[].av1"));
|
||||
Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nesta.a"));
|
||||
Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nesta.a.av1"));
|
||||
Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nesta.a].av1"));
|
||||
Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nesta.a[.av1"));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+23
-23
@@ -1,23 +1,23 @@
|
||||
<?xml version="1.0"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>1.2.0.Beta1-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>keycloak-broker-parent</artifactId>
|
||||
<name>Keycloak Broker Parent</name>
|
||||
<description/>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<modules>
|
||||
<module>core</module>
|
||||
<module>oidc</module>
|
||||
<module>saml</module>
|
||||
</modules>
|
||||
|
||||
</project>
|
||||
<?xml version="1.0"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>1.4.0.Final</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>keycloak-broker-parent</artifactId>
|
||||
<name>Keycloak Broker Parent</name>
|
||||
<description/>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<modules>
|
||||
<module>core</module>
|
||||
<module>oidc</module>
|
||||
<module>saml</module>
|
||||
</modules>
|
||||
|
||||
</project>
|
||||
+50
-53
@@ -1,53 +1,50 @@
|
||||
<?xml version="1.0"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>1.2.0.Beta1-SNAPSHOT</version>
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>keycloak-broker-saml</artifactId>
|
||||
<name>Keycloak Broker - SAML Identity Provider</name>
|
||||
<description/>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-broker-core</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-saml-protocol</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.picketlink</groupId>
|
||||
<artifactId>picketlink-federation</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-services</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-events-api</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jboss.logging</groupId>
|
||||
<artifactId>jboss-logging</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
<?xml version="1.0"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>1.4.0.Final</version>
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>keycloak-broker-saml</artifactId>
|
||||
<name>Keycloak Broker - SAML Identity Provider</name>
|
||||
<description/>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-broker-core</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-saml-protocol</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-saml-core</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-services</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-events-api</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jboss.logging</groupId>
|
||||
<artifactId>jboss-logging</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -1,458 +1,492 @@
|
||||
package org.keycloak.broker.saml;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.ClientConnection;
|
||||
import org.keycloak.VerificationException;
|
||||
import org.keycloak.broker.provider.FederatedIdentity;
|
||||
import org.keycloak.broker.provider.IdentityBrokerException;
|
||||
import org.keycloak.broker.provider.IdentityProvider;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.protocol.saml.SAML2LogoutResponseBuilder;
|
||||
import org.keycloak.protocol.saml.SAMLRequestParser;
|
||||
import org.keycloak.protocol.saml.SamlProtocol;
|
||||
import org.keycloak.protocol.saml.SamlProtocolUtils;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.services.resources.flows.Flows;
|
||||
import org.picketlink.common.constants.GeneralConstants;
|
||||
import org.picketlink.common.constants.JBossSAMLConstants;
|
||||
import org.picketlink.common.constants.JBossSAMLURIConstants;
|
||||
import org.picketlink.common.exceptions.ConfigurationException;
|
||||
import org.picketlink.common.exceptions.ProcessingException;
|
||||
import org.picketlink.common.util.DocumentUtil;
|
||||
import org.picketlink.common.util.StaxParserUtil;
|
||||
import org.picketlink.identity.federation.api.saml.v2.response.SAML2Response;
|
||||
import org.picketlink.identity.federation.core.parsers.saml.SAMLParser;
|
||||
import org.picketlink.identity.federation.core.saml.v2.common.SAMLDocumentHolder;
|
||||
import org.picketlink.identity.federation.core.util.JAXPValidationUtil;
|
||||
import org.picketlink.identity.federation.core.util.XMLEncryptionUtil;
|
||||
import org.picketlink.identity.federation.core.util.XMLSignatureUtil;
|
||||
import org.picketlink.identity.federation.saml.v2.assertion.AssertionType;
|
||||
import org.picketlink.identity.federation.saml.v2.assertion.AuthnStatementType;
|
||||
import org.picketlink.identity.federation.saml.v2.assertion.EncryptedAssertionType;
|
||||
import org.picketlink.identity.federation.saml.v2.assertion.NameIDType;
|
||||
import org.picketlink.identity.federation.saml.v2.assertion.SubjectType;
|
||||
import org.picketlink.identity.federation.saml.v2.protocol.LogoutRequestType;
|
||||
import org.picketlink.identity.federation.saml.v2.protocol.RequestAbstractType;
|
||||
import org.picketlink.identity.federation.saml.v2.protocol.ResponseType;
|
||||
import org.picketlink.identity.federation.saml.v2.protocol.StatusResponseType;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.Node;
|
||||
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.FormParam;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.HttpHeaders;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriBuilder;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
import javax.xml.namespace.QName;
|
||||
import java.io.IOException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class SAMLEndpoint {
|
||||
protected static final Logger logger = Logger.getLogger(SAMLEndpoint.class);
|
||||
public static final String SAML_FEDERATED_SESSION_INDEX = "SAML_FEDERATED_SESSION_INDEX";
|
||||
public static final String SAML_FEDERATED_SUBJECT = "SAML_FEDERATED_SUBJECT";
|
||||
public static final String SAML_FEDERATED_SUBJECT_NAMEFORMAT = "SAML_FEDERATED_SUBJECT_NAMEFORMAT";
|
||||
protected RealmModel realm;
|
||||
protected EventBuilder event;
|
||||
protected SAMLIdentityProviderConfig config;
|
||||
protected IdentityProvider.AuthenticationCallback callback;
|
||||
|
||||
@Context
|
||||
private UriInfo uriInfo;
|
||||
|
||||
@Context
|
||||
private KeycloakSession session;
|
||||
|
||||
@Context
|
||||
private ClientConnection clientConnection;
|
||||
|
||||
@Context
|
||||
private HttpHeaders headers;
|
||||
|
||||
|
||||
public SAMLEndpoint(RealmModel realm, SAMLIdentityProviderConfig config, IdentityProvider.AuthenticationCallback callback) {
|
||||
this.realm = realm;
|
||||
this.config = config;
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
@GET
|
||||
public Response redirectBinding(@QueryParam(GeneralConstants.SAML_REQUEST_KEY) String samlRequest,
|
||||
@QueryParam(GeneralConstants.SAML_RESPONSE_KEY) String samlResponse,
|
||||
@QueryParam(GeneralConstants.RELAY_STATE) String relayState) {
|
||||
return new RedirectBinding().execute(samlRequest, samlResponse, relayState);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*/
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
public Response postBinding(@FormParam(GeneralConstants.SAML_REQUEST_KEY) String samlRequest,
|
||||
@FormParam(GeneralConstants.SAML_RESPONSE_KEY) String samlResponse,
|
||||
@FormParam(GeneralConstants.RELAY_STATE) String relayState) {
|
||||
return new PostBinding().execute(samlRequest, samlResponse, relayState);
|
||||
}
|
||||
|
||||
protected abstract class Binding {
|
||||
private boolean checkSsl() {
|
||||
if (uriInfo.getBaseUri().getScheme().equals("https")) {
|
||||
return true;
|
||||
} else {
|
||||
return !realm.getSslRequired().isRequired(clientConnection);
|
||||
}
|
||||
}
|
||||
|
||||
protected Response basicChecks(String samlRequest, String samlResponse) {
|
||||
if (!checkSsl()) {
|
||||
event.event(EventType.LOGIN);
|
||||
event.error(Errors.SSL_REQUIRED);
|
||||
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.HTTPS_REQUIRED);
|
||||
}
|
||||
if (!realm.isEnabled()) {
|
||||
event.event(EventType.LOGIN_ERROR);
|
||||
event.error(Errors.REALM_DISABLED);
|
||||
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.REALM_NOT_ENABLED);
|
||||
}
|
||||
|
||||
if (samlRequest == null && samlResponse == null) {
|
||||
event.event(EventType.LOGIN);
|
||||
event.error(Errors.INVALID_REQUEST);
|
||||
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.INVALID_REQUEST );
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected abstract String getBindingType();
|
||||
protected abstract void verifySignature(SAMLDocumentHolder documentHolder) throws VerificationException;
|
||||
protected abstract SAMLDocumentHolder extractRequestDocument(String samlRequest);
|
||||
protected abstract SAMLDocumentHolder extractResponseDocument(String response);
|
||||
protected PublicKey getIDPKey() {
|
||||
X509Certificate certificate = null;
|
||||
try {
|
||||
certificate = XMLSignatureUtil.getX509CertificateFromKeyInfoString(config.getSigningCertificate().replaceAll("\\s", ""));
|
||||
} catch (ProcessingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return certificate.getPublicKey();
|
||||
}
|
||||
|
||||
public Response execute(String samlRequest, String samlResponse, String relayState) {
|
||||
event = new EventBuilder(realm, session, clientConnection);
|
||||
Response response = basicChecks(samlRequest, samlResponse);
|
||||
if (response != null) return response;
|
||||
if (samlRequest != null) return handleSamlRequest(samlRequest, relayState);
|
||||
else return handleSamlResponse(samlResponse, relayState);
|
||||
}
|
||||
|
||||
protected Response handleSamlRequest(String samlRequest, String relayState) {
|
||||
SAMLDocumentHolder holder = extractRequestDocument(samlRequest);
|
||||
RequestAbstractType requestAbstractType = (RequestAbstractType) holder.getSamlObject();
|
||||
// validate destination
|
||||
if (!uriInfo.getAbsolutePath().equals(requestAbstractType.getDestination())) {
|
||||
event.event(EventType.IDENTITY_PROVIDER_RESPONSE);
|
||||
event.error(Errors.INVALID_SAML_RESPONSE);
|
||||
event.detail(Details.REASON, "invalid_destination");
|
||||
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.INVALID_REQUEST);
|
||||
}
|
||||
if (config.isValidateSignature()) {
|
||||
try {
|
||||
verifySignature(holder);
|
||||
} catch (VerificationException e) {
|
||||
logger.error("validation failed", e);
|
||||
event.event(EventType.IDENTITY_PROVIDER_RESPONSE);
|
||||
event.error(Errors.INVALID_SIGNATURE);
|
||||
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.INVALID_REQUESTER);
|
||||
}
|
||||
}
|
||||
|
||||
if (requestAbstractType instanceof LogoutRequestType) {
|
||||
logger.debug("** logout request");
|
||||
event.event(EventType.LOGOUT);
|
||||
LogoutRequestType logout = (LogoutRequestType) requestAbstractType;
|
||||
return logoutRequest(logout, relayState);
|
||||
|
||||
} else {
|
||||
event.event(EventType.LOGIN);
|
||||
event.error(Errors.INVALID_TOKEN);
|
||||
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.INVALID_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
protected Response logoutRequest(LogoutRequestType request, String relayState) {
|
||||
String brokerUserId = config.getAlias() + "." + request.getNameID().getValue();
|
||||
if (request.getSessionIndex() == null || request.getSessionIndex().isEmpty()) {
|
||||
List<UserSessionModel> userSessions = session.sessions().getUserSessionByBrokerUserId(realm, brokerUserId);
|
||||
for (UserSessionModel userSession : userSessions) {
|
||||
try {
|
||||
AuthenticationManager.backchannelLogout(session, realm, userSession, uriInfo, clientConnection, headers);
|
||||
} catch (Exception e) {
|
||||
logger.warn("failed to do backchannel logout for userSession", e);
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
for (String sessionIndex : request.getSessionIndex()) {
|
||||
String brokerSessionId = brokerUserId + "." + sessionIndex;
|
||||
UserSessionModel userSession = session.sessions().getUserSessionByBrokerSessionId(realm, brokerSessionId);
|
||||
if (userSession != null) {
|
||||
try {
|
||||
AuthenticationManager.backchannelLogout(session, realm, userSession, uriInfo, clientConnection, headers);
|
||||
} catch (Exception e) {
|
||||
logger.warn("failed to do backchannel logout for userSession", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String issuerURL = getEntityId(uriInfo, realm);
|
||||
SAML2LogoutResponseBuilder builder = new SAML2LogoutResponseBuilder();
|
||||
builder.logoutRequestID(request.getID());
|
||||
builder.destination(config.getSingleLogoutServiceUrl());
|
||||
builder.issuer(issuerURL);
|
||||
builder.relayState(relayState);
|
||||
if (config.isWantAuthnRequestsSigned()) {
|
||||
builder.signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate())
|
||||
.signDocument();
|
||||
}
|
||||
try {
|
||||
if (config.isPostBindingResponse()) {
|
||||
return builder.postBinding().response();
|
||||
} else {
|
||||
return builder.redirectBinding().response();
|
||||
}
|
||||
} catch (ConfigurationException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (ProcessingException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private String getEntityId(UriInfo uriInfo, RealmModel realm) {
|
||||
return UriBuilder.fromUri(uriInfo.getBaseUri()).path("realms").path(realm.getName()).build().toString();
|
||||
}
|
||||
protected Response handleLoginResponse(String samlResponse, SAMLDocumentHolder holder, ResponseType responseType, String relayState) {
|
||||
|
||||
try {
|
||||
AssertionType assertion = getAssertion(responseType);
|
||||
SubjectType subject = assertion.getSubject();
|
||||
SubjectType.STSubType subType = subject.getSubType();
|
||||
NameIDType subjectNameID = (NameIDType) subType.getBaseID();
|
||||
Map<String, String> notes = new HashMap<>();
|
||||
notes.put(SAML_FEDERATED_SUBJECT, subjectNameID.getValue());
|
||||
if (subjectNameID.getFormat() != null) notes.put(SAML_FEDERATED_SUBJECT_NAMEFORMAT, subjectNameID.getFormat().toString());
|
||||
FederatedIdentity identity = new FederatedIdentity(subjectNameID.getValue());
|
||||
|
||||
identity.setUsername(subjectNameID.getValue());
|
||||
|
||||
if (subjectNameID.getFormat().toString().equals(JBossSAMLURIConstants.NAMEID_FORMAT_EMAIL.get())) {
|
||||
identity.setEmail(subjectNameID.getValue());
|
||||
}
|
||||
|
||||
if (config.isStoreToken()) {
|
||||
identity.setToken(samlResponse);
|
||||
}
|
||||
|
||||
AuthnStatementType authn = null;
|
||||
for (Object statement : assertion.getStatements()) {
|
||||
if (statement instanceof AuthnStatementType) {
|
||||
authn = (AuthnStatementType)statement;
|
||||
break;
|
||||
}
|
||||
}
|
||||
String brokerUserId = config.getAlias() + "." + subjectNameID.getValue();
|
||||
identity.setBrokerUserId(brokerUserId);
|
||||
if (authn != null && authn.getSessionIndex() != null) {
|
||||
identity.setBrokerSessionId(identity.getBrokerUserId() + "." + authn.getSessionIndex());
|
||||
notes.put(SAML_FEDERATED_SESSION_INDEX, authn.getSessionIndex());
|
||||
}
|
||||
return callback.authenticated(notes, config, identity, relayState);
|
||||
|
||||
} catch (Exception e) {
|
||||
throw new IdentityBrokerException("Could not process response from SAML identity provider.", e);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
private AssertionType getAssertion(ResponseType responseType) throws ProcessingException {
|
||||
List<ResponseType.RTChoiceType> assertions = responseType.getAssertions();
|
||||
|
||||
if (assertions.isEmpty()) {
|
||||
throw new IdentityBrokerException("No assertion from response.");
|
||||
}
|
||||
|
||||
ResponseType.RTChoiceType rtChoiceType = assertions.get(0);
|
||||
EncryptedAssertionType encryptedAssertion = rtChoiceType.getEncryptedAssertion();
|
||||
|
||||
if (encryptedAssertion != null) {
|
||||
decryptAssertion(responseType, realm.getPrivateKey());
|
||||
|
||||
}
|
||||
return responseType.getAssertions().get(0).getAssertion();
|
||||
}
|
||||
|
||||
public Response handleSamlResponse(String samlResponse, String relayState) {
|
||||
SAMLDocumentHolder holder = extractResponseDocument(samlResponse);
|
||||
StatusResponseType statusResponse = (StatusResponseType)holder.getSamlObject();
|
||||
// validate destination
|
||||
if (!uriInfo.getAbsolutePath().toString().equals(statusResponse.getDestination())) {
|
||||
event.event(EventType.IDENTITY_PROVIDER_RESPONSE);
|
||||
event.error(Errors.INVALID_SAML_RESPONSE);
|
||||
event.detail(Details.REASON, "invalid_destination");
|
||||
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.INVALID_FEDERATED_IDENTITY_ACTION);
|
||||
}
|
||||
if (config.isValidateSignature()) {
|
||||
try {
|
||||
verifySignature(holder);
|
||||
} catch (VerificationException e) {
|
||||
logger.error("validation failed", e);
|
||||
event.event(EventType.IDENTITY_PROVIDER_RESPONSE);
|
||||
event.error(Errors.INVALID_SIGNATURE);
|
||||
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.INVALID_FEDERATED_IDENTITY_ACTION);
|
||||
}
|
||||
}
|
||||
if (statusResponse instanceof ResponseType) {
|
||||
return handleLoginResponse(samlResponse, holder, (ResponseType)statusResponse, relayState);
|
||||
|
||||
} else {
|
||||
// todo need to check that it is actually a LogoutResponse
|
||||
return handleLogoutResponse(holder, statusResponse, relayState);
|
||||
}
|
||||
//throw new RuntimeException("Unknown response type");
|
||||
|
||||
}
|
||||
|
||||
protected Response handleLogoutResponse(SAMLDocumentHolder holder, StatusResponseType responseType, String relayState) {
|
||||
if (relayState == null) {
|
||||
logger.error("no valid user session");
|
||||
event.event(EventType.LOGOUT);
|
||||
event.error(Errors.USER_SESSION_NOT_FOUND);
|
||||
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR);
|
||||
}
|
||||
UserSessionModel userSession = session.sessions().getUserSession(realm, relayState);
|
||||
if (userSession == null) {
|
||||
logger.error("no valid user session");
|
||||
event.event(EventType.LOGOUT);
|
||||
event.error(Errors.USER_SESSION_NOT_FOUND);
|
||||
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR);
|
||||
}
|
||||
if (userSession.getState() != UserSessionModel.State.LOGGING_OUT) {
|
||||
logger.error("usersession in different state");
|
||||
event.event(EventType.LOGOUT);
|
||||
event.error(Errors.USER_SESSION_NOT_FOUND);
|
||||
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.SESSION_NOT_ACTIVE);
|
||||
}
|
||||
return AuthenticationManager.finishBrowserLogout(session, realm, userSession, uriInfo, clientConnection, headers);
|
||||
}
|
||||
|
||||
|
||||
protected ResponseType decryptAssertion(ResponseType responseType, PrivateKey privateKey) throws ProcessingException {
|
||||
SAML2Response saml2Response = new SAML2Response();
|
||||
|
||||
try {
|
||||
Document doc = saml2Response.convert(responseType);
|
||||
Element enc = DocumentUtil.getElement(doc, new QName(JBossSAMLConstants.ENCRYPTED_ASSERTION.get()));
|
||||
|
||||
if (enc == null) {
|
||||
throw new IdentityBrokerException("No encrypted assertion found.");
|
||||
}
|
||||
|
||||
String oldID = enc.getAttribute(JBossSAMLConstants.ID.get());
|
||||
Document newDoc = DocumentUtil.createDocument();
|
||||
Node importedNode = newDoc.importNode(enc, true);
|
||||
newDoc.appendChild(importedNode);
|
||||
|
||||
Element decryptedDocumentElement = XMLEncryptionUtil.decryptElementInDocument(newDoc, privateKey);
|
||||
SAMLParser parser = new SAMLParser();
|
||||
|
||||
JAXPValidationUtil.checkSchemaValidation(decryptedDocumentElement);
|
||||
AssertionType assertion = (AssertionType) parser.parse(StaxParserUtil.getXMLEventReader(DocumentUtil
|
||||
.getNodeAsStream(decryptedDocumentElement)));
|
||||
|
||||
responseType.replaceAssertion(oldID, new ResponseType.RTChoiceType(assertion));
|
||||
|
||||
return responseType;
|
||||
} catch (Exception e) {
|
||||
throw new IdentityBrokerException("Could not decrypt assertion.", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
protected class PostBinding extends Binding {
|
||||
@Override
|
||||
protected void verifySignature(SAMLDocumentHolder documentHolder) throws VerificationException {
|
||||
SamlProtocolUtils.verifyDocumentSignature(documentHolder.getSamlDocument(), getIDPKey());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SAMLDocumentHolder extractRequestDocument(String samlRequest) {
|
||||
return SAMLRequestParser.parseRequestPostBinding(samlRequest);
|
||||
}
|
||||
@Override
|
||||
protected SAMLDocumentHolder extractResponseDocument(String response) {
|
||||
return SAMLRequestParser.parseResponsePostBinding(response);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getBindingType() {
|
||||
return SamlProtocol.SAML_POST_BINDING;
|
||||
}
|
||||
}
|
||||
|
||||
protected class RedirectBinding extends Binding {
|
||||
@Override
|
||||
protected void verifySignature(SAMLDocumentHolder documentHolder) throws VerificationException {
|
||||
PublicKey publicKey = getIDPKey();
|
||||
SamlProtocolUtils.verifyRedirectSignature(publicKey, uriInfo);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
protected SAMLDocumentHolder extractRequestDocument(String samlRequest) {
|
||||
return SAMLRequestParser.parseRequestRedirectBinding(samlRequest);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SAMLDocumentHolder extractResponseDocument(String response) {
|
||||
return SAMLRequestParser.parseRequestRedirectBinding(response);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getBindingType() {
|
||||
return SamlProtocol.SAML_REDIRECT_BINDING;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
package org.keycloak.broker.saml;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.ClientConnection;
|
||||
import org.keycloak.VerificationException;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.broker.provider.IdentityBrokerException;
|
||||
import org.keycloak.broker.provider.IdentityProvider;
|
||||
import org.keycloak.dom.saml.v2.assertion.AssertionType;
|
||||
import org.keycloak.dom.saml.v2.assertion.AttributeStatementType;
|
||||
import org.keycloak.dom.saml.v2.assertion.AttributeType;
|
||||
import org.keycloak.dom.saml.v2.assertion.AuthnStatementType;
|
||||
import org.keycloak.dom.saml.v2.assertion.EncryptedAssertionType;
|
||||
import org.keycloak.dom.saml.v2.assertion.NameIDType;
|
||||
import org.keycloak.dom.saml.v2.assertion.SubjectType;
|
||||
import org.keycloak.dom.saml.v2.protocol.LogoutRequestType;
|
||||
import org.keycloak.dom.saml.v2.protocol.RequestAbstractType;
|
||||
import org.keycloak.dom.saml.v2.protocol.ResponseType;
|
||||
import org.keycloak.dom.saml.v2.protocol.StatusResponseType;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.protocol.saml.SAML2LogoutResponseBuilder;
|
||||
import org.keycloak.protocol.saml.SAMLRequestParser;
|
||||
import org.keycloak.protocol.saml.SamlProtocol;
|
||||
import org.keycloak.protocol.saml.SamlProtocolUtils;
|
||||
import org.keycloak.saml.common.constants.GeneralConstants;
|
||||
import org.keycloak.saml.common.constants.JBossSAMLConstants;
|
||||
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
|
||||
import org.keycloak.saml.common.exceptions.ConfigurationException;
|
||||
import org.keycloak.saml.common.exceptions.ProcessingException;
|
||||
import org.keycloak.saml.common.util.DocumentUtil;
|
||||
import org.keycloak.saml.common.util.StaxParserUtil;
|
||||
import org.keycloak.saml.processing.api.saml.v2.response.SAML2Response;
|
||||
import org.keycloak.saml.processing.core.parsers.saml.SAMLParser;
|
||||
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
|
||||
import org.keycloak.saml.processing.core.saml.v2.constants.X500SAMLProfileConstants;
|
||||
import org.keycloak.saml.processing.core.util.JAXPValidationUtil;
|
||||
import org.keycloak.saml.processing.core.util.XMLEncryptionUtil;
|
||||
import org.keycloak.saml.processing.core.util.XMLSignatureUtil;
|
||||
import org.keycloak.saml.processing.web.util.PostBindingUtil;
|
||||
import org.keycloak.services.ErrorPage;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.Node;
|
||||
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.FormParam;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.HttpHeaders;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriBuilder;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
import javax.xml.namespace.QName;
|
||||
import java.io.IOException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class SAMLEndpoint {
|
||||
protected static final Logger logger = Logger.getLogger(SAMLEndpoint.class);
|
||||
public static final String SAML_FEDERATED_SESSION_INDEX = "SAML_FEDERATED_SESSION_INDEX";
|
||||
public static final String SAML_FEDERATED_SUBJECT = "SAML_FEDERATED_SUBJECT";
|
||||
public static final String SAML_FEDERATED_SUBJECT_NAMEFORMAT = "SAML_FEDERATED_SUBJECT_NAMEFORMAT";
|
||||
public static final String SAML_LOGIN_RESPONSE = "SAML_LOGIN_RESPONSE";
|
||||
public static final String SAML_ASSERTION = "SAML_ASSERTION";
|
||||
public static final String SAML_AUTHN_STATEMENT = "SAML_AUTHN_STATEMENT";
|
||||
protected RealmModel realm;
|
||||
protected EventBuilder event;
|
||||
protected SAMLIdentityProviderConfig config;
|
||||
protected IdentityProvider.AuthenticationCallback callback;
|
||||
protected SAMLIdentityProvider provider;
|
||||
|
||||
@Context
|
||||
private UriInfo uriInfo;
|
||||
|
||||
@Context
|
||||
private KeycloakSession session;
|
||||
|
||||
@Context
|
||||
private ClientConnection clientConnection;
|
||||
|
||||
@Context
|
||||
private HttpHeaders headers;
|
||||
|
||||
|
||||
public SAMLEndpoint(RealmModel realm, SAMLIdentityProvider provider, SAMLIdentityProviderConfig config, IdentityProvider.AuthenticationCallback callback) {
|
||||
this.realm = realm;
|
||||
this.config = config;
|
||||
this.callback = callback;
|
||||
this.provider = provider;
|
||||
}
|
||||
|
||||
@GET
|
||||
public Response redirectBinding(@QueryParam(GeneralConstants.SAML_REQUEST_KEY) String samlRequest,
|
||||
@QueryParam(GeneralConstants.SAML_RESPONSE_KEY) String samlResponse,
|
||||
@QueryParam(GeneralConstants.RELAY_STATE) String relayState) {
|
||||
return new RedirectBinding().execute(samlRequest, samlResponse, relayState);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*/
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
public Response postBinding(@FormParam(GeneralConstants.SAML_REQUEST_KEY) String samlRequest,
|
||||
@FormParam(GeneralConstants.SAML_RESPONSE_KEY) String samlResponse,
|
||||
@FormParam(GeneralConstants.RELAY_STATE) String relayState) {
|
||||
return new PostBinding().execute(samlRequest, samlResponse, relayState);
|
||||
}
|
||||
|
||||
protected abstract class Binding {
|
||||
private boolean checkSsl() {
|
||||
if (uriInfo.getBaseUri().getScheme().equals("https")) {
|
||||
return true;
|
||||
} else {
|
||||
return !realm.getSslRequired().isRequired(clientConnection);
|
||||
}
|
||||
}
|
||||
|
||||
protected Response basicChecks(String samlRequest, String samlResponse) {
|
||||
if (!checkSsl()) {
|
||||
event.event(EventType.LOGIN);
|
||||
event.error(Errors.SSL_REQUIRED);
|
||||
return ErrorPage.error(session, Messages.HTTPS_REQUIRED);
|
||||
}
|
||||
if (!realm.isEnabled()) {
|
||||
event.event(EventType.LOGIN_ERROR);
|
||||
event.error(Errors.REALM_DISABLED);
|
||||
return ErrorPage.error(session, Messages.REALM_NOT_ENABLED);
|
||||
}
|
||||
|
||||
if (samlRequest == null && samlResponse == null) {
|
||||
event.event(EventType.LOGIN);
|
||||
event.error(Errors.INVALID_REQUEST);
|
||||
return ErrorPage.error(session, Messages.INVALID_REQUEST);
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected abstract String getBindingType();
|
||||
protected abstract void verifySignature(SAMLDocumentHolder documentHolder) throws VerificationException;
|
||||
protected abstract SAMLDocumentHolder extractRequestDocument(String samlRequest);
|
||||
protected abstract SAMLDocumentHolder extractResponseDocument(String response);
|
||||
protected PublicKey getIDPKey() {
|
||||
X509Certificate certificate = null;
|
||||
try {
|
||||
certificate = XMLSignatureUtil.getX509CertificateFromKeyInfoString(config.getSigningCertificate().replaceAll("\\s", ""));
|
||||
} catch (ProcessingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return certificate.getPublicKey();
|
||||
}
|
||||
|
||||
public Response execute(String samlRequest, String samlResponse, String relayState) {
|
||||
event = new EventBuilder(realm, session, clientConnection);
|
||||
Response response = basicChecks(samlRequest, samlResponse);
|
||||
if (response != null) return response;
|
||||
if (samlRequest != null) return handleSamlRequest(samlRequest, relayState);
|
||||
else return handleSamlResponse(samlResponse, relayState);
|
||||
}
|
||||
|
||||
protected Response handleSamlRequest(String samlRequest, String relayState) {
|
||||
SAMLDocumentHolder holder = extractRequestDocument(samlRequest);
|
||||
RequestAbstractType requestAbstractType = (RequestAbstractType) holder.getSamlObject();
|
||||
// validate destination
|
||||
if (requestAbstractType.getDestination() != null && !uriInfo.getAbsolutePath().equals(requestAbstractType.getDestination())) {
|
||||
event.event(EventType.IDENTITY_PROVIDER_RESPONSE);
|
||||
event.detail(Details.REASON, "invalid_destination");
|
||||
event.error(Errors.INVALID_SAML_RESPONSE);
|
||||
return ErrorPage.error(session, Messages.INVALID_REQUEST);
|
||||
}
|
||||
if (config.isValidateSignature()) {
|
||||
try {
|
||||
verifySignature(holder);
|
||||
} catch (VerificationException e) {
|
||||
logger.error("validation failed", e);
|
||||
event.event(EventType.IDENTITY_PROVIDER_RESPONSE);
|
||||
event.error(Errors.INVALID_SIGNATURE);
|
||||
return ErrorPage.error(session, Messages.INVALID_REQUESTER);
|
||||
}
|
||||
}
|
||||
|
||||
if (requestAbstractType instanceof LogoutRequestType) {
|
||||
logger.debug("** logout request");
|
||||
event.event(EventType.LOGOUT);
|
||||
LogoutRequestType logout = (LogoutRequestType) requestAbstractType;
|
||||
return logoutRequest(logout, relayState);
|
||||
|
||||
} else {
|
||||
event.event(EventType.LOGIN);
|
||||
event.error(Errors.INVALID_TOKEN);
|
||||
return ErrorPage.error(session, Messages.INVALID_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
protected Response logoutRequest(LogoutRequestType request, String relayState) {
|
||||
String brokerUserId = config.getAlias() + "." + request.getNameID().getValue();
|
||||
if (request.getSessionIndex() == null || request.getSessionIndex().isEmpty()) {
|
||||
List<UserSessionModel> userSessions = session.sessions().getUserSessionByBrokerUserId(realm, brokerUserId);
|
||||
for (UserSessionModel userSession : userSessions) {
|
||||
if (userSession.getState() == UserSessionModel.State.LOGGING_OUT || userSession.getState() == UserSessionModel.State.LOGGED_OUT) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
AuthenticationManager.backchannelLogout(session, realm, userSession, uriInfo, clientConnection, headers, false);
|
||||
} catch (Exception e) {
|
||||
logger.warn("failed to do backchannel logout for userSession", e);
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
for (String sessionIndex : request.getSessionIndex()) {
|
||||
String brokerSessionId = brokerUserId + "." + sessionIndex;
|
||||
UserSessionModel userSession = session.sessions().getUserSessionByBrokerSessionId(realm, brokerSessionId);
|
||||
if (userSession != null) {
|
||||
if (userSession.getState() == UserSessionModel.State.LOGGING_OUT || userSession.getState() == UserSessionModel.State.LOGGED_OUT) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
AuthenticationManager.backchannelLogout(session, realm, userSession, uriInfo, clientConnection, headers, false);
|
||||
} catch (Exception e) {
|
||||
logger.warn("failed to do backchannel logout for userSession", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String issuerURL = getEntityId(uriInfo, realm);
|
||||
SAML2LogoutResponseBuilder builder = new SAML2LogoutResponseBuilder();
|
||||
builder.logoutRequestID(request.getID());
|
||||
builder.destination(config.getSingleLogoutServiceUrl());
|
||||
builder.issuer(issuerURL);
|
||||
builder.relayState(relayState);
|
||||
if (config.isWantAuthnRequestsSigned()) {
|
||||
builder.signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate())
|
||||
.signDocument();
|
||||
}
|
||||
try {
|
||||
if (config.isPostBindingResponse()) {
|
||||
return builder.postBinding().response();
|
||||
} else {
|
||||
return builder.redirectBinding().response();
|
||||
}
|
||||
} catch (ConfigurationException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (ProcessingException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private String getEntityId(UriInfo uriInfo, RealmModel realm) {
|
||||
return UriBuilder.fromUri(uriInfo.getBaseUri()).path("realms").path(realm.getName()).build().toString();
|
||||
}
|
||||
protected Response handleLoginResponse(String samlResponse, SAMLDocumentHolder holder, ResponseType responseType, String relayState) {
|
||||
|
||||
try {
|
||||
AssertionType assertion = getAssertion(responseType);
|
||||
SubjectType subject = assertion.getSubject();
|
||||
SubjectType.STSubType subType = subject.getSubType();
|
||||
NameIDType subjectNameID = (NameIDType) subType.getBaseID();
|
||||
//Map<String, String> notes = new HashMap<>();
|
||||
BrokeredIdentityContext identity = new BrokeredIdentityContext(subjectNameID.getValue());
|
||||
identity.setCode(relayState);
|
||||
identity.getContextData().put(SAML_LOGIN_RESPONSE, responseType);
|
||||
identity.getContextData().put(SAML_ASSERTION, assertion);
|
||||
|
||||
identity.setUsername(subjectNameID.getValue());
|
||||
|
||||
//SAML Spec 2.2.2 Format is optional
|
||||
if (subjectNameID.getFormat() != null && subjectNameID.getFormat().toString().equals(JBossSAMLURIConstants.NAMEID_FORMAT_EMAIL.get())) {
|
||||
identity.setEmail(subjectNameID.getValue());
|
||||
}
|
||||
|
||||
if (config.isStoreToken()) {
|
||||
identity.setToken(samlResponse);
|
||||
}
|
||||
|
||||
AuthnStatementType authn = null;
|
||||
for (Object statement : assertion.getStatements()) {
|
||||
if (statement instanceof AuthnStatementType) {
|
||||
authn = (AuthnStatementType)statement;
|
||||
identity.getContextData().put(SAML_AUTHN_STATEMENT, authn);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (assertion.getAttributeStatements() != null ) {
|
||||
for (AttributeStatementType attrStatement : assertion.getAttributeStatements()) {
|
||||
for (AttributeStatementType.ASTChoiceType choice : attrStatement.getAttributes()) {
|
||||
AttributeType attribute = choice.getAttribute();
|
||||
if (X500SAMLProfileConstants.EMAIL.getFriendlyName().equals(attribute.getFriendlyName())
|
||||
|| X500SAMLProfileConstants.EMAIL.get().equals(attribute.getName())) {
|
||||
if (!attribute.getAttributeValue().isEmpty()) identity.setEmail(attribute.getAttributeValue().get(0).toString());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
String brokerUserId = config.getAlias() + "." + subjectNameID.getValue();
|
||||
identity.setBrokerUserId(brokerUserId);
|
||||
identity.setIdpConfig(config);
|
||||
identity.setIdp(provider);
|
||||
if (authn != null && authn.getSessionIndex() != null) {
|
||||
identity.setBrokerSessionId(identity.getBrokerUserId() + "." + authn.getSessionIndex());
|
||||
}
|
||||
|
||||
|
||||
return callback.authenticated(identity);
|
||||
|
||||
} catch (Exception e) {
|
||||
throw new IdentityBrokerException("Could not process response from SAML identity provider.", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private AssertionType getAssertion(ResponseType responseType) throws ProcessingException {
|
||||
List<ResponseType.RTChoiceType> assertions = responseType.getAssertions();
|
||||
|
||||
if (assertions.isEmpty()) {
|
||||
throw new IdentityBrokerException("No assertion from response.");
|
||||
}
|
||||
|
||||
ResponseType.RTChoiceType rtChoiceType = assertions.get(0);
|
||||
EncryptedAssertionType encryptedAssertion = rtChoiceType.getEncryptedAssertion();
|
||||
|
||||
if (encryptedAssertion != null) {
|
||||
decryptAssertion(responseType, realm.getPrivateKey());
|
||||
|
||||
}
|
||||
return responseType.getAssertions().get(0).getAssertion();
|
||||
}
|
||||
|
||||
public Response handleSamlResponse(String samlResponse, String relayState) {
|
||||
SAMLDocumentHolder holder = extractResponseDocument(samlResponse);
|
||||
StatusResponseType statusResponse = (StatusResponseType)holder.getSamlObject();
|
||||
// validate destination
|
||||
if (statusResponse.getDestination() != null && !uriInfo.getAbsolutePath().toString().equals(statusResponse.getDestination())) {
|
||||
event.event(EventType.IDENTITY_PROVIDER_RESPONSE);
|
||||
event.detail(Details.REASON, "invalid_destination");
|
||||
event.error(Errors.INVALID_SAML_RESPONSE);
|
||||
return ErrorPage.error(session, Messages.INVALID_FEDERATED_IDENTITY_ACTION);
|
||||
}
|
||||
if (config.isValidateSignature()) {
|
||||
try {
|
||||
verifySignature(holder);
|
||||
} catch (VerificationException e) {
|
||||
logger.error("validation failed", e);
|
||||
event.event(EventType.IDENTITY_PROVIDER_RESPONSE);
|
||||
event.error(Errors.INVALID_SIGNATURE);
|
||||
return ErrorPage.error(session, Messages.INVALID_FEDERATED_IDENTITY_ACTION);
|
||||
}
|
||||
}
|
||||
if (statusResponse instanceof ResponseType) {
|
||||
return handleLoginResponse(samlResponse, holder, (ResponseType)statusResponse, relayState);
|
||||
|
||||
} else {
|
||||
// todo need to check that it is actually a LogoutResponse
|
||||
return handleLogoutResponse(holder, statusResponse, relayState);
|
||||
}
|
||||
//throw new RuntimeException("Unknown response type");
|
||||
|
||||
}
|
||||
|
||||
protected Response handleLogoutResponse(SAMLDocumentHolder holder, StatusResponseType responseType, String relayState) {
|
||||
if (relayState == null) {
|
||||
logger.error("no valid user session");
|
||||
event.event(EventType.LOGOUT);
|
||||
event.error(Errors.USER_SESSION_NOT_FOUND);
|
||||
return ErrorPage.error(session, Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR);
|
||||
}
|
||||
UserSessionModel userSession = session.sessions().getUserSession(realm, relayState);
|
||||
if (userSession == null) {
|
||||
logger.error("no valid user session");
|
||||
event.event(EventType.LOGOUT);
|
||||
event.error(Errors.USER_SESSION_NOT_FOUND);
|
||||
return ErrorPage.error(session, Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR);
|
||||
}
|
||||
if (userSession.getState() != UserSessionModel.State.LOGGING_OUT) {
|
||||
logger.error("usersession in different state");
|
||||
event.event(EventType.LOGOUT);
|
||||
event.error(Errors.USER_SESSION_NOT_FOUND);
|
||||
return ErrorPage.error(session, Messages.SESSION_NOT_ACTIVE);
|
||||
}
|
||||
return AuthenticationManager.finishBrowserLogout(session, realm, userSession, uriInfo, clientConnection, headers);
|
||||
}
|
||||
|
||||
|
||||
protected ResponseType decryptAssertion(ResponseType responseType, PrivateKey privateKey) throws ProcessingException {
|
||||
SAML2Response saml2Response = new SAML2Response();
|
||||
|
||||
try {
|
||||
Document doc = saml2Response.convert(responseType);
|
||||
Element enc = DocumentUtil.getElement(doc, new QName(JBossSAMLConstants.ENCRYPTED_ASSERTION.get()));
|
||||
|
||||
if (enc == null) {
|
||||
throw new IdentityBrokerException("No encrypted assertion found.");
|
||||
}
|
||||
|
||||
String oldID = enc.getAttribute(JBossSAMLConstants.ID.get());
|
||||
Document newDoc = DocumentUtil.createDocument();
|
||||
Node importedNode = newDoc.importNode(enc, true);
|
||||
newDoc.appendChild(importedNode);
|
||||
|
||||
Element decryptedDocumentElement = XMLEncryptionUtil.decryptElementInDocument(newDoc, privateKey);
|
||||
SAMLParser parser = new SAMLParser();
|
||||
|
||||
JAXPValidationUtil.checkSchemaValidation(decryptedDocumentElement);
|
||||
AssertionType assertion = (AssertionType) parser.parse(StaxParserUtil.getXMLEventReader(DocumentUtil
|
||||
.getNodeAsStream(decryptedDocumentElement)));
|
||||
|
||||
responseType.replaceAssertion(oldID, new ResponseType.RTChoiceType(assertion));
|
||||
|
||||
return responseType;
|
||||
} catch (Exception e) {
|
||||
throw new IdentityBrokerException("Could not decrypt assertion.", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
protected class PostBinding extends Binding {
|
||||
@Override
|
||||
protected void verifySignature(SAMLDocumentHolder documentHolder) throws VerificationException {
|
||||
SamlProtocolUtils.verifyDocumentSignature(documentHolder.getSamlDocument(), getIDPKey());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SAMLDocumentHolder extractRequestDocument(String samlRequest) {
|
||||
return SAMLRequestParser.parseRequestPostBinding(samlRequest);
|
||||
}
|
||||
@Override
|
||||
protected SAMLDocumentHolder extractResponseDocument(String response) {
|
||||
byte[] samlBytes = PostBindingUtil.base64Decode(response);
|
||||
String xml = new String(samlBytes);
|
||||
return SAMLRequestParser.parseResponseDocument(samlBytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getBindingType() {
|
||||
return SamlProtocol.SAML_POST_BINDING;
|
||||
}
|
||||
}
|
||||
|
||||
protected class RedirectBinding extends Binding {
|
||||
@Override
|
||||
protected void verifySignature(SAMLDocumentHolder documentHolder) throws VerificationException {
|
||||
PublicKey publicKey = getIDPKey();
|
||||
SamlProtocolUtils.verifyRedirectSignature(publicKey, uriInfo);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
protected SAMLDocumentHolder extractRequestDocument(String samlRequest) {
|
||||
return SAMLRequestParser.parseRequestRedirectBinding(samlRequest);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SAMLDocumentHolder extractResponseDocument(String response) {
|
||||
return SAMLRequestParser.parseRequestRedirectBinding(response);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getBindingType() {
|
||||
return SamlProtocol.SAML_REDIRECT_BINDING;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -17,22 +17,36 @@
|
||||
*/
|
||||
package org.keycloak.broker.saml;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.broker.provider.AbstractIdentityProvider;
|
||||
import org.keycloak.broker.provider.AuthenticationRequest;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.broker.provider.IdentityBrokerException;
|
||||
import org.keycloak.broker.provider.util.SimpleHttp;
|
||||
import org.keycloak.dom.saml.v2.assertion.AssertionType;
|
||||
import org.keycloak.dom.saml.v2.assertion.AuthnStatementType;
|
||||
import org.keycloak.dom.saml.v2.assertion.NameIDType;
|
||||
import org.keycloak.dom.saml.v2.assertion.SubjectType;
|
||||
import org.keycloak.dom.saml.v2.protocol.ResponseType;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.models.ClientSessionModel;
|
||||
import org.keycloak.models.FederatedIdentityModel;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.protocol.saml.SAML2AuthnRequestBuilder;
|
||||
import org.keycloak.protocol.saml.SAML2LogoutRequestBuilder;
|
||||
import org.keycloak.protocol.saml.SAML2NameIDPolicyBuilder;
|
||||
import org.picketlink.common.constants.JBossSAMLURIConstants;
|
||||
import org.keycloak.saml.common.constants.GeneralConstants;
|
||||
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
|
||||
import org.keycloak.saml.common.exceptions.ConfigurationException;
|
||||
import org.keycloak.saml.common.exceptions.ParsingException;
|
||||
import org.keycloak.saml.common.exceptions.ProcessingException;
|
||||
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriBuilder;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
import java.io.IOException;
|
||||
import java.security.KeyPair;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
@@ -41,17 +55,18 @@ import java.security.PublicKey;
|
||||
* @author Pedro Igor
|
||||
*/
|
||||
public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityProviderConfig> {
|
||||
protected static final Logger logger = Logger.getLogger(SAMLIdentityProvider.class);
|
||||
public SAMLIdentityProvider(SAMLIdentityProviderConfig config) {
|
||||
super(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object callback(RealmModel realm, AuthenticationCallback callback, EventBuilder event) {
|
||||
return new SAMLEndpoint(realm, getConfig(), callback);
|
||||
return new SAMLEndpoint(realm, this, getConfig(), callback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response handleRequest(AuthenticationRequest request) {
|
||||
public Response performLogin(AuthenticationRequest request) {
|
||||
try {
|
||||
UriInfo uriInfo = request.getUriInfo();
|
||||
RealmModel realm = request.getRealm();
|
||||
@@ -112,30 +127,78 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
|
||||
return UriBuilder.fromUri(uriInfo.getBaseUri()).path("realms").path(realm.getName()).build().toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void attachUserSession(UserSessionModel userSession, ClientSessionModel clientSession, BrokeredIdentityContext context) {
|
||||
ResponseType responseType = (ResponseType)context.getContextData().get(SAMLEndpoint.SAML_LOGIN_RESPONSE);
|
||||
AssertionType assertion = (AssertionType)context.getContextData().get(SAMLEndpoint.SAML_ASSERTION);
|
||||
SubjectType subject = assertion.getSubject();
|
||||
SubjectType.STSubType subType = subject.getSubType();
|
||||
NameIDType subjectNameID = (NameIDType) subType.getBaseID();
|
||||
userSession.setNote(SAMLEndpoint.SAML_FEDERATED_SUBJECT, subjectNameID.getValue());
|
||||
if (subjectNameID.getFormat() != null) userSession.setNote(SAMLEndpoint.SAML_FEDERATED_SUBJECT_NAMEFORMAT, subjectNameID.getFormat().toString());
|
||||
AuthnStatementType authn = (AuthnStatementType)context.getContextData().get(SAMLEndpoint.SAML_AUTHN_STATEMENT);
|
||||
if (authn != null && authn.getSessionIndex() != null) {
|
||||
userSession.setNote(SAMLEndpoint.SAML_FEDERATED_SESSION_INDEX, authn.getSessionIndex());
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response retrieveToken(FederatedIdentityModel identity) {
|
||||
return Response.ok(identity.getToken()).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response keycloakInitiatedBrowserLogout(UserSessionModel userSession, UriInfo uriInfo, RealmModel realm) {
|
||||
if (getConfig().getSingleLogoutServiceUrl() == null || getConfig().getSingleLogoutServiceUrl().trim().equals("")) return null;
|
||||
public void backchannelLogout(UserSessionModel userSession, UriInfo uriInfo, RealmModel realm) {
|
||||
String singleLogoutServiceUrl = getConfig().getSingleLogoutServiceUrl();
|
||||
if (singleLogoutServiceUrl == null || singleLogoutServiceUrl.trim().equals("") || !getConfig().isBackchannelSupported()) return;
|
||||
SAML2LogoutRequestBuilder logoutBuilder = buildLogoutRequest(userSession, uriInfo, realm, singleLogoutServiceUrl);
|
||||
try {
|
||||
int status = SimpleHttp.doPost(singleLogoutServiceUrl)
|
||||
.param(GeneralConstants.SAML_REQUEST_KEY, logoutBuilder.postBinding().encoded())
|
||||
.param(GeneralConstants.RELAY_STATE, userSession.getId()).asStatus();
|
||||
boolean success = status >=200 && status < 400;
|
||||
if (!success) {
|
||||
logger.warn("Failed saml backchannel broker logout to: " + singleLogoutServiceUrl);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("Failed saml backchannel broker logout to: " + singleLogoutServiceUrl, e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response keycloakInitiatedBrowserLogout(UserSessionModel userSession, UriInfo uriInfo, RealmModel realm) {
|
||||
String singleLogoutServiceUrl = getConfig().getSingleLogoutServiceUrl();
|
||||
if (singleLogoutServiceUrl == null || singleLogoutServiceUrl.trim().equals("")) return null;
|
||||
|
||||
if (getConfig().isBackchannelSupported()) {
|
||||
backchannelLogout(userSession, uriInfo, realm);
|
||||
return null;
|
||||
} else {
|
||||
try {
|
||||
SAML2LogoutRequestBuilder logoutBuilder = buildLogoutRequest(userSession, uriInfo, realm, singleLogoutServiceUrl);
|
||||
return logoutBuilder.postBinding().request();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected SAML2LogoutRequestBuilder buildLogoutRequest(UserSessionModel userSession, UriInfo uriInfo, RealmModel realm, String singleLogoutServiceUrl) {
|
||||
SAML2LogoutRequestBuilder logoutBuilder = new SAML2LogoutRequestBuilder()
|
||||
.assertionExpiration(realm.getAccessCodeLifespan())
|
||||
.issuer(getEntityId(uriInfo, realm))
|
||||
.sessionIndex(userSession.getNote(SAMLEndpoint.SAML_FEDERATED_SESSION_INDEX))
|
||||
.userPrincipal(userSession.getNote(SAMLEndpoint.SAML_FEDERATED_SUBJECT), userSession.getNote(SAMLEndpoint.SAML_FEDERATED_SUBJECT_NAMEFORMAT))
|
||||
.destination(getConfig().getSingleLogoutServiceUrl());
|
||||
.destination(singleLogoutServiceUrl)
|
||||
.relayState(userSession.getId());
|
||||
if (getConfig().isWantAuthnRequestsSigned()) {
|
||||
logoutBuilder.signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate())
|
||||
.signDocument();
|
||||
}
|
||||
try {
|
||||
return logoutBuilder.relayState(userSession.getId()).postBinding().request();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
return logoutBuilder;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -110,4 +110,13 @@ public class SAMLIdentityProviderConfig extends IdentityProviderModel {
|
||||
public void setPostBindingResponse(boolean postBindingResponse) {
|
||||
getConfig().put("postBindingResponse", String.valueOf(postBindingResponse));
|
||||
}
|
||||
|
||||
public boolean isBackchannelSupported() {
|
||||
return Boolean.valueOf(getConfig().get("backchannelSupported"));
|
||||
}
|
||||
|
||||
public void setBackchannelSupported(boolean backchannel) {
|
||||
getConfig().put("backchannelSupported", String.valueOf(backchannel));
|
||||
}
|
||||
|
||||
}
|
||||
+15
-10
@@ -18,17 +18,21 @@
|
||||
package org.keycloak.broker.saml;
|
||||
|
||||
import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
|
||||
import org.keycloak.broker.saml.mappers.UsernameTemplateMapper;
|
||||
import org.keycloak.dom.saml.v2.metadata.EndpointType;
|
||||
import org.keycloak.dom.saml.v2.metadata.EntitiesDescriptorType;
|
||||
import org.keycloak.dom.saml.v2.metadata.EntityDescriptorType;
|
||||
import org.keycloak.dom.saml.v2.metadata.IDPSSODescriptorType;
|
||||
import org.keycloak.dom.saml.v2.metadata.KeyDescriptorType;
|
||||
import org.keycloak.dom.saml.v2.metadata.KeyTypes;
|
||||
import org.keycloak.models.IdentityProviderMapperModel;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.picketlink.common.constants.JBossSAMLURIConstants;
|
||||
import org.picketlink.common.exceptions.ParsingException;
|
||||
import org.picketlink.common.util.DocumentUtil;
|
||||
import org.picketlink.identity.federation.core.parsers.saml.SAMLParser;
|
||||
import org.picketlink.identity.federation.saml.v2.metadata.EndpointType;
|
||||
import org.picketlink.identity.federation.saml.v2.metadata.EntitiesDescriptorType;
|
||||
import org.picketlink.identity.federation.saml.v2.metadata.EntityDescriptorType;
|
||||
import org.picketlink.identity.federation.saml.v2.metadata.IDPSSODescriptorType;
|
||||
import org.picketlink.identity.federation.saml.v2.metadata.KeyDescriptorType;
|
||||
import org.picketlink.identity.federation.saml.v2.metadata.KeyTypes;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
|
||||
import org.keycloak.saml.common.exceptions.ParsingException;
|
||||
import org.keycloak.saml.common.util.DocumentUtil;
|
||||
import org.keycloak.saml.processing.core.parsers.saml.SAMLParser;
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
import javax.xml.namespace.QName;
|
||||
@@ -150,4 +154,5 @@ public class SAMLIdentityProviderFactory extends AbstractIdentityProviderFactory
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
}
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
package org.keycloak.broker.saml.mappers;
|
||||
|
||||
import org.keycloak.broker.provider.AbstractIdentityProviderMapper;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.broker.provider.HardcodedRoleMapper;
|
||||
import org.keycloak.broker.provider.IdentityBrokerException;
|
||||
import org.keycloak.broker.saml.SAMLEndpoint;
|
||||
import org.keycloak.broker.saml.SAMLIdentityProviderFactory;
|
||||
import org.keycloak.dom.saml.v2.assertion.AssertionType;
|
||||
import org.keycloak.dom.saml.v2.assertion.AttributeStatementType;
|
||||
import org.keycloak.dom.saml.v2.assertion.AttributeType;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.IdentityProviderMapperModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.representations.JsonWebToken;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class AttributeToRoleMapper extends AbstractIdentityProviderMapper {
|
||||
|
||||
public static final String[] COMPATIBLE_PROVIDERS = {SAMLIdentityProviderFactory.PROVIDER_ID};
|
||||
|
||||
private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
|
||||
|
||||
public static final String ATTRIBUTE_NAME = "attribute.name";
|
||||
public static final String ATTRIBUTE_FRIENDLY_NAME = "attribute.friendly.name";
|
||||
public static final String ATTRIBUTE_VALUE = "attribute.value";
|
||||
|
||||
static {
|
||||
ProviderConfigProperty property;
|
||||
property = new ProviderConfigProperty();
|
||||
property.setName(ATTRIBUTE_NAME);
|
||||
property.setLabel("Attribute Name");
|
||||
property.setHelpText("Name of attribute to search for in assertion. You can leave this blank and specify a friendly name instead.");
|
||||
property.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
configProperties.add(property);
|
||||
property = new ProviderConfigProperty();
|
||||
property.setName(ATTRIBUTE_FRIENDLY_NAME);
|
||||
property.setLabel("Friendly Name");
|
||||
property.setHelpText("Friendly name of attribute to search for in assertion. You can leave this blank and specify a name instead.");
|
||||
property.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
configProperties.add(property);
|
||||
property = new ProviderConfigProperty();
|
||||
property.setName(ATTRIBUTE_VALUE);
|
||||
property.setLabel("Attribute Value");
|
||||
property.setHelpText("Value the attribute must have. If the attribute is a list, then the value must be contained in the list.");
|
||||
property.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
configProperties.add(property);
|
||||
property = new ProviderConfigProperty();
|
||||
property.setName(HardcodedRoleMapper.ROLE);
|
||||
property.setLabel("Role");
|
||||
property.setHelpText("Role to grant to user. Click 'Select Role' button to browse roles, or just type it in the textbox. To reference an application role the syntax is appname.approle, i.e. myapp.myrole");
|
||||
property.setType(ProviderConfigProperty.ROLE_TYPE);
|
||||
configProperties.add(property);
|
||||
}
|
||||
|
||||
public static final String PROVIDER_ID = "saml-role-idp-mapper";
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return configProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getCompatibleProviders() {
|
||||
return COMPATIBLE_PROVIDERS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayCategory() {
|
||||
return "Role Mapper";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "SAML Attribute to Role";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
String roleName = mapperModel.getConfig().get(HardcodedRoleMapper.ROLE);
|
||||
if (isAttributePresent(mapperModel, context)) {
|
||||
RoleModel role = HardcodedRoleMapper.getRoleFromString(realm, roleName);
|
||||
if (role == null) throw new IdentityBrokerException("Unable to find role: " + roleName);
|
||||
user.grantRole(role);
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean isAttributePresent(IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
String name = mapperModel.getConfig().get(ATTRIBUTE_NAME);
|
||||
if (name != null && name.trim().equals("")) name = null;
|
||||
String friendly = mapperModel.getConfig().get(ATTRIBUTE_FRIENDLY_NAME);
|
||||
if (friendly != null && friendly.trim().equals("")) friendly = null;
|
||||
String desiredValue = mapperModel.getConfig().get(ATTRIBUTE_VALUE);
|
||||
AssertionType assertion = (AssertionType)context.getContextData().get(SAMLEndpoint.SAML_ASSERTION);
|
||||
for (AttributeStatementType statement : assertion.getAttributeStatements()) {
|
||||
for (AttributeStatementType.ASTChoiceType choice : statement.getAttributes()) {
|
||||
AttributeType attr = choice.getAttribute();
|
||||
if (name != null && !name.equals(attr.getName())) continue;
|
||||
if (friendly != null && !name.equals(attr.getFriendlyName())) continue;
|
||||
for (Object val : attr.getAttributeValue()) {
|
||||
if (val.equals(desiredValue)) return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
String roleName = mapperModel.getConfig().get(HardcodedRoleMapper.ROLE);
|
||||
if (!isAttributePresent(mapperModel, context)) {
|
||||
RoleModel role = HardcodedRoleMapper.getRoleFromString(realm, roleName);
|
||||
if (role == null) throw new IdentityBrokerException("Unable to find role: " + roleName);
|
||||
user.deleteRoleMapping(role);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "If a claim exists, grant the user the specified realm or application role.";
|
||||
}
|
||||
|
||||
}
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
package org.keycloak.broker.saml.mappers;
|
||||
|
||||
import org.keycloak.broker.provider.AbstractIdentityProviderMapper;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.broker.saml.SAMLEndpoint;
|
||||
import org.keycloak.broker.saml.SAMLIdentityProviderFactory;
|
||||
import org.keycloak.dom.saml.v2.assertion.AssertionType;
|
||||
import org.keycloak.dom.saml.v2.assertion.AttributeStatementType;
|
||||
import org.keycloak.dom.saml.v2.assertion.AttributeType;
|
||||
import org.keycloak.models.IdentityProviderMapperModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class UserAttributeMapper extends AbstractIdentityProviderMapper {
|
||||
|
||||
public static final String[] COMPATIBLE_PROVIDERS = {SAMLIdentityProviderFactory.PROVIDER_ID};
|
||||
|
||||
private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
|
||||
|
||||
public static final String ATTRIBUTE_NAME = "attribute.name";
|
||||
public static final String ATTRIBUTE_FRIENDLY_NAME = "attribute.friendly.name";
|
||||
public static final String USER_ATTRIBUTE = "user.attribute";
|
||||
|
||||
static {
|
||||
ProviderConfigProperty property;
|
||||
property = new ProviderConfigProperty();
|
||||
property.setName(ATTRIBUTE_NAME);
|
||||
property.setLabel("Attribute Name");
|
||||
property.setHelpText("Name of attribute to search for in assertion. You can leave this blank and specify a friendly name instead.");
|
||||
property.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
configProperties.add(property);
|
||||
property = new ProviderConfigProperty();
|
||||
property.setName(ATTRIBUTE_FRIENDLY_NAME);
|
||||
property.setLabel("Friendly Name");
|
||||
property.setHelpText("Friendly name of attribute to search for in assertion. You can leave this blank and specify a name instead.");
|
||||
property.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
configProperties.add(property);
|
||||
property = new ProviderConfigProperty();
|
||||
property.setName(USER_ATTRIBUTE);
|
||||
property.setLabel("User Attribute Name");
|
||||
property.setHelpText("User attribute name to store saml attribute.");
|
||||
property.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
configProperties.add(property);
|
||||
}
|
||||
|
||||
public static final String PROVIDER_ID = "saml-user-attribute-idp-mapper";
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return configProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getCompatibleProviders() {
|
||||
return COMPATIBLE_PROVIDERS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayCategory() {
|
||||
return "Attribute Importer";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "Attribute Importer";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
String attribute = mapperModel.getConfig().get(USER_ATTRIBUTE);
|
||||
Object value = getAttribute(mapperModel, context);
|
||||
if (value != null) {
|
||||
user.setSingleAttribute(attribute, value.toString());
|
||||
}
|
||||
}
|
||||
|
||||
protected String getAttribute(IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
String name = mapperModel.getConfig().get(ATTRIBUTE_NAME);
|
||||
if (name != null && name.trim().equals("")) name = null;
|
||||
String friendly = mapperModel.getConfig().get(ATTRIBUTE_FRIENDLY_NAME);
|
||||
if (friendly != null && friendly.trim().equals("")) friendly = null;
|
||||
AssertionType assertion = (AssertionType)context.getContextData().get(SAMLEndpoint.SAML_ASSERTION);
|
||||
for (AttributeStatementType statement : assertion.getAttributeStatements()) {
|
||||
for (AttributeStatementType.ASTChoiceType choice : statement.getAttributes()) {
|
||||
AttributeType attr = choice.getAttribute();
|
||||
if (name != null && !name.equals(attr.getName())) continue;
|
||||
if (friendly != null && !name.equals(attr.getFriendlyName())) continue;
|
||||
|
||||
List<Object> attributeValue = attr.getAttributeValue();
|
||||
if (attributeValue == null || attributeValue.isEmpty()) return null;
|
||||
return attributeValue.get(0).toString();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
String attribute = mapperModel.getConfig().get(USER_ATTRIBUTE);
|
||||
Object value = getAttribute(mapperModel, context);
|
||||
String current = user.getFirstAttribute(attribute);
|
||||
if (value != null && !value.equals(current)) {
|
||||
user.setSingleAttribute(attribute, value.toString());
|
||||
} else if (value == null) {
|
||||
user.removeAttribute(attribute);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Import declared saml attribute if it exists in assertion into the specified user attribute.";
|
||||
}
|
||||
|
||||
}
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
package org.keycloak.broker.saml.mappers;
|
||||
|
||||
import org.keycloak.broker.provider.AbstractIdentityProviderMapper;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.broker.saml.SAMLEndpoint;
|
||||
import org.keycloak.broker.saml.SAMLIdentityProviderFactory;
|
||||
import org.keycloak.dom.saml.v2.assertion.AssertionType;
|
||||
import org.keycloak.dom.saml.v2.assertion.AttributeStatementType;
|
||||
import org.keycloak.dom.saml.v2.assertion.AttributeType;
|
||||
import org.keycloak.dom.saml.v2.assertion.NameIDType;
|
||||
import org.keycloak.dom.saml.v2.assertion.SubjectType;
|
||||
import org.keycloak.models.IdentityProviderMapperModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class UsernameTemplateMapper extends AbstractIdentityProviderMapper {
|
||||
|
||||
public static final String[] COMPATIBLE_PROVIDERS = {SAMLIdentityProviderFactory.PROVIDER_ID};
|
||||
|
||||
private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
|
||||
|
||||
public static final String TEMPLATE = "template";
|
||||
|
||||
static {
|
||||
ProviderConfigProperty property;
|
||||
property = new ProviderConfigProperty();
|
||||
property.setName(TEMPLATE);
|
||||
property.setLabel("Template");
|
||||
property.setHelpText("Template to use to format the username to import. Substitutions are enclosed in ${}. For example: '${ALIAS}.${NAMEID}'. ALIAS is the provider alias. NAMEID is that SAML name id assertion. ATTRIBUTE.<NAME> references a SAML attribute where name is the attribute name or friendly name.");
|
||||
property.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
property.setDefaultValue("${ALIAS}.${NAMEID}");
|
||||
configProperties.add(property);
|
||||
}
|
||||
|
||||
public static final String PROVIDER_ID = "saml-username-idp-mapper";
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return configProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getCompatibleProviders() {
|
||||
return COMPATIBLE_PROVIDERS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayCategory() {
|
||||
return "Preprocessor";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "Username Template Importer";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
|
||||
}
|
||||
static Pattern substitution = Pattern.compile("\\$\\{([^}]+)\\}");
|
||||
|
||||
@Override
|
||||
public void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
AssertionType assertion = (AssertionType)context.getContextData().get(SAMLEndpoint.SAML_ASSERTION);
|
||||
String template = mapperModel.getConfig().get(TEMPLATE);
|
||||
Matcher m = substitution.matcher(template);
|
||||
StringBuffer sb = new StringBuffer();
|
||||
while (m.find()) {
|
||||
String variable = m.group(1);
|
||||
if (variable.equals("ALIAS")) {
|
||||
m.appendReplacement(sb, context.getIdpConfig().getAlias());
|
||||
} else if (variable.equals("UUID")) {
|
||||
m.appendReplacement(sb, KeycloakModelUtils.generateId());
|
||||
} else if (variable.equals("NAMEID")) {
|
||||
SubjectType subject = assertion.getSubject();
|
||||
SubjectType.STSubType subType = subject.getSubType();
|
||||
NameIDType subjectNameID = (NameIDType) subType.getBaseID();
|
||||
m.appendReplacement(sb, subjectNameID.getValue());
|
||||
} else if (variable.startsWith("ATTRIBUTE.")) {
|
||||
String name = variable.substring("ATTRIBUTE.".length());
|
||||
String value = "";
|
||||
for (AttributeStatementType statement : assertion.getAttributeStatements()) {
|
||||
for (AttributeStatementType.ASTChoiceType choice : statement.getAttributes()) {
|
||||
AttributeType attr = choice.getAttribute();
|
||||
if (name.equals(attr.getName()) || name.equals(attr.getFriendlyName())) {
|
||||
List<Object> attributeValue = attr.getAttributeValue();
|
||||
if (attributeValue != null && !attributeValue.isEmpty()) {
|
||||
value = attributeValue.get(0).toString();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
m.appendReplacement(sb, value);
|
||||
} else {
|
||||
m.appendReplacement(sb, m.group(1));
|
||||
}
|
||||
|
||||
}
|
||||
m.appendTail(sb);
|
||||
context.setModelUsername(sb.toString());
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Format the username to import.";
|
||||
}
|
||||
|
||||
}
|
||||
broker/saml/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderMapper
Executable
+3
@@ -0,0 +1,3 @@
|
||||
org.keycloak.broker.saml.mappers.AttributeToRoleMapper
|
||||
org.keycloak.broker.saml.mappers.UserAttributeMapper
|
||||
org.keycloak.broker.saml.mappers.UsernameTemplateMapper
|
||||
Regular → Executable
+1
-5
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>1.2.0.Beta1-SNAPSHOT</version>
|
||||
<version>1.4.0.Final</version>
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
@@ -17,22 +17,18 @@
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-export-import-api</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-export-import-single-file</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-core</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-model-api</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.codehaus.jackson</groupId>
|
||||
|
||||
+10
-2
@@ -24,6 +24,8 @@ import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.codehaus.jackson.JsonToken;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.exportimport.Strategy;
|
||||
@@ -84,7 +86,11 @@ public class DefaultFileConnectionProviderFactory implements FileConnectionProvi
|
||||
FileInputStream fis = null;
|
||||
try {
|
||||
fis = new FileInputStream(kcdata);
|
||||
Model model = JsonSerialization.readValue(fis, Model.class);
|
||||
ImportUtils.importFromStream(session, JsonSerialization.mapper, fis, Strategy.IGNORE_EXISTING);
|
||||
session.realms().getMigrationModel().setStoredVersion(model.getModelVersion());
|
||||
|
||||
ImportUtils.importRealms(session, model.getRealms(), Strategy.IGNORE_EXISTING);
|
||||
} catch (IOException ioe) {
|
||||
logger.error("Unable to read model file " + kcdata.getAbsolutePath(), ioe);
|
||||
} finally {
|
||||
@@ -128,8 +134,10 @@ public class DefaultFileConnectionProviderFactory implements FileConnectionProvi
|
||||
for (RealmModel realm : realms) {
|
||||
reps.add(ExportUtils.exportRealm(session, realm, true));
|
||||
}
|
||||
|
||||
JsonSerialization.prettyMapper.writeValue(outStream, reps);
|
||||
Model model = new Model();
|
||||
model.setRealms(reps);
|
||||
model.setModelVersion(session.realms().getMigrationModel().getStoredVersion());
|
||||
JsonSerialization.prettyMapper.writeValue(outStream, model);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -9,6 +9,11 @@ import org.keycloak.provider.Spi;
|
||||
*/
|
||||
public class FileConnectionSpi implements Spi {
|
||||
|
||||
@Override
|
||||
public boolean isInternal() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "connectionsFile";
|
||||
|
||||
Regular → Executable
+10
@@ -37,6 +37,8 @@ public class InMemoryModel {
|
||||
// realmId, userId, userModel
|
||||
private final Map<String, Map<String,UserModel>> allUsers = new HashMap<String, Map<String,UserModel>>();
|
||||
|
||||
private String modelVersion;
|
||||
|
||||
public InMemoryModel() {
|
||||
}
|
||||
|
||||
@@ -45,6 +47,14 @@ public class InMemoryModel {
|
||||
allUsers.put(id, new HashMap<String, UserModel>());
|
||||
}
|
||||
|
||||
public String getModelVersion() {
|
||||
return modelVersion;
|
||||
}
|
||||
|
||||
public void setModelVersion(String modelVersion) {
|
||||
this.modelVersion = modelVersion;
|
||||
}
|
||||
|
||||
public RealmModel getRealm(String id) {
|
||||
return allRealms.get(id);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.keycloak.connections.file;
|
||||
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class Model {
|
||||
private String modelVersion;
|
||||
private List<RealmRepresentation> realms;
|
||||
|
||||
public String getModelVersion() {
|
||||
return modelVersion;
|
||||
}
|
||||
|
||||
public void setModelVersion(String modelVersion) {
|
||||
this.modelVersion = modelVersion;
|
||||
}
|
||||
|
||||
public List<RealmRepresentation> getRealms() {
|
||||
return realms;
|
||||
}
|
||||
|
||||
public void setRealms(List<RealmRepresentation> realms) {
|
||||
this.realms = realms;
|
||||
}
|
||||
}
|
||||
Executable
+36
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>1.4.0.Final</version>
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>keycloak-connections-http-client</artifactId>
|
||||
<name>Keycloak Connections Apache HttpClient</name>
|
||||
<description/>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-model-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpclient</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jboss.logging</groupId>
|
||||
<artifactId>jboss-logging</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
package org.keycloak.connections.httpclient;
|
||||
|
||||
import org.apache.http.HttpEntity;
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.client.HttpClient;
|
||||
import org.apache.http.client.entity.EntityBuilder;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.apache.http.entity.ContentType;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.util.EnvUtil;
|
||||
import org.keycloak.util.KeystoreUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.KeyStore;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public class DefaultHttpClientFactory implements HttpClientFactory {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(DefaultHttpClientFactory.class);
|
||||
|
||||
private volatile CloseableHttpClient httpClient;
|
||||
|
||||
@Override
|
||||
public HttpClientProvider create(KeycloakSession session) {
|
||||
return new HttpClientProvider() {
|
||||
@Override
|
||||
public HttpClient getHttpClient() {
|
||||
return httpClient;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int postText(String uri, String text) throws IOException {
|
||||
HttpPost request = new HttpPost(uri);
|
||||
request.setEntity(EntityBuilder.create().setText(text).setContentType(ContentType.TEXT_PLAIN).build());
|
||||
HttpResponse response = httpClient.execute(request);
|
||||
try {
|
||||
return response.getStatusLine().getStatusCode();
|
||||
} finally {
|
||||
HttpEntity entity = response.getEntity();
|
||||
if (entity != null) {
|
||||
InputStream is = entity.getContent();
|
||||
if (is != null) is.close();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream get(String uri) throws IOException {
|
||||
HttpGet request = new HttpGet(uri);
|
||||
HttpResponse response = httpClient.execute(request);
|
||||
HttpEntity entity = response.getEntity();
|
||||
if (entity == null) return null;
|
||||
return entity.getContent();
|
||||
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
try {
|
||||
httpClient.close();
|
||||
} catch (IOException e) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "default";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope config) {
|
||||
long socketTimeout = config.getLong("socket-timeout-millis", -1L);
|
||||
long establishConnectionTimeout = config.getLong("establish-connection-timeout-millis", -1L);
|
||||
int maxPooledPerRoute = config.getInt("max-pooled-per-route", 0);
|
||||
int connectionPoolSize = config.getInt("connection-pool-size", 200);
|
||||
boolean disableTrustManager = config.getBoolean("disable-trust-manager", false);
|
||||
boolean disableCookies = config.getBoolean("disable-cookies", true);
|
||||
String hostnameVerificationPolicy = config.get("hostname-verification-policy", "WILDCARD");
|
||||
HttpClientBuilder.HostnameVerificationPolicy hostnamePolicy = HttpClientBuilder.HostnameVerificationPolicy.valueOf(hostnameVerificationPolicy);
|
||||
String truststore = config.get("truststore");
|
||||
String truststorePassword = config.get("truststore-password");
|
||||
String clientKeystore = config.get("client-keystore");
|
||||
String clientKeystorePassword = config.get("client-keystore-password");
|
||||
String clientPrivateKeyPassword = config.get("client-key-password");
|
||||
|
||||
HttpClientBuilder builder = new HttpClientBuilder();
|
||||
builder.socketTimeout(socketTimeout, TimeUnit.MILLISECONDS)
|
||||
.establishConnectionTimeout(establishConnectionTimeout, TimeUnit.MILLISECONDS)
|
||||
.maxPooledPerRoute(maxPooledPerRoute)
|
||||
.connectionPoolSize(connectionPoolSize)
|
||||
.hostnameVerification(hostnamePolicy)
|
||||
.disableCookies(disableCookies);
|
||||
if (disableTrustManager) builder.disableTrustManager();
|
||||
if (truststore != null) {
|
||||
truststore = EnvUtil.replace(truststore);
|
||||
try {
|
||||
builder.trustStore(KeystoreUtil.loadKeyStore(truststore, truststorePassword));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to load truststore", e);
|
||||
}
|
||||
}
|
||||
if (clientKeystore != null) {
|
||||
clientKeystore = EnvUtil.replace(clientKeystore);
|
||||
try {
|
||||
KeyStore clientCertKeystore = KeystoreUtil.loadKeyStore(clientKeystore, clientKeystorePassword);
|
||||
builder.keyStore(clientCertKeystore, clientPrivateKeyPassword);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to load keystore", e);
|
||||
}
|
||||
}
|
||||
httpClient = builder.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
Executable
+281
@@ -0,0 +1,281 @@
|
||||
package org.keycloak.connections.httpclient;
|
||||
|
||||
import org.apache.http.client.config.RequestConfig;
|
||||
import org.apache.http.conn.ssl.AllowAllHostnameVerifier;
|
||||
import org.apache.http.conn.ssl.BrowserCompatHostnameVerifier;
|
||||
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
|
||||
import org.apache.http.conn.ssl.SSLContexts;
|
||||
import org.apache.http.conn.ssl.StrictHostnameVerifier;
|
||||
import org.apache.http.conn.ssl.X509HostnameVerifier;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.impl.client.HttpClients;
|
||||
|
||||
import javax.net.ssl.HostnameVerifier;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLException;
|
||||
import javax.net.ssl.SSLSession;
|
||||
import javax.net.ssl.SSLSocket;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
import java.io.IOException;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.UnrecoverableKeyException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Abstraction for creating HttpClients. Allows SSL configuration.
|
||||
*
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class HttpClientBuilder {
|
||||
public static enum HostnameVerificationPolicy {
|
||||
/**
|
||||
* Hostname verification is not done on the server's certificate
|
||||
*/
|
||||
ANY,
|
||||
/**
|
||||
* Allows wildcards in subdomain names i.e. *.foo.com
|
||||
*/
|
||||
WILDCARD,
|
||||
/**
|
||||
* CN must match hostname connecting to
|
||||
*/
|
||||
STRICT
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
private static class PassthroughTrustManager implements X509TrustManager {
|
||||
public void checkClientTrusted(X509Certificate[] chain,
|
||||
String authType) throws CertificateException {
|
||||
}
|
||||
|
||||
public void checkServerTrusted(X509Certificate[] chain,
|
||||
String authType) throws CertificateException {
|
||||
}
|
||||
|
||||
public X509Certificate[] getAcceptedIssuers() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected KeyStore truststore;
|
||||
protected KeyStore clientKeyStore;
|
||||
protected String clientPrivateKeyPassword;
|
||||
protected boolean disableTrustManager;
|
||||
protected HostnameVerificationPolicy policy = HostnameVerificationPolicy.WILDCARD;
|
||||
protected SSLContext sslContext;
|
||||
protected int connectionPoolSize = 100;
|
||||
protected int maxPooledPerRoute = 0;
|
||||
protected long connectionTTL = -1;
|
||||
protected TimeUnit connectionTTLUnit = TimeUnit.MILLISECONDS;
|
||||
protected HostnameVerifier verifier = null;
|
||||
protected long socketTimeout = -1;
|
||||
protected TimeUnit socketTimeoutUnits = TimeUnit.MILLISECONDS;
|
||||
protected long establishConnectionTimeout = -1;
|
||||
protected TimeUnit establishConnectionTimeoutUnits = TimeUnit.MILLISECONDS;
|
||||
protected boolean disableCookies = false;
|
||||
|
||||
|
||||
/**
|
||||
* Socket inactivity timeout
|
||||
*
|
||||
* @param timeout
|
||||
* @param unit
|
||||
* @return
|
||||
*/
|
||||
public HttpClientBuilder socketTimeout(long timeout, TimeUnit unit)
|
||||
{
|
||||
this.socketTimeout = timeout;
|
||||
this.socketTimeoutUnits = unit;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* When trying to make an initial socket connection, what is the timeout?
|
||||
*
|
||||
* @param timeout
|
||||
* @param unit
|
||||
* @return
|
||||
*/
|
||||
public HttpClientBuilder establishConnectionTimeout(long timeout, TimeUnit unit)
|
||||
{
|
||||
this.establishConnectionTimeout = timeout;
|
||||
this.establishConnectionTimeoutUnits = unit;
|
||||
return this;
|
||||
}
|
||||
|
||||
public HttpClientBuilder connectionTTL(long ttl, TimeUnit unit) {
|
||||
this.connectionTTL = ttl;
|
||||
this.connectionTTLUnit = unit;
|
||||
return this;
|
||||
}
|
||||
|
||||
public HttpClientBuilder maxPooledPerRoute(int maxPooledPerRoute) {
|
||||
this.maxPooledPerRoute = maxPooledPerRoute;
|
||||
return this;
|
||||
}
|
||||
|
||||
public HttpClientBuilder connectionPoolSize(int connectionPoolSize) {
|
||||
this.connectionPoolSize = connectionPoolSize;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable trust management and hostname verification. <i>NOTE</i> this is a security
|
||||
* hole, so only set this option if you cannot or do not want to verify the identity of the
|
||||
* host you are communicating with.
|
||||
*/
|
||||
public HttpClientBuilder disableTrustManager() {
|
||||
this.disableTrustManager = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable cookie management.
|
||||
*/
|
||||
public HttpClientBuilder disableCookies(boolean disable) {
|
||||
this.disableTrustManager = disable;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* SSL policy used to verify hostnames
|
||||
*
|
||||
* @param policy
|
||||
* @return
|
||||
*/
|
||||
public HttpClientBuilder hostnameVerification(HostnameVerificationPolicy policy) {
|
||||
this.policy = policy;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
public HttpClientBuilder sslContext(SSLContext sslContext) {
|
||||
this.sslContext = sslContext;
|
||||
return this;
|
||||
}
|
||||
|
||||
public HttpClientBuilder trustStore(KeyStore truststore) {
|
||||
this.truststore = truststore;
|
||||
return this;
|
||||
}
|
||||
|
||||
public HttpClientBuilder keyStore(KeyStore keyStore, String password) {
|
||||
this.clientKeyStore = keyStore;
|
||||
this.clientPrivateKeyPassword = password;
|
||||
return this;
|
||||
}
|
||||
|
||||
public HttpClientBuilder keyStore(KeyStore keyStore, char[] password) {
|
||||
this.clientKeyStore = keyStore;
|
||||
this.clientPrivateKeyPassword = new String(password);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
static class VerifierWrapper implements X509HostnameVerifier {
|
||||
protected HostnameVerifier verifier;
|
||||
|
||||
VerifierWrapper(HostnameVerifier verifier) {
|
||||
this.verifier = verifier;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void verify(String host, SSLSocket ssl) throws IOException {
|
||||
if (!verifier.verify(host, ssl.getSession())) throw new SSLException("Hostname verification failure");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void verify(String host, X509Certificate cert) throws SSLException {
|
||||
throw new SSLException("This verification path not implemented");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void verify(String host, String[] cns, String[] subjectAlts) throws SSLException {
|
||||
throw new SSLException("This verification path not implemented");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean verify(String s, SSLSession sslSession) {
|
||||
return verifier.verify(s, sslSession);
|
||||
}
|
||||
}
|
||||
|
||||
public CloseableHttpClient build() {
|
||||
X509HostnameVerifier verifier = null;
|
||||
if (this.verifier != null) verifier = new VerifierWrapper(this.verifier);
|
||||
else {
|
||||
switch (policy) {
|
||||
case ANY:
|
||||
verifier = new AllowAllHostnameVerifier();
|
||||
break;
|
||||
case WILDCARD:
|
||||
verifier = new BrowserCompatHostnameVerifier();
|
||||
break;
|
||||
case STRICT:
|
||||
verifier = new StrictHostnameVerifier();
|
||||
break;
|
||||
}
|
||||
}
|
||||
try {
|
||||
SSLConnectionSocketFactory sslsf = null;
|
||||
SSLContext theContext = sslContext;
|
||||
if (disableTrustManager) {
|
||||
theContext = SSLContext.getInstance("TLS");
|
||||
theContext.init(null, new TrustManager[]{new PassthroughTrustManager()},
|
||||
new SecureRandom());
|
||||
verifier = new AllowAllHostnameVerifier();
|
||||
sslsf = new SSLConnectionSocketFactory(theContext, verifier);
|
||||
} else if (theContext != null) {
|
||||
sslsf = new SSLConnectionSocketFactory(theContext, verifier);
|
||||
} else if (clientKeyStore != null || truststore != null) {
|
||||
theContext = createSslContext("TLS", clientKeyStore, clientPrivateKeyPassword, truststore, null);
|
||||
sslsf = new SSLConnectionSocketFactory(theContext, verifier);
|
||||
} else {
|
||||
final SSLContext tlsContext = SSLContext.getInstance("TLS");
|
||||
tlsContext.init(null, null, null);
|
||||
sslsf = new SSLConnectionSocketFactory(tlsContext, verifier);
|
||||
}
|
||||
RequestConfig requestConfig = RequestConfig.custom()
|
||||
.setConnectTimeout((int) establishConnectionTimeout)
|
||||
.setSocketTimeout((int) socketTimeout).build();
|
||||
|
||||
org.apache.http.impl.client.HttpClientBuilder builder = HttpClients.custom()
|
||||
.setDefaultRequestConfig(requestConfig)
|
||||
.setSSLSocketFactory(sslsf)
|
||||
.setMaxConnTotal(connectionPoolSize)
|
||||
.setMaxConnPerRoute(maxPooledPerRoute);
|
||||
if (disableCookies) builder.disableCookieManagement();
|
||||
return builder.build();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private SSLContext createSslContext(
|
||||
final String algorithm,
|
||||
final KeyStore keystore,
|
||||
final String keyPassword,
|
||||
final KeyStore truststore,
|
||||
final SecureRandom random)
|
||||
throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
|
||||
return SSLContexts.custom()
|
||||
.useProtocol(algorithm)
|
||||
.setSecureRandom(random)
|
||||
.loadKeyMaterial(keystore, keyPassword != null ? keyPassword.toCharArray() : null)
|
||||
.loadTrustMaterial(truststore)
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
Executable
+11
@@ -0,0 +1,11 @@
|
||||
package org.keycloak.connections.httpclient;
|
||||
|
||||
import org.apache.http.client.HttpClient;
|
||||
import org.keycloak.provider.Provider;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public interface HttpClientFactory extends ProviderFactory<HttpClientProvider> {
|
||||
}
|
||||
Executable
+34
@@ -0,0 +1,34 @@
|
||||
package org.keycloak.connections.httpclient;
|
||||
|
||||
import org.apache.http.client.HttpClient;
|
||||
import org.keycloak.provider.Provider;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public interface HttpClientProvider extends Provider {
|
||||
HttpClient getHttpClient();
|
||||
|
||||
/**
|
||||
* Helper method
|
||||
*
|
||||
* @param uri
|
||||
* @param text
|
||||
* @return http response status
|
||||
* @throws IOException
|
||||
*/
|
||||
public int postText(String uri, String text) throws IOException;
|
||||
|
||||
/**
|
||||
* Helper method
|
||||
*
|
||||
* @param uri
|
||||
* @return response stream, you must close this stream or leaks will happen
|
||||
* @throws IOException
|
||||
*/
|
||||
public InputStream get(String uri) throws IOException;
|
||||
}
|
||||
Executable
+32
@@ -0,0 +1,32 @@
|
||||
package org.keycloak.connections.httpclient;
|
||||
|
||||
import org.keycloak.provider.Provider;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
import org.keycloak.provider.Spi;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public class HttpClientSpi implements Spi {
|
||||
|
||||
@Override
|
||||
public boolean isInternal() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "connectionsHttpClient";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends Provider> getProviderClass() {
|
||||
return HttpClientProvider.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends ProviderFactory> getProviderFactoryClass() {
|
||||
return HttpClientFactory.class;
|
||||
}
|
||||
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
org.keycloak.connections.httpclient.DefaultHttpClientFactory
|
||||
+1
@@ -0,0 +1 @@
|
||||
org.keycloak.connections.httpclient.HttpClientSpi
|
||||
@@ -1,33 +1,31 @@
|
||||
<?xml version="1.0"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>1.2.0.Beta1-SNAPSHOT</version>
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>keycloak-connections-infinispan</artifactId>
|
||||
<name>Keycloak Connections Infinispan</name>
|
||||
<description/>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-core</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-model-api</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.infinispan</groupId>
|
||||
<artifactId>infinispan-core</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
<?xml version="1.0"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>1.4.0.Final</version>
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>keycloak-connections-infinispan</artifactId>
|
||||
<name>Keycloak Connections Infinispan</name>
|
||||
<description/>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-model-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.infinispan</groupId>
|
||||
<artifactId>infinispan-core</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
+5
@@ -9,6 +9,11 @@ import org.keycloak.provider.Spi;
|
||||
*/
|
||||
public class InfinispanConnectionSpi implements Spi {
|
||||
|
||||
@Override
|
||||
public boolean isInternal() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "connectionsInfinispan";
|
||||
|
||||
@@ -1,78 +1,76 @@
|
||||
<?xml version="1.0"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>1.2.0.Beta1-SNAPSHOT</version>
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>keycloak-connections-jpa-liquibase</artifactId>
|
||||
<name>Keycloak Connections JPA Liquibase Updater</name>
|
||||
<description/>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-connections-jpa</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-model-api</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.liquibase</groupId>
|
||||
<artifactId>liquibase-core</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.yaml</groupId>
|
||||
<artifactId>snakeyaml</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jboss.logging</groupId>
|
||||
<artifactId>jboss-logging</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.liquibase</groupId>
|
||||
<artifactId>liquibase-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<changeLogFile>META-INF/jpa-changelog-master.xml</changeLogFile>
|
||||
|
||||
<url>${url}</url>
|
||||
<driver>${driver}</driver>
|
||||
<username>${username}</username>
|
||||
<password>${password}</password>
|
||||
|
||||
<referenceUrl>${referenceUrl}</referenceUrl>
|
||||
<referenceDriver>${driver}</referenceDriver>
|
||||
<referenceUsername>${username}</referenceUsername>
|
||||
<referencePassword>${password}</referencePassword>
|
||||
</configuration>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
<version>${h2.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<properties>
|
||||
<username>sa</username>
|
||||
<password></password>
|
||||
<driver>org.h2.Driver</driver>
|
||||
</properties>
|
||||
</project>
|
||||
<?xml version="1.0"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>1.4.0.Final</version>
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>keycloak-connections-jpa-liquibase</artifactId>
|
||||
<name>Keycloak Connections JPA Liquibase Updater</name>
|
||||
<description/>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-connections-jpa</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-model-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.liquibase</groupId>
|
||||
<artifactId>liquibase-core</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.yaml</groupId>
|
||||
<artifactId>snakeyaml</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jboss.logging</groupId>
|
||||
<artifactId>jboss-logging</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-services</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.liquibase</groupId>
|
||||
<artifactId>liquibase-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<changeLogFile>META-INF/jpa-changelog-master.xml</changeLogFile>
|
||||
|
||||
<url>${project.url}</url>
|
||||
<driver>${driver}</driver>
|
||||
<username>${username}</username>
|
||||
<password>${password}</password>
|
||||
|
||||
<referenceUrl>${referenceUrl}</referenceUrl>
|
||||
<referenceDriver>${driver}</referenceDriver>
|
||||
<referenceUsername>${username}</referenceUsername>
|
||||
<referencePassword>${password}</referencePassword>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<properties>
|
||||
<username>sa</username>
|
||||
<password></password>
|
||||
<driver>org.h2.Driver</driver>
|
||||
</properties>
|
||||
</project>
|
||||
+15
-8
@@ -46,26 +46,26 @@ public class LiquibaseJpaUpdaterProvider implements JpaUpdaterProvider {
|
||||
private static final String CHANGELOG = "META-INF/jpa-changelog-master.xml";
|
||||
|
||||
@Override
|
||||
public String getCurrentVersionSql() {
|
||||
return "SELECT ID from DATABASECHANGELOG ORDER BY DATEEXECUTED DESC LIMIT 1";
|
||||
public String getCurrentVersionSql(String defaultSchema) {
|
||||
return "SELECT ID from " + getTable("DATABASECHANGELOG", defaultSchema) + " ORDER BY DATEEXECUTED DESC LIMIT 1";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(KeycloakSession session, Connection connection) {
|
||||
public void update(KeycloakSession session, Connection connection, String defaultSchema) {
|
||||
logger.debug("Starting database update");
|
||||
|
||||
// Need ThreadLocal as liquibase doesn't seem to have API to inject custom objects into tasks
|
||||
ThreadLocalSessionContext.setCurrentSession(session);
|
||||
|
||||
try {
|
||||
Liquibase liquibase = getLiquibase(connection);
|
||||
Liquibase liquibase = getLiquibase(connection, defaultSchema);
|
||||
|
||||
List<ChangeSet> changeSets = liquibase.listUnrunChangeSets((Contexts) null);
|
||||
if (!changeSets.isEmpty()) {
|
||||
if (changeSets.get(0).getId().equals(FIRST_VERSION)) {
|
||||
Statement statement = connection.createStatement();
|
||||
try {
|
||||
statement.executeQuery("SELECT id FROM REALM");
|
||||
statement.executeQuery("SELECT id FROM " + getTable("REALM", defaultSchema));
|
||||
|
||||
logger.infov("Updating database from {0} to {1}", FIRST_VERSION, changeSets.get(changeSets.size() - 1).getId());
|
||||
liquibase.markNextChangeSetRan(null);
|
||||
@@ -93,9 +93,9 @@ public class LiquibaseJpaUpdaterProvider implements JpaUpdaterProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate(Connection connection) {
|
||||
public void validate(Connection connection, String defaultSchema) {
|
||||
try {
|
||||
Liquibase liquibase = getLiquibase(connection);
|
||||
Liquibase liquibase = getLiquibase(connection, defaultSchema);
|
||||
|
||||
liquibase.validate();
|
||||
} catch (Exception e) {
|
||||
@@ -103,7 +103,7 @@ public class LiquibaseJpaUpdaterProvider implements JpaUpdaterProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private Liquibase getLiquibase(Connection connection) throws Exception {
|
||||
private Liquibase getLiquibase(Connection connection, String defaultSchema) throws Exception {
|
||||
ServiceLocator sl = ServiceLocator.getInstance();
|
||||
|
||||
if (!System.getProperties().containsKey("liquibase.scan.packages")) {
|
||||
@@ -125,6 +125,9 @@ public class LiquibaseJpaUpdaterProvider implements JpaUpdaterProvider {
|
||||
|
||||
LogFactory.setInstance(new LogWrapper());
|
||||
Database database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(new JdbcConnection(connection));
|
||||
if (defaultSchema != null) {
|
||||
database.setDefaultSchemaName(defaultSchema);
|
||||
}
|
||||
return new Liquibase(CHANGELOG, new ClassLoaderResourceAccessor(getClass().getClassLoader()), database);
|
||||
}
|
||||
|
||||
@@ -228,4 +231,8 @@ public class LiquibaseJpaUpdaterProvider implements JpaUpdaterProvider {
|
||||
|
||||
}
|
||||
|
||||
private String getTable(String table, String defaultSchema) {
|
||||
return defaultSchema != null ? defaultSchema + "." + table : table;
|
||||
}
|
||||
|
||||
}
|
||||
+15
-1
@@ -15,14 +15,18 @@ import liquibase.resource.ResourceAccessor;
|
||||
import liquibase.snapshot.SnapshotGeneratorFactory;
|
||||
import liquibase.statement.SqlStatement;
|
||||
import liquibase.structure.core.Table;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.connections.jpa.updater.liquibase.ThreadLocalSessionContext;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.services.DefaultKeycloakSessionFactory;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public abstract class CustomKeycloakTask implements CustomSqlChange {
|
||||
|
||||
private final Logger logger = Logger.getLogger(getClass());
|
||||
|
||||
protected KeycloakSession kcSession;
|
||||
|
||||
protected Database database;
|
||||
@@ -49,8 +53,18 @@ public abstract class CustomKeycloakTask implements CustomSqlChange {
|
||||
@Override
|
||||
public void setUp() throws SetupException {
|
||||
this.kcSession = ThreadLocalSessionContext.getCurrentSession();
|
||||
|
||||
if (this.kcSession == null) {
|
||||
throw new SetupException("No KeycloakSession provided in ThreadLocal");
|
||||
// Probably running Liquibase from maven plugin. Try to create kcSession programmatically
|
||||
logger.info("No KeycloakSession provided in ThreadLocal. Initializing KeycloakSessionFactory");
|
||||
|
||||
try {
|
||||
DefaultKeycloakSessionFactory factory = new DefaultKeycloakSessionFactory();
|
||||
factory.init();
|
||||
this.kcSession = factory.create();
|
||||
} catch (Exception e) {
|
||||
throw new SetupException("Exception when initializing factory", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package org.keycloak.connections.jpa.updater.liquibase.custom;
|
||||
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
|
||||
import liquibase.datatype.DataTypeFactory;
|
||||
import liquibase.exception.CustomChangeException;
|
||||
import liquibase.statement.core.InsertStatement;
|
||||
import liquibase.structure.core.Table;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class JpaUpdate1_2_0_CR1 extends CustomKeycloakTask {
|
||||
|
||||
@Override
|
||||
protected void generateStatementsImpl() throws CustomChangeException {
|
||||
String realmClientTableName = database.correctObjectName("REALM_CLIENT", Table.class);
|
||||
|
||||
try {
|
||||
String trueValue = DataTypeFactory.getInstance().getTrueBooleanValue(database);
|
||||
PreparedStatement statement = jdbcConnection.prepareStatement("select CLIENT.REALM_ID, CLIENT.ID CLIENT_ID from CLIENT where CLIENT.CONSENT_REQUIRED = " + trueValue);
|
||||
|
||||
try {
|
||||
ResultSet resultSet = statement.executeQuery();
|
||||
try {
|
||||
while (resultSet.next()) {
|
||||
String realmId = resultSet.getString("REALM_ID");
|
||||
String oauthClientId = resultSet.getString("CLIENT_ID");
|
||||
|
||||
InsertStatement realmClientInsert = new InsertStatement(null, null, realmClientTableName)
|
||||
.addColumnValue("REALM_ID", realmId)
|
||||
.addColumnValue("CLIENT_ID", oauthClientId);
|
||||
statements.add(realmClientInsert);
|
||||
}
|
||||
} finally {
|
||||
resultSet.close();
|
||||
}
|
||||
} finally {
|
||||
statement.close();
|
||||
}
|
||||
|
||||
confirmationMessage.append("Inserted " + statements.size() + " OAuth Clients to REALM_CLIENT table");
|
||||
} catch (Exception e) {
|
||||
throw new CustomChangeException(getTaskId() + ": Exception when updating data from previous version", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getTaskId() {
|
||||
return "Update 1.2.0.CR1";
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
package org.keycloak.connections.jpa.updater.liquibase.custom;
|
||||
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
|
||||
import liquibase.datatype.DataTypeFactory;
|
||||
import liquibase.exception.CustomChangeException;
|
||||
import liquibase.statement.core.InsertStatement;
|
||||
import liquibase.statement.core.UpdateStatement;
|
||||
import liquibase.structure.core.Table;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class JpaUpdate1_4_0_Final extends CustomKeycloakTask {
|
||||
|
||||
@Override
|
||||
protected void generateStatementsImpl() throws CustomChangeException {
|
||||
String userAttributeTableName = database.correctObjectName("USER_ATTRIBUTE", Table.class);
|
||||
|
||||
try {
|
||||
PreparedStatement statement = jdbcConnection.prepareStatement("select NAME, USER_ID from USER_ATTRIBUTE");
|
||||
|
||||
try {
|
||||
ResultSet resultSet = statement.executeQuery();
|
||||
try {
|
||||
while (resultSet.next()) {
|
||||
String name = resultSet.getString(1);
|
||||
String userId = resultSet.getString(2);
|
||||
|
||||
UpdateStatement updateStatement = new UpdateStatement(null, null, userAttributeTableName)
|
||||
.addNewColumnValue("ID", KeycloakModelUtils.generateId())
|
||||
.setWhereClause("NAME='" + name + "' AND USER_ID='" + userId + "'");
|
||||
statements.add(updateStatement);
|
||||
}
|
||||
} finally {
|
||||
resultSet.close();
|
||||
}
|
||||
} finally {
|
||||
statement.close();
|
||||
}
|
||||
|
||||
confirmationMessage.append("Updated " + statements.size() + " attributes in USER_ATTRIBUTE table");
|
||||
} catch (Exception e) {
|
||||
throw new CustomChangeException(getTaskId() + ": Exception when updating data from previous version", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getTaskId() {
|
||||
return "Update 1.4.0.Final";
|
||||
}
|
||||
}
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
|
||||
<changeSet author="bburke@redhat.com" id="1.2.0.RC1">
|
||||
<delete tableName="CLIENT_SESSION_ROLE"/>
|
||||
<delete tableName="CLIENT_SESSION_NOTE"/>
|
||||
<delete tableName="CLIENT_SESSION"/>
|
||||
<delete tableName="USER_SESSION_NOTE"/>
|
||||
<delete tableName="USER_SESSION"/>
|
||||
|
||||
<createTable tableName="MIGRATION_MODEL">
|
||||
<column name="ID" type="VARCHAR(36)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="VERSION" type="VARCHAR(36)">
|
||||
<constraints nullable="true"/>
|
||||
</column>
|
||||
</createTable>
|
||||
|
||||
<createTable tableName="IDENTITY_PROVIDER_MAPPER">
|
||||
<column name="ID" type="VARCHAR(36)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="NAME" type="VARCHAR(255)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="IDP_ALIAS" type="VARCHAR(255)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="IDP_MAPPER_NAME" type="VARCHAR(255)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="REALM_ID" type="VARCHAR(36)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</createTable>
|
||||
<createTable tableName="IDP_MAPPER_CONFIG">
|
||||
<column name="IDP_MAPPER_ID" type="VARCHAR(36)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="VALUE" type="CLOB"/>
|
||||
<column name="NAME" type="VARCHAR(255)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</createTable>
|
||||
<addColumn tableName="CREDENTIAL">
|
||||
<column name="CREATED_DATE" type="BIGINT"/>
|
||||
</addColumn>
|
||||
<createTable tableName="USER_CONSENT">
|
||||
<column name="ID" type="VARCHAR(36)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="CLIENT_ID" type="VARCHAR(36)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="USER_ID" type="VARCHAR(36)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</createTable>
|
||||
<createTable tableName="USER_CONSENT_ROLE">
|
||||
<column name="USER_CONSENT_ID" type="VARCHAR(36)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="ROLE_ID" type="VARCHAR(36)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</createTable>
|
||||
<createTable tableName="USER_CONSENT_PROT_MAPPER">
|
||||
<column name="USER_CONSENT_ID" type="VARCHAR(36)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="PROTOCOL_MAPPER_ID" type="VARCHAR(36)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</createTable>
|
||||
<createTable tableName="CLIENT_SESSION_PROT_MAPPER">
|
||||
<column name="PROTOCOL_MAPPER_ID" type="VARCHAR(36)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="CLIENT_SESSION" type="VARCHAR(36)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</createTable>
|
||||
<addPrimaryKey columnNames="ID" constraintName="CONSTRAINT_MIGMOD" tableName="MIGRATION_MODEL"/>
|
||||
<addPrimaryKey columnNames="ID" constraintName="CONSTRAINT_IDPM" tableName="IDENTITY_PROVIDER_MAPPER"/>
|
||||
<addPrimaryKey columnNames="IDP_MAPPER_ID, NAME" constraintName="CONSTRAINT_IDPMConfig" tableName="IDP_MAPPER_CONFIG"/>
|
||||
<addPrimaryKey columnNames="ID" constraintName="CONSTRAINT_GRNTCSNT_PM" tableName="USER_CONSENT"/>
|
||||
<addPrimaryKey columnNames="USER_CONSENT_ID, ROLE_ID" constraintName="CONSTRAINT_GRNTCSNT_ROLE_PM" tableName="USER_CONSENT_ROLE"/>
|
||||
<addPrimaryKey columnNames="USER_CONSENT_ID, PROTOCOL_MAPPER_ID" constraintName="CONSTRAINT_GRNTCSNT_PRM_PM" tableName="USER_CONSENT_PROT_MAPPER"/>
|
||||
<addPrimaryKey columnNames="CLIENT_SESSION, PROTOCOL_MAPPER_ID" constraintName="CONSTRAINT_CS_PMP_PK" tableName="CLIENT_SESSION_PROT_MAPPER"/>
|
||||
<addForeignKeyConstraint baseColumnNames="REALM_ID" baseTableName="IDENTITY_PROVIDER_MAPPER" constraintName="FK_IDPM_REALM" referencedColumnNames="ID" referencedTableName="REALM"/>
|
||||
<addForeignKeyConstraint baseColumnNames="IDP_MAPPER_ID" baseTableName="IDP_MAPPER_CONFIG" constraintName="FK_IDPMConfig" referencedColumnNames="ID" referencedTableName="IDENTITY_PROVIDER_MAPPER"/>
|
||||
<addForeignKeyConstraint baseColumnNames="USER_ID" baseTableName="USER_CONSENT" constraintName="FK_GRNTCSNT_USER" referencedColumnNames="ID" referencedTableName="USER_ENTITY"/>
|
||||
<addForeignKeyConstraint baseColumnNames="USER_CONSENT_ID" baseTableName="USER_CONSENT_ROLE" constraintName="FK_GRNTCSNT_ROLE_GR" referencedColumnNames="ID" referencedTableName="USER_CONSENT"/>
|
||||
<addForeignKeyConstraint baseColumnNames="USER_CONSENT_ID" baseTableName="USER_CONSENT_PROT_MAPPER" constraintName="FK_GRNTCSNT_PRM_GR" referencedColumnNames="ID" referencedTableName="USER_CONSENT"/>
|
||||
<addForeignKeyConstraint baseColumnNames="CLIENT_SESSION" baseTableName="CLIENT_SESSION_PROT_MAPPER" constraintName="FK_33A8SGQW18I532811V7O2DK89" referencedColumnNames="ID" referencedTableName="CLIENT_SESSION"/>
|
||||
|
||||
<renameColumn tableName="CLIENT" newColumnName="CLIENT_ID" oldColumnName="NAME" columnDataType="VARCHAR(255)"/>
|
||||
<addColumn tableName="CLIENT">
|
||||
<column name="CONSENT_REQUIRED" type="BOOLEAN" defaultValueBoolean="false">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="NAME" type="VARCHAR(255)" />
|
||||
</addColumn>
|
||||
<addColumn tableName="IDENTITY_PROVIDER">
|
||||
<column name="ADD_TOKEN_ROLE" type="BOOLEAN" defaultValueBoolean="true">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</addColumn>
|
||||
<update tableName="CLIENT">
|
||||
<column name="CONSENT_REQUIRED" valueBoolean="true"/>
|
||||
<where>DTYPE = 'OAuthClientEntity'</where>
|
||||
</update>
|
||||
<dropColumn tableName="CLIENT" columnName="DTYPE"/>
|
||||
|
||||
<dropForeignKeyConstraint baseTableName="REALM" constraintName="FK_RSAF444KK6QRKMS7N56AIWQ5Y" />
|
||||
<renameColumn tableName="REALM" newColumnName="MASTER_ADMIN_CLIENT" oldColumnName="MASTER_ADMIN_APP" columnDataType="VARCHAR(36)"/>
|
||||
<addForeignKeyConstraint baseColumnNames="MASTER_ADMIN_CLIENT" baseTableName="REALM" constraintName="FK_TRAF444KK6QRKMS7N56AIWQ5Y" referencedColumnNames="ID" referencedTableName="CLIENT"/>
|
||||
|
||||
<dropForeignKeyConstraint baseTableName="REALM_APPLICATION" constraintName="FK_82S3P0DIUXAWWQQSA528UBY2Q" />
|
||||
<renameTable oldTableName="REALM_APPLICATION" newTableName="REALM_CLIENT"/>
|
||||
<renameColumn tableName="REALM_CLIENT" newColumnName="CLIENT_ID" oldColumnName="APPLICATION_ID" columnDataType="VARCHAR(36)"/>
|
||||
<addForeignKeyConstraint baseColumnNames="CLIENT_ID" baseTableName="REALM_CLIENT" constraintName="FK_93S3P0DIUXAWWQQSA528UBY2Q" referencedColumnNames="ID" referencedTableName="CLIENT"/>
|
||||
|
||||
<dropForeignKeyConstraint baseTableName="APPLICATION_DEFAULT_ROLES" constraintName="FK_MAYLTS7KLWQW2H8M2B5JOYTKY" />
|
||||
<renameTable oldTableName="APPLICATION_DEFAULT_ROLES" newTableName="CLIENT_DEFAULT_ROLES"/>
|
||||
<renameColumn tableName="CLIENT_DEFAULT_ROLES" newColumnName="CLIENT_ID" oldColumnName="APPLICATION_ID" columnDataType="VARCHAR(36)"/>
|
||||
<addForeignKeyConstraint baseColumnNames="CLIENT_ID" baseTableName="CLIENT_DEFAULT_ROLES" constraintName="FK_NUILTS7KLWQW2H8M2B5JOYTKY" referencedColumnNames="ID" referencedTableName="CLIENT"/>
|
||||
|
||||
<dropForeignKeyConstraint baseTableName="APP_NODE_REGISTRATIONS" constraintName="FK8454723BA992F594" />
|
||||
<renameTable oldTableName="APP_NODE_REGISTRATIONS" newTableName="CLIENT_NODE_REGISTRATIONS"/>
|
||||
<renameColumn tableName="CLIENT_NODE_REGISTRATIONS" newColumnName="CLIENT_ID" oldColumnName="APPLICATION_ID" columnDataType="VARCHAR(36)"/>
|
||||
<addForeignKeyConstraint baseColumnNames="CLIENT_ID" baseTableName="CLIENT_NODE_REGISTRATIONS" constraintName="FK4129723BA992F594" referencedColumnNames="ID" referencedTableName="CLIENT"/>
|
||||
|
||||
<dropForeignKeyConstraint baseTableName="KEYCLOAK_ROLE" constraintName="FK_PIMO5LE2C0RAL09FL8CM9WFW9" />
|
||||
<renameColumn tableName="KEYCLOAK_ROLE" newColumnName="CLIENT" oldColumnName="APPLICATION" columnDataType="VARCHAR(36)"/>
|
||||
<renameColumn tableName="KEYCLOAK_ROLE" newColumnName="CLIENT_ROLE" oldColumnName="APPLICATION_ROLE" columnDataType="BOOLEAN"/>
|
||||
<renameColumn tableName="KEYCLOAK_ROLE" newColumnName="CLIENT_REALM_CONSTRAINT" oldColumnName="APP_REALM_CONSTRAINT" columnDataType="VARCHAR(36)"/>
|
||||
<addForeignKeyConstraint baseColumnNames="CLIENT" baseTableName="KEYCLOAK_ROLE" constraintName="FK_KJHO5LE2C0RAL09FL8CM9WFW9" referencedColumnNames="ID" referencedTableName="CLIENT"/>
|
||||
|
||||
<dropUniqueConstraint tableName="KEYCLOAK_ROLE" constraintName="UK_J3RWUVD56ONTGSUHOGM184WW2"/>
|
||||
<addUniqueConstraint columnNames="NAME,CLIENT_REALM_CONSTRAINT" constraintName="UK_J3RWUVD56ONTGSUHOGM184WW2-2" tableName="KEYCLOAK_ROLE"/>
|
||||
<addUniqueConstraint columnNames="CLIENT_ID, USER_ID" constraintName="UK_JKUWUVD56ONTGSUHOGM8UEWRT" tableName="USER_CONSENT"/>
|
||||
|
||||
<customChange class="org.keycloak.connections.jpa.updater.liquibase.custom.JpaUpdate1_2_0_CR1"/>
|
||||
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
|
||||
<changeSet author="keycloak" id="1.2.0.Final">
|
||||
<!-- KEYCLOAK-1277 -->
|
||||
<update tableName="CLIENT">
|
||||
<column name="DIRECT_GRANTS_ONLY" valueBoolean="false"/>
|
||||
<where>DIRECT_GRANTS_ONLY is null</where>
|
||||
</update>
|
||||
<update tableName="CLIENT">
|
||||
<column name="BEARER_ONLY" valueBoolean="false"/>
|
||||
<where>BEARER_ONLY is null</where>
|
||||
</update>
|
||||
<update tableName="CLIENT">
|
||||
<column name="SURROGATE_AUTH_REQUIRED" valueBoolean="false"/>
|
||||
<where>SURROGATE_AUTH_REQUIRED is null</where>
|
||||
</update>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
+200
@@ -0,0 +1,200 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
|
||||
<changeSet author="bburke@redhat.com" id="1.3.0">
|
||||
<delete tableName="CLIENT_SESSION_ROLE"/>
|
||||
<delete tableName="CLIENT_SESSION_PROT_MAPPER"/>
|
||||
<delete tableName="CLIENT_SESSION_NOTE"/>
|
||||
<delete tableName="CLIENT_SESSION"/>
|
||||
<delete tableName="USER_SESSION_NOTE"/>
|
||||
<delete tableName="USER_SESSION"/>
|
||||
<createTable tableName="ADMIN_EVENT_ENTITY">
|
||||
<column name="ID" type="VARCHAR(36)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="ADMIN_EVENT_TIME" type="BIGINT"/>
|
||||
<column name="REALM_ID" type="VARCHAR(255)"/>
|
||||
<column name="OPERATION_TYPE" type="VARCHAR(255)"/>
|
||||
<column name="AUTH_REALM_ID" type="VARCHAR(255)"/>
|
||||
<column name="AUTH_CLIENT_ID" type="VARCHAR(255)"/>
|
||||
<column name="AUTH_USER_ID" type="VARCHAR(255)"/>
|
||||
<column name="IP_ADDRESS" type="VARCHAR(255)"/>
|
||||
<column name="RESOURCE_PATH" type="VARCHAR(2550)"/>
|
||||
<column name="REPRESENTATION" type="TEXT(25500)"/>
|
||||
<column name="ERROR" type="VARCHAR(255)"/>
|
||||
</createTable>
|
||||
<createTable tableName="AUTHENTICATOR">
|
||||
<column name="ID" type="VARCHAR(36)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="ALIAS" type="VARCHAR(255)"/>
|
||||
<column name="REALM_ID" type="VARCHAR(36)"/>
|
||||
<column name="PROVIDER_ID" type="VARCHAR(255)"/>
|
||||
</createTable>
|
||||
<createTable tableName="AUTHENTICATION_FLOW">
|
||||
<column name="ID" type="VARCHAR(36)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="ALIAS" type="VARCHAR(255)"/>
|
||||
<column name="DESCRIPTION" type="VARCHAR(255)"/>
|
||||
<column name="REALM_ID" type="VARCHAR(36)"/>
|
||||
</createTable>
|
||||
<createTable tableName="AUTHENTICATION_EXECUTION">
|
||||
<column name="ID" type="VARCHAR(36)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="ALIAS" type="VARCHAR(255)"/>
|
||||
<column name="AUTHENTICATOR" type="VARCHAR(36)"/>
|
||||
<column name="REALM_ID" type="VARCHAR(36)"/>
|
||||
<column name="FLOW_ID" type="VARCHAR(36)"/>
|
||||
<column name="REQUIREMENT" type="INT"/>
|
||||
<column name="PRIORITY" type="INT"/>
|
||||
<column name="USER_SETUP_ALLOWED" type="BOOLEAN" defaultValueBoolean="false">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="AUTHENTICATOR_FLOW" type="BOOLEAN" defaultValueBoolean="false">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</createTable>
|
||||
<createTable tableName="AUTHENTICATOR_CONFIG">
|
||||
<column name="AUTHENTICATOR_ID" type="VARCHAR(36)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="VALUE" type="CLOB"/>
|
||||
<column name="NAME" type="VARCHAR(255)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</createTable>
|
||||
<createTable tableName="USER_FEDERATION_MAPPER">
|
||||
<column name="ID" type="VARCHAR(36)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="NAME" type="VARCHAR(255)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="FEDERATION_PROVIDER_ID" type="VARCHAR(36)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="FEDERATION_MAPPER_TYPE" type="VARCHAR(255)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="REALM_ID" type="VARCHAR(36)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</createTable>
|
||||
<createTable tableName="USER_FEDERATION_MAPPER_CONFIG">
|
||||
<column name="USER_FEDERATION_MAPPER_ID" type="VARCHAR(36)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="VALUE" type="VARCHAR(255)"/>
|
||||
<column name="NAME" type="VARCHAR(255)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</createTable>
|
||||
|
||||
<addColumn tableName="REALM">
|
||||
<column name="ADMIN_EVENTS_ENABLED" type="BOOLEAN" defaultValueBoolean="false">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="ADMIN_EVENTS_DETAILS_ENABLED" type="BOOLEAN" defaultValueBoolean="false">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="EDIT_USERNAME_ALLOWED" type="BOOLEAN" defaultValueBoolean="false">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</addColumn>
|
||||
<createTable tableName="CLIENT_SESSION_AUTH_STATUS">
|
||||
<column name="AUTHENTICATOR" type="VARCHAR(36)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="STATUS" type="INT"/>
|
||||
<column name="CLIENT_SESSION" type="VARCHAR(36)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</createTable>
|
||||
<addColumn tableName="CLIENT_SESSION">
|
||||
<column name="AUTH_USER_ID" type="VARCHAR(36)"/>
|
||||
</addColumn>
|
||||
<addColumn tableName="IDENTITY_PROVIDER">
|
||||
<column name="TRUST_EMAIL" type="BOOLEAN" defaultValueBoolean="false">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="UPDATE_PROFILE_FIRST_LGN_MD" type="VARCHAR(255)" defaultValue="on">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</addColumn>
|
||||
<!-- migrate value from UPDATE_PROFILE_FIRST_LOGIN to UPDATE_PROFILE_FIRST_LGN_MD then drop it -->
|
||||
<update tableName="IDENTITY_PROVIDER">
|
||||
<column name="UPDATE_PROFILE_FIRST_LGN_MD" value="off"/>
|
||||
<where>UPDATE_PROFILE_FIRST_LOGIN = :value</where>
|
||||
<whereParams>
|
||||
<param valueBoolean="false" />
|
||||
</whereParams>
|
||||
</update>
|
||||
<dropDefaultValue tableName="IDENTITY_PROVIDER" columnName="UPDATE_PROFILE_FIRST_LOGIN" />
|
||||
<dropColumn tableName="IDENTITY_PROVIDER" columnName="UPDATE_PROFILE_FIRST_LOGIN"/>
|
||||
|
||||
<addColumn tableName="USER_REQUIRED_ACTION">
|
||||
<column name="REQUIRED_ACTION" type="VARCHAR(255)" defaultValue=" ">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</addColumn>
|
||||
<!-- VERIFY_EMAIL, UPDATE_PROFILE, CONFIGURE_TOTP, UPDATE_PASSWORD -->
|
||||
<update tableName="USER_REQUIRED_ACTION">
|
||||
<column name="REQUIRED_ACTION" value="VERIFY_EMAIL"/>
|
||||
<where>ACTION = 0</where>
|
||||
</update>
|
||||
<update tableName="USER_REQUIRED_ACTION">
|
||||
<column name="REQUIRED_ACTION" value="UPDATE_PROFILE"/>
|
||||
<where>ACTION = 1</where>
|
||||
</update>
|
||||
<update tableName="USER_REQUIRED_ACTION">
|
||||
<column name="REQUIRED_ACTION" value="CONFIGURE_TOTP"/>
|
||||
<where>ACTION = 2</where>
|
||||
</update>
|
||||
<update tableName="USER_REQUIRED_ACTION">
|
||||
<column name="REQUIRED_ACTION" value="UPDATE_PASSWORD"/>
|
||||
<where>ACTION = 3</where>
|
||||
</update>
|
||||
<addPrimaryKey columnNames="ID" constraintName="CONSTRAINT_AUTH_PK" tableName="AUTHENTICATOR"/>
|
||||
<addPrimaryKey columnNames="ID" constraintName="CONSTRAINT_AUTH_FLOW_PK" tableName="AUTHENTICATION_FLOW"/>
|
||||
<addPrimaryKey columnNames="ID" constraintName="CONSTRAINT_AUTH_EXEC_PK" tableName="AUTHENTICATION_EXECUTION"/>
|
||||
<addPrimaryKey columnNames="AUTHENTICATOR_ID, NAME" constraintName="CONSTRAINT_AUTH_CFG_PK" tableName="AUTHENTICATOR_CONFIG"/>
|
||||
<dropPrimaryKey constraintName="CONSTRAINT_2" tableName="USER_REQUIRED_ACTION"/>
|
||||
<dropColumn tableName="USER_REQUIRED_ACTION" columnName="ACTION"/>
|
||||
<addPrimaryKey columnNames="REQUIRED_ACTION, USER_ID" constraintName="CONSTRAINT_REQUIRED_ACTION" tableName="USER_REQUIRED_ACTION"/>
|
||||
<addPrimaryKey columnNames="CLIENT_SESSION, AUTHENTICATOR" constraintName="CONSTRAINT_AUTH_STATUS_PK" tableName="CLIENT_SESSION_AUTH_STATUS"/>
|
||||
<addPrimaryKey columnNames="ID" constraintName="CONSTRAINT_FEDMAPPERPM" tableName="USER_FEDERATION_MAPPER"/>
|
||||
<addPrimaryKey columnNames="USER_FEDERATION_MAPPER_ID, NAME" constraintName="CONSTRAINT_FEDMAPPER_CFG_PM" tableName="USER_FEDERATION_MAPPER_CONFIG"/>
|
||||
<addForeignKeyConstraint baseColumnNames="CLIENT_SESSION" baseTableName="CLIENT_SESSION_AUTH_STATUS" constraintName="AUTH_STATUS_CONSTRAINT" referencedColumnNames="ID" referencedTableName="CLIENT_SESSION"/>
|
||||
<addForeignKeyConstraint baseColumnNames="REALM_ID" baseTableName="AUTHENTICATOR" constraintName="FK_AUTH_REALM" referencedColumnNames="ID" referencedTableName="REALM"/>
|
||||
<addForeignKeyConstraint baseColumnNames="REALM_ID" baseTableName="AUTHENTICATION_FLOW" constraintName="FK_AUTH_FLOW_REALM" referencedColumnNames="ID" referencedTableName="REALM"/>
|
||||
<addForeignKeyConstraint baseColumnNames="REALM_ID" baseTableName="AUTHENTICATION_EXECUTION" constraintName="FK_AUTH_EXEC_REALM" referencedColumnNames="ID" referencedTableName="REALM"/>
|
||||
<addForeignKeyConstraint baseColumnNames="FLOW_ID" baseTableName="AUTHENTICATION_EXECUTION" constraintName="FK_AUTH_EXEC_FLOW" referencedColumnNames="ID" referencedTableName="AUTHENTICATION_FLOW"/>
|
||||
<addForeignKeyConstraint baseColumnNames="REALM_ID" baseTableName="USER_FEDERATION_MAPPER" constraintName="FK_FEDMAPPERPM_REALM" referencedColumnNames="ID" referencedTableName="REALM"/>
|
||||
<addForeignKeyConstraint baseColumnNames="FEDERATION_PROVIDER_ID" baseTableName="USER_FEDERATION_MAPPER" constraintName="FK_FEDMAPPERPM_FEDPRV" referencedColumnNames="ID" referencedTableName="USER_FEDERATION_PROVIDER"/>
|
||||
<addForeignKeyConstraint baseColumnNames="USER_FEDERATION_MAPPER_ID" baseTableName="USER_FEDERATION_MAPPER_CONFIG" constraintName="FK_FEDMAPPER_CFG" referencedColumnNames="ID" referencedTableName="USER_FEDERATION_MAPPER"/>
|
||||
|
||||
<dropDefaultValue tableName="REALM" columnName="PASSWORD_CRED_GRANT_ALLOWED" />
|
||||
<dropColumn tableName="REALM" columnName="PASSWORD_CRED_GRANT_ALLOWED"/>
|
||||
|
||||
<!-- KEYCLOAK-1298 Change constraint names to be upper-cased -->
|
||||
<dropForeignKeyConstraint baseTableName="PROTOCOL_MAPPER_CONFIG" constraintName="FK_PMConfig" />
|
||||
<dropPrimaryKey constraintName="CONSTRAINT_PMConfig" tableName="PROTOCOL_MAPPER_CONFIG"/>
|
||||
<addPrimaryKey columnNames="PROTOCOL_MAPPER_ID, NAME" constraintName="CONSTRAINT_PMCONFIG" tableName="PROTOCOL_MAPPER_CONFIG"/>
|
||||
<addForeignKeyConstraint baseColumnNames="PROTOCOL_MAPPER_ID" baseTableName="PROTOCOL_MAPPER_CONFIG" constraintName="FK_PMCONFIG" referencedColumnNames="ID" referencedTableName="PROTOCOL_MAPPER"/>
|
||||
|
||||
<dropForeignKeyConstraint baseTableName="IDP_MAPPER_CONFIG" constraintName="FK_IDPMConfig" />
|
||||
<dropPrimaryKey constraintName="CONSTRAINT_IDPMConfig" tableName="IDP_MAPPER_CONFIG"/>
|
||||
<addPrimaryKey columnNames="IDP_MAPPER_ID, NAME" constraintName="CONSTRAINT_IDPMCONFIG" tableName="IDP_MAPPER_CONFIG"/>
|
||||
<addForeignKeyConstraint baseColumnNames="IDP_MAPPER_ID" baseTableName="IDP_MAPPER_CONFIG" constraintName="FK_IDPMCONFIG" referencedColumnNames="ID" referencedTableName="IDENTITY_PROVIDER_MAPPER"/>
|
||||
|
||||
<!-- Sybase specific hacks -->
|
||||
<modifySql dbms="sybase">
|
||||
<replace replace="[USER_REQUIRED_ACTION] DROP PRIMARY KEY" with="[USER_REQUIRED_ACTION] DROP CONSTRAINT CONSTRAINT_2" />
|
||||
<replace replace="[PROTOCOL_MAPPER_CONFIG] DROP PRIMARY KEY" with="[PROTOCOL_MAPPER_CONFIG] DROP CONSTRAINT CONSTRAINT_PMConfig" />
|
||||
<replace replace="[IDP_MAPPER_CONFIG] DROP PRIMARY KEY" with="[IDP_MAPPER_CONFIG] DROP CONSTRAINT CONSTRAINT_IDPMConfig" />
|
||||
<regExpReplace replace=".*(SET DEFAULT NULL)" with="SELECT 1" />
|
||||
</modifySql>
|
||||
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
|
||||
<changeSet author="bburke@redhat.com" id="1.4.0">
|
||||
<delete tableName="CLIENT_SESSION_AUTH_STATUS"/>
|
||||
<delete tableName="CLIENT_SESSION_ROLE"/>
|
||||
<delete tableName="CLIENT_SESSION_PROT_MAPPER"/>
|
||||
<delete tableName="CLIENT_SESSION_NOTE"/>
|
||||
<delete tableName="CLIENT_SESSION"/>
|
||||
<delete tableName="USER_SESSION_NOTE"/>
|
||||
<delete tableName="USER_SESSION"/>
|
||||
|
||||
<addColumn tableName="CLIENT">
|
||||
<column name="SERVICE_ACCOUNTS_ENABLED" type="BOOLEAN" defaultValueBoolean="false">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</addColumn>
|
||||
<addColumn tableName="CLIENT_SESSION">
|
||||
<column name="CURRENT_ACTION" type="VARCHAR(36)">
|
||||
<constraints nullable="true"/>
|
||||
</column>
|
||||
</addColumn>
|
||||
<addColumn tableName="AUTHENTICATION_FLOW">
|
||||
<column name="PROVIDER_ID" type="VARCHAR(36)" defaultValue="basic-flow">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="TOP_LEVEL" type="BOOLEAN" defaultValueBoolean="false">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="BUILT_IN" type="BOOLEAN" defaultValueBoolean="false">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</addColumn>
|
||||
<addColumn tableName="AUTHENTICATION_EXECUTION">
|
||||
<column name="AUTH_FLOW_ID" type="VARCHAR(36)">
|
||||
<constraints nullable="true"/>
|
||||
</column>
|
||||
<column name="AUTH_CONFIG" type="VARCHAR(36)">
|
||||
<constraints nullable="true"/>
|
||||
</column>
|
||||
</addColumn>
|
||||
<addColumn tableName="USER_ATTRIBUTE">
|
||||
<column name="ID" type="VARCHAR(36)" defaultValue="sybase-needs-something-here">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</addColumn>
|
||||
|
||||
<dropColumn tableName="AUTHENTICATOR" columnName="PROVIDER_ID"/>
|
||||
<renameTable oldTableName="AUTHENTICATOR_CONFIG" newTableName="AUTHENTICATOR_CONFIG_ENTRY"/>
|
||||
<renameTable oldTableName="AUTHENTICATOR" newTableName="AUTHENTICATOR_CONFIG"/>
|
||||
<!-- OAUTH_GRANT,
|
||||
CODE_TO_TOKEN,
|
||||
VERIFY_EMAIL,
|
||||
UPDATE_PROFILE,
|
||||
CONFIGURE_TOTP,
|
||||
UPDATE_PASSWORD,
|
||||
RECOVER_PASSWORD,
|
||||
AUTHENTICATE,
|
||||
SOCIAL_CALLBACK,
|
||||
LOGGED_OUT -->
|
||||
<update tableName="CLIENT_SESSION">
|
||||
<column name="CURRENT_ACTION" value="OAUTH_GRANT"/>
|
||||
<where>ACTION = 0</where>
|
||||
</update>
|
||||
<update tableName="CLIENT_SESSION">
|
||||
<column name="CURRENT_ACTION" value="CODE_TO_TOKEN"/>
|
||||
<where>ACTION = 1</where>
|
||||
</update>
|
||||
<update tableName="CLIENT_SESSION">
|
||||
<column name="CURRENT_ACTION" value="VERIFY_EMAIL"/>
|
||||
<where>ACTION = 2</where>
|
||||
</update>
|
||||
<update tableName="CLIENT_SESSION">
|
||||
<column name="CURRENT_ACTION" value="UPDATE_PROFILE"/>
|
||||
<where>ACTION = 3</where>
|
||||
</update>
|
||||
<update tableName="CLIENT_SESSION">
|
||||
<column name="CURRENT_ACTION" value="CONFIGURE_TOTP"/>
|
||||
<where>ACTION = 4</where>
|
||||
</update>
|
||||
<update tableName="CLIENT_SESSION">
|
||||
<column name="CURRENT_ACTION" value="UPDATE_PASSWORD"/>
|
||||
<where>ACTION = 5</where>
|
||||
</update>
|
||||
<update tableName="CLIENT_SESSION">
|
||||
<column name="CURRENT_ACTION" value="RECOVER_PASSWORD"/>
|
||||
<where>ACTION = 6</where>
|
||||
</update>
|
||||
<update tableName="CLIENT_SESSION">
|
||||
<column name="CURRENT_ACTION" value="AUTHENTICATE"/>
|
||||
<where>ACTION = 7</where>
|
||||
</update>
|
||||
<update tableName="CLIENT_SESSION">
|
||||
<column name="CURRENT_ACTION" value="SOCIAL_CALLBACK"/>
|
||||
<where>ACTION = 8</where>
|
||||
</update>
|
||||
<update tableName="CLIENT_SESSION">
|
||||
<column name="CURRENT_ACTION" value="LOGGED_OUT"/>
|
||||
<where>ACTION = 9</where>
|
||||
</update>
|
||||
|
||||
<createTable tableName="CLIENT_USER_SESSION_NOTE">
|
||||
<column name="NAME" type="VARCHAR(255)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="VALUE" type="VARCHAR(2048)"/>
|
||||
<column name="CLIENT_SESSION" type="VARCHAR(36)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</createTable>
|
||||
<createTable tableName="REQUIRED_ACTION_PROVIDER">
|
||||
<column name="ID" type="VARCHAR(36)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="ALIAS" type="VARCHAR(255)"/>
|
||||
<column name="NAME" type="VARCHAR(255)"/>
|
||||
<column name="REALM_ID" type="VARCHAR(36)"/>
|
||||
<column name="ENABLED" type="BOOLEAN" defaultValueBoolean="false">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="DEFAULT_ACTION" type="BOOLEAN" defaultValueBoolean="false">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="PROVIDER_ID" type="VARCHAR(255)"/>
|
||||
</createTable>
|
||||
<createTable tableName="REQUIRED_ACTION_CONFIG">
|
||||
<column name="REQUIRED_ACTION_ID" type="VARCHAR(36)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="VALUE" type="CLOB"/>
|
||||
<column name="NAME" type="VARCHAR(255)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</createTable>
|
||||
|
||||
<customChange class="org.keycloak.connections.jpa.updater.liquibase.custom.JpaUpdate1_4_0_Final"/>
|
||||
|
||||
<dropPrimaryKey constraintName="CONSTRAINT_6" tableName="USER_ATTRIBUTE"/>
|
||||
<addPrimaryKey columnNames="ID" constraintName="CONSTRAINT_USER_ATTRIBUTE_PK" tableName="USER_ATTRIBUTE"/>
|
||||
<addPrimaryKey columnNames="ID" constraintName="CONSTRAINT_REQ_ACT_PRV_PK" tableName="REQUIRED_ACTION_PROVIDER"/>
|
||||
<addPrimaryKey columnNames="REQUIRED_ACTION_ID, NAME" constraintName="CONSTRAINT_REQ_ACT_CFG_PK" tableName="REQUIRED_ACTION_CONFIG"/>
|
||||
<addPrimaryKey columnNames="CLIENT_SESSION, NAME" constraintName="CONSTR_CL_USR_SES_NOTE" tableName="CLIENT_USER_SESSION_NOTE"/>
|
||||
<addForeignKeyConstraint baseColumnNames="REALM_ID" baseTableName="REQUIRED_ACTION_PROVIDER" constraintName="FK_REQ_ACT_REALM" referencedColumnNames="ID" referencedTableName="REALM"/>
|
||||
<addForeignKeyConstraint baseColumnNames="CLIENT_SESSION" baseTableName="CLIENT_USER_SESSION_NOTE" constraintName="FK_CL_USR_SES_NOTE" referencedColumnNames="ID" referencedTableName="CLIENT_SESSION"/>
|
||||
<dropColumn tableName="CLIENT_SESSION" columnName="ACTION"/>
|
||||
<addColumn tableName="USER_ENTITY">
|
||||
<column name="CREATED_TIMESTAMP" type="BIGINT"/>
|
||||
<column name="SERVICE_ACCOUNT_CLIENT_LINK" type="VARCHAR(36)"/>
|
||||
</addColumn>
|
||||
|
||||
<!-- Sybase specific hacks -->
|
||||
<modifySql dbms="sybase">
|
||||
<replace replace="[USER_ATTRIBUTE] DROP PRIMARY KEY" with="[USER_ATTRIBUTE] DROP CONSTRAINT CONSTRAINT_6" />
|
||||
</modifySql>
|
||||
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
Regular → Executable
+4
@@ -4,4 +4,8 @@
|
||||
<include file="META-INF/jpa-changelog-1.1.0.Beta1.xml"/>
|
||||
<include file="META-INF/jpa-changelog-1.1.0.Final.xml"/>
|
||||
<include file="META-INF/jpa-changelog-1.2.0.Beta1.xml"/>
|
||||
<include file="META-INF/jpa-changelog-1.2.0.CR1.xml"/>
|
||||
<include file="META-INF/jpa-changelog-1.2.0.Final.xml"/>
|
||||
<include file="META-INF/jpa-changelog-1.3.0.xml"/>
|
||||
<include file="META-INF/jpa-changelog-1.4.0.xml"/>
|
||||
</databaseChangeLog>
|
||||
+36
-39
@@ -1,39 +1,36 @@
|
||||
<?xml version="1.0"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>1.2.0.Beta1-SNAPSHOT</version>
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>keycloak-connections-jpa</artifactId>
|
||||
<name>Keycloak Connections JPA</name>
|
||||
<description/>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-core</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-model-api</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.hibernate.javax.persistence</groupId>
|
||||
<artifactId>hibernate-jpa-2.0-api</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.hibernate</groupId>
|
||||
<artifactId>hibernate-entitymanager</artifactId>
|
||||
<version>${hibernate.entitymanager.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
<?xml version="1.0"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>1.4.0.Final</version>
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>keycloak-connections-jpa</artifactId>
|
||||
<name>Keycloak Connections JPA</name>
|
||||
<description/>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-model-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.hibernate.javax.persistence</groupId>
|
||||
<artifactId>hibernate-jpa-2.1-api</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.hibernate</groupId>
|
||||
<artifactId>hibernate-entitymanager</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
+103
-76
@@ -13,10 +13,12 @@ import javax.persistence.EntityManagerFactory;
|
||||
import javax.persistence.Persistence;
|
||||
import javax.sql.DataSource;
|
||||
import java.sql.Connection;
|
||||
import java.sql.DatabaseMetaData;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
@@ -29,6 +31,8 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide
|
||||
private volatile EntityManagerFactory emf;
|
||||
|
||||
private Config.Scope config;
|
||||
|
||||
private Map<String,String> operationalInfo;
|
||||
|
||||
@Override
|
||||
public JpaConnectionProvider create(KeycloakSession session) {
|
||||
@@ -70,105 +74,123 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide
|
||||
|
||||
Connection connection = null;
|
||||
|
||||
String unitName = config.get("unitName");
|
||||
String databaseSchema = config.get("databaseSchema");
|
||||
|
||||
Map<String, Object> properties = new HashMap<String, Object>();
|
||||
|
||||
// Only load config from keycloak-server.json if unitName is not specified
|
||||
if (unitName == null) {
|
||||
unitName = "keycloak-default";
|
||||
String unitName = "keycloak-default";
|
||||
|
||||
String dataSource = config.get("dataSource");
|
||||
if (dataSource != null) {
|
||||
if (config.getBoolean("jta", false)) {
|
||||
properties.put(AvailableSettings.JTA_DATASOURCE, dataSource);
|
||||
} else {
|
||||
properties.put(AvailableSettings.NON_JTA_DATASOURCE, dataSource);
|
||||
}
|
||||
String dataSource = config.get("dataSource");
|
||||
if (dataSource != null) {
|
||||
if (config.getBoolean("jta", false)) {
|
||||
properties.put(AvailableSettings.JTA_DATASOURCE, dataSource);
|
||||
} else {
|
||||
properties.put(AvailableSettings.JDBC_URL, config.get("url"));
|
||||
properties.put(AvailableSettings.JDBC_DRIVER, config.get("driver"));
|
||||
|
||||
String user = config.get("user");
|
||||
if (user != null) {
|
||||
properties.put(AvailableSettings.JDBC_USER, user);
|
||||
}
|
||||
String password = config.get("password");
|
||||
if (password != null) {
|
||||
properties.put(AvailableSettings.JDBC_PASSWORD, password);
|
||||
}
|
||||
properties.put(AvailableSettings.NON_JTA_DATASOURCE, dataSource);
|
||||
}
|
||||
} else {
|
||||
properties.put(AvailableSettings.JDBC_URL, config.get("url"));
|
||||
properties.put(AvailableSettings.JDBC_DRIVER, config.get("driver"));
|
||||
|
||||
String driverDialect = config.get("driverDialect");
|
||||
if (driverDialect != null && driverDialect.length() > 0) {
|
||||
properties.put("hibernate.dialect", driverDialect);
|
||||
String user = config.get("user");
|
||||
if (user != null) {
|
||||
properties.put(AvailableSettings.JDBC_USER, user);
|
||||
}
|
||||
|
||||
if (databaseSchema != null) {
|
||||
if (databaseSchema.equals("development-update")) {
|
||||
properties.put("hibernate.hbm2ddl.auto", "update");
|
||||
databaseSchema = null;
|
||||
} else if (databaseSchema.equals("development-validate")) {
|
||||
properties.put("hibernate.hbm2ddl.auto", "validate");
|
||||
databaseSchema = null;
|
||||
}
|
||||
String password = config.get("password");
|
||||
if (password != null) {
|
||||
properties.put(AvailableSettings.JDBC_PASSWORD, password);
|
||||
}
|
||||
}
|
||||
|
||||
properties.put("hibernate.show_sql", config.getBoolean("showSql", false));
|
||||
properties.put("hibernate.format_sql", config.getBoolean("formatSql", true));
|
||||
String driverDialect = config.get("driverDialect");
|
||||
if (driverDialect != null && driverDialect.length() > 0) {
|
||||
properties.put("hibernate.dialect", driverDialect);
|
||||
}
|
||||
|
||||
String schema = config.get("schema");
|
||||
if (schema != null) {
|
||||
properties.put("hibernate.default_schema", schema);
|
||||
}
|
||||
|
||||
if (databaseSchema != null) {
|
||||
logger.trace("Updating database");
|
||||
|
||||
JpaUpdaterProvider updater = session.getProvider(JpaUpdaterProvider.class);
|
||||
if (updater == null) {
|
||||
throw new RuntimeException("Can't update database: JPA updater provider not found");
|
||||
if (databaseSchema.equals("development-update")) {
|
||||
properties.put("hibernate.hbm2ddl.auto", "update");
|
||||
databaseSchema = null;
|
||||
} else if (databaseSchema.equals("development-validate")) {
|
||||
properties.put("hibernate.hbm2ddl.auto", "validate");
|
||||
databaseSchema = null;
|
||||
}
|
||||
|
||||
connection = getConnection();
|
||||
|
||||
if (databaseSchema.equals("update")) {
|
||||
String currentVersion = null;
|
||||
try {
|
||||
ResultSet resultSet = connection.createStatement().executeQuery(updater.getCurrentVersionSql());
|
||||
if (resultSet.next()) {
|
||||
currentVersion = resultSet.getString(1);
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
}
|
||||
|
||||
if (currentVersion == null || !JpaUpdaterProvider.LAST_VERSION.equals(currentVersion)) {
|
||||
updater.update(session, connection);
|
||||
} else {
|
||||
logger.debug("Database is up to date");
|
||||
}
|
||||
} else if (databaseSchema.equals("validate")) {
|
||||
updater.validate(connection);
|
||||
} else {
|
||||
throw new RuntimeException("Invalid value for databaseSchema: " + databaseSchema);
|
||||
}
|
||||
|
||||
logger.trace("Database update completed");
|
||||
}
|
||||
|
||||
logger.trace("Creating EntityManagerFactory");
|
||||
emf = Persistence.createEntityManagerFactory(unitName, properties);
|
||||
logger.trace("EntityManagerFactory created");
|
||||
properties.put("hibernate.show_sql", config.getBoolean("showSql", false));
|
||||
properties.put("hibernate.format_sql", config.getBoolean("formatSql", true));
|
||||
|
||||
// Close after creating EntityManagerFactory to prevent in-mem databases from closing
|
||||
if (connection != null) {
|
||||
try {
|
||||
connection.close();
|
||||
} catch (SQLException e) {
|
||||
logger.warn(e);
|
||||
}
|
||||
connection = getConnection();
|
||||
try{
|
||||
prepareOperationalInfo(connection);
|
||||
|
||||
if (databaseSchema != null) {
|
||||
logger.trace("Updating database");
|
||||
|
||||
JpaUpdaterProvider updater = session.getProvider(JpaUpdaterProvider.class);
|
||||
if (updater == null) {
|
||||
throw new RuntimeException("Can't update database: JPA updater provider not found");
|
||||
}
|
||||
|
||||
if (databaseSchema.equals("update")) {
|
||||
String currentVersion = null;
|
||||
try {
|
||||
ResultSet resultSet = connection.createStatement().executeQuery(updater.getCurrentVersionSql(schema));
|
||||
if (resultSet.next()) {
|
||||
currentVersion = resultSet.getString(1);
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
}
|
||||
|
||||
if (currentVersion == null || !JpaUpdaterProvider.LAST_VERSION.equals(currentVersion)) {
|
||||
updater.update(session, connection, schema);
|
||||
} else {
|
||||
logger.debug("Database is up to date");
|
||||
}
|
||||
} else if (databaseSchema.equals("validate")) {
|
||||
updater.validate(connection, schema);
|
||||
} else {
|
||||
throw new RuntimeException("Invalid value for databaseSchema: " + databaseSchema);
|
||||
}
|
||||
|
||||
logger.trace("Database update completed");
|
||||
}
|
||||
|
||||
logger.trace("Creating EntityManagerFactory");
|
||||
emf = Persistence.createEntityManagerFactory(unitName, properties);
|
||||
logger.trace("EntityManagerFactory created");
|
||||
|
||||
} finally {
|
||||
// Close after creating EntityManagerFactory to prevent in-mem databases from closing
|
||||
if (connection != null) {
|
||||
try {
|
||||
connection.close();
|
||||
} catch (SQLException e) {
|
||||
logger.warn(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void prepareOperationalInfo(Connection connection) {
|
||||
try {
|
||||
operationalInfo = new LinkedHashMap<>();
|
||||
DatabaseMetaData md = connection.getMetaData();
|
||||
operationalInfo.put("databaseUrl",md.getURL());
|
||||
operationalInfo.put("databaseUser", md.getUserName());
|
||||
operationalInfo.put("databaseProduct", md.getDatabaseProductName() + " " + md.getDatabaseProductVersion());
|
||||
operationalInfo.put("databaseDriver", md.getDriverName() + " " + md.getDriverVersion());
|
||||
} catch (SQLException e) {
|
||||
logger.warn("Unable to prepare operational info due database exception: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private Connection getConnection() {
|
||||
try {
|
||||
@@ -184,5 +206,10 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide
|
||||
throw new RuntimeException("Failed to connect to database", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String,String> getOperationalInfo() {
|
||||
return operationalInfo;
|
||||
}
|
||||
|
||||
}
|
||||
+3
-2
@@ -1,9 +1,10 @@
|
||||
package org.keycloak.connections.jpa;
|
||||
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
import org.keycloak.provider.ServerInfoAwareProviderFactory;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public interface JpaConnectionProviderFactory extends ProviderFactory<JpaConnectionProvider> {
|
||||
public interface JpaConnectionProviderFactory extends ServerInfoAwareProviderFactory<JpaConnectionProvider> {
|
||||
|
||||
}
|
||||
@@ -9,6 +9,11 @@ import org.keycloak.provider.Spi;
|
||||
*/
|
||||
public class JpaConnectionSpi implements Spi {
|
||||
|
||||
@Override
|
||||
public boolean isInternal() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "connectionsJpa";
|
||||
|
||||
+53
-53
@@ -1,53 +1,53 @@
|
||||
package org.keycloak.connections.jpa;
|
||||
|
||||
import org.keycloak.models.KeycloakTransaction;
|
||||
|
||||
import javax.persistence.EntityManager;
|
||||
import javax.persistence.PersistenceException;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class JpaKeycloakTransaction implements KeycloakTransaction {
|
||||
|
||||
protected EntityManager em;
|
||||
|
||||
public JpaKeycloakTransaction(EntityManager em) {
|
||||
this.em = em;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void begin() {
|
||||
em.getTransaction().begin();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void commit() {
|
||||
try {
|
||||
em.getTransaction().commit();
|
||||
} catch (PersistenceException e) {
|
||||
throw PersistenceExceptionConverter.convert(e.getCause() != null ? e.getCause() : e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rollback() {
|
||||
em.getTransaction().rollback();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRollbackOnly() {
|
||||
em.getTransaction().setRollbackOnly();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getRollbackOnly() {
|
||||
return em.getTransaction().getRollbackOnly();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isActive() {
|
||||
return em.getTransaction().isActive();
|
||||
}
|
||||
}
|
||||
package org.keycloak.connections.jpa;
|
||||
|
||||
import org.keycloak.models.KeycloakTransaction;
|
||||
|
||||
import javax.persistence.EntityManager;
|
||||
import javax.persistence.PersistenceException;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class JpaKeycloakTransaction implements KeycloakTransaction {
|
||||
|
||||
protected EntityManager em;
|
||||
|
||||
public JpaKeycloakTransaction(EntityManager em) {
|
||||
this.em = em;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void begin() {
|
||||
em.getTransaction().begin();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void commit() {
|
||||
try {
|
||||
em.getTransaction().commit();
|
||||
} catch (PersistenceException e) {
|
||||
throw PersistenceExceptionConverter.convert(e.getCause() != null ? e.getCause() : e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rollback() {
|
||||
em.getTransaction().rollback();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRollbackOnly() {
|
||||
em.getTransaction().setRollbackOnly();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getRollbackOnly() {
|
||||
return em.getTransaction().getRollbackOnly();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isActive() {
|
||||
return em.getTransaction().isActive();
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -12,12 +12,12 @@ public interface JpaUpdaterProvider extends Provider {
|
||||
|
||||
public String FIRST_VERSION = "1.0.0.Final";
|
||||
|
||||
public String LAST_VERSION = "1.2.0.Beta1";
|
||||
public String LAST_VERSION = "1.4.0";
|
||||
|
||||
public String getCurrentVersionSql();
|
||||
public String getCurrentVersionSql(String defaultSchema);
|
||||
|
||||
public void update(KeycloakSession session, Connection connection);
|
||||
public void update(KeycloakSession session, Connection connection, String defaultSchema);
|
||||
|
||||
public void validate(Connection connection);
|
||||
public void validate(Connection connection, String defaultSchema);
|
||||
|
||||
}
|
||||
@@ -9,6 +9,11 @@ import org.keycloak.provider.Spi;
|
||||
*/
|
||||
public class JpaUpdaterSpi implements Spi {
|
||||
|
||||
@Override
|
||||
public boolean isInternal() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "connectionsJpaUpdater";
|
||||
|
||||
@@ -1,41 +1,54 @@
|
||||
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd"
|
||||
version="1.0">
|
||||
<persistence-unit name="keycloak-default" transaction-type="RESOURCE_LOCAL">
|
||||
<class>org.keycloak.models.jpa.entities.ApplicationEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.CredentialEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.OAuthClientEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.RealmEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.RealmAttributeEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.RequiredCredentialEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.UserFederationProviderEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.RoleEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.FederatedIdentityEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.UserEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.UserRequiredActionEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.UserAttributeEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.UserRoleMappingEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.ScopeMappingEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.IdentityProviderEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.ClientIdentityProviderMappingEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.ProtocolMapperEntity</class>
|
||||
|
||||
<!-- JpaUserSessionProvider -->
|
||||
<class>org.keycloak.models.sessions.jpa.entities.ClientSessionEntity</class>
|
||||
<class>org.keycloak.models.sessions.jpa.entities.ClientSessionRoleEntity</class>
|
||||
<class>org.keycloak.models.sessions.jpa.entities.ClientSessionNoteEntity</class>
|
||||
<class>org.keycloak.models.sessions.jpa.entities.UserSessionNoteEntity</class>
|
||||
<class>org.keycloak.models.sessions.jpa.entities.UserSessionEntity</class>
|
||||
<class>org.keycloak.models.sessions.jpa.entities.UsernameLoginFailureEntity</class>
|
||||
|
||||
<!-- JpaAuditProvider -->
|
||||
<class>org.keycloak.events.jpa.EventEntity</class>
|
||||
|
||||
<exclude-unlisted-classes>true</exclude-unlisted-classes>
|
||||
|
||||
<properties>
|
||||
<property name="jboss.as.jpa.managed" value="false"/>
|
||||
</properties>
|
||||
</persistence-unit>
|
||||
</persistence>
|
||||
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd"
|
||||
version="1.0">
|
||||
<persistence-unit name="keycloak-default" transaction-type="RESOURCE_LOCAL">
|
||||
<class>org.keycloak.models.jpa.entities.ClientEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.CredentialEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.RealmEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.RealmAttributeEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.RequiredCredentialEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.UserFederationProviderEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.UserFederationMapperEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.RoleEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.FederatedIdentityEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.MigrationModelEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.UserEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.UserRequiredActionEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.UserAttributeEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.UserRoleMappingEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.ScopeMappingEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.IdentityProviderEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.IdentityProviderMapperEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.ClientIdentityProviderMappingEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.ProtocolMapperEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.UserConsentEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.UserConsentRoleEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.UserConsentProtocolMapperEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.AuthenticationFlowEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.AuthenticationExecutionEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.AuthenticatorConfigEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.RequiredActionProviderEntity</class>
|
||||
|
||||
<!-- JpaUserSessionProvider -->
|
||||
<class>org.keycloak.models.sessions.jpa.entities.ClientSessionEntity</class>
|
||||
<class>org.keycloak.models.sessions.jpa.entities.ClientSessionRoleEntity</class>
|
||||
<class>org.keycloak.models.sessions.jpa.entities.ClientSessionAuthStatusEntity</class>
|
||||
<class>org.keycloak.models.sessions.jpa.entities.ClientSessionProtocolMapperEntity</class>
|
||||
<class>org.keycloak.models.sessions.jpa.entities.ClientSessionNoteEntity</class>
|
||||
<class>org.keycloak.models.sessions.jpa.entities.ClientUserSessionNoteEntity</class>
|
||||
<class>org.keycloak.models.sessions.jpa.entities.UserSessionNoteEntity</class>
|
||||
<class>org.keycloak.models.sessions.jpa.entities.UserSessionEntity</class>
|
||||
<class>org.keycloak.models.sessions.jpa.entities.UsernameLoginFailureEntity</class>
|
||||
|
||||
<!-- JpaAuditProviders -->
|
||||
<class>org.keycloak.events.jpa.EventEntity</class>
|
||||
<class>org.keycloak.events.jpa.AdminEventEntity</class>
|
||||
|
||||
<exclude-unlisted-classes>true</exclude-unlisted-classes>
|
||||
|
||||
<properties>
|
||||
<property name="jboss.as.jpa.managed" value="false"/>
|
||||
</properties>
|
||||
</persistence-unit>
|
||||
</persistence>
|
||||
Regular → Executable
+1
-4
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>1.2.0.Beta1-SNAPSHOT</version>
|
||||
<version>1.4.0.Final</version>
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
@@ -17,17 +17,14 @@
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-core</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-model-api</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-connections-mongo</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mongodb</groupId>
|
||||
|
||||
+5
-5
@@ -6,10 +6,7 @@ import com.mongodb.DBCollection;
|
||||
import com.mongodb.DBCursor;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.connections.mongo.updater.MongoUpdaterProvider;
|
||||
import org.keycloak.connections.mongo.updater.impl.updates.Update;
|
||||
import org.keycloak.connections.mongo.updater.impl.updates.Update1_0_0_Final;
|
||||
import org.keycloak.connections.mongo.updater.impl.updates.Update1_1_0_Beta1;
|
||||
import org.keycloak.connections.mongo.updater.impl.updates.Update1_2_0_Beta1;
|
||||
import org.keycloak.connections.mongo.updater.impl.updates.*;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
||||
import java.util.Date;
|
||||
@@ -28,7 +25,10 @@ public class DefaultMongoUpdaterProvider implements MongoUpdaterProvider {
|
||||
private Class<? extends Update>[] updates = new Class[]{
|
||||
Update1_0_0_Final.class,
|
||||
Update1_1_0_Beta1.class,
|
||||
Update1_2_0_Beta1.class
|
||||
Update1_2_0_Beta1.class,
|
||||
Update1_2_0_CR1.class,
|
||||
Update1_3_0.class,
|
||||
Update1_4_0.class
|
||||
};
|
||||
|
||||
@Override
|
||||
|
||||
+6
-9
@@ -53,15 +53,12 @@ public abstract class Update {
|
||||
log.debugv("Deleted entries from {0}", collection);
|
||||
}
|
||||
|
||||
protected String insertApplicationRole(DBCollection roles, String roleName, String applicationId) {
|
||||
BasicDBObject role = new BasicDBObject();
|
||||
String roleId = KeycloakModelUtils.generateId();
|
||||
role.append("_id", roleId);
|
||||
role.append("name", roleName);
|
||||
role.append("applicationId", applicationId);
|
||||
role.append("nameIndex", applicationId + "//" + roleName);
|
||||
roles.insert(role);
|
||||
return roleId;
|
||||
protected void removeField(String collection, String field) {
|
||||
db.getCollection(collection).update(new BasicDBObject(), new BasicDBObject("$unset" , new BasicDBObject(field, 1)), false, true);
|
||||
}
|
||||
|
||||
protected void renameCollection(String collection, String newName) {
|
||||
db.getCollection(collection).rename(newName);
|
||||
}
|
||||
|
||||
public void setLog(Logger log) {
|
||||
|
||||
+12
-1
@@ -249,7 +249,7 @@ public class Update1_2_0_Beta1 extends Update {
|
||||
dbMapper.put("protocolMapper", protocolMapper.getProtocolMapper());
|
||||
|
||||
Map<String, String> config = protocolMapper.getConfig();
|
||||
BasicDBObject dbConfig = MapMapper.convertMap(config);
|
||||
BasicDBObject dbConfig = MapMapper.convertMap(config, null);
|
||||
dbMapper.put("config", dbConfig);
|
||||
|
||||
dbProtocolMappers.add(dbMapper);
|
||||
@@ -266,4 +266,15 @@ public class Update1_2_0_Beta1 extends Update {
|
||||
}
|
||||
}
|
||||
|
||||
private String insertApplicationRole(DBCollection roles, String roleName, String applicationId) {
|
||||
BasicDBObject role = new BasicDBObject();
|
||||
String roleId = KeycloakModelUtils.generateId();
|
||||
role.append("_id", roleId);
|
||||
role.append("name", roleName);
|
||||
role.append("applicationId", applicationId);
|
||||
role.append("nameIndex", applicationId + "//" + roleName);
|
||||
roles.insert(role);
|
||||
return roleId;
|
||||
}
|
||||
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package org.keycloak.connections.mongo.updater.impl.updates;
|
||||
|
||||
import com.mongodb.BasicDBObject;
|
||||
import com.mongodb.DBCollection;
|
||||
import com.mongodb.DBCursor;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class Update1_2_0_CR1 extends Update {
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "1.2.0.CR1";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(KeycloakSession session) {
|
||||
deleteEntries("clientSessions");
|
||||
deleteEntries("sessions");
|
||||
|
||||
convertApplicationsToClients();
|
||||
convertOAuthClientsToClients();
|
||||
|
||||
db.getCollection("realms").update(new BasicDBObject(), new BasicDBObject("$rename", new BasicDBObject("adminAppId", "masterAdminClient")), false, true);
|
||||
|
||||
ensureIndex("userConsents", new String[]{"clientId", "userId"}, true, false);
|
||||
}
|
||||
|
||||
private void convertApplicationsToClients() {
|
||||
DBCollection applications = db.getCollection("applications");
|
||||
applications.dropIndex("realmId_1_name_1");
|
||||
|
||||
applications.update(new BasicDBObject(), new BasicDBObject("$set", new BasicDBObject("consentRequired", false)), false, true);
|
||||
applications.update(new BasicDBObject(), new BasicDBObject("$rename", new BasicDBObject("name", "clientId")), false, true);
|
||||
renameCollection("applications", "clients");
|
||||
log.debugv("Converted applications to clients");
|
||||
|
||||
DBCollection roles = db.getCollection("roles");
|
||||
roles.update(new BasicDBObject(), new BasicDBObject("$rename", new BasicDBObject("applicationId", "clientId")), false, true);
|
||||
log.debugv("Renamed roles.applicationId to roles.clientId");
|
||||
|
||||
ensureIndex("clients", new String[]{"realmId", "clientId"}, true, false);
|
||||
}
|
||||
|
||||
private void convertOAuthClientsToClients() {
|
||||
DBCollection clients = db.getCollection("clients");
|
||||
DBCollection oauthClients = db.getCollection("oauthClients");
|
||||
oauthClients.dropIndex("realmId_1_name_1");
|
||||
|
||||
oauthClients.update(new BasicDBObject(), new BasicDBObject("$rename", new BasicDBObject("name", "clientId")), false, true);
|
||||
oauthClients.update(new BasicDBObject(), new BasicDBObject("$set", new BasicDBObject("consentRequired", true)), false, true);
|
||||
|
||||
DBCursor curs = oauthClients.find();
|
||||
while (curs.hasNext()) {
|
||||
clients.insert(curs.next());
|
||||
}
|
||||
|
||||
oauthClients.drop();
|
||||
log.debugv("Converted oauthClients to clients");
|
||||
}
|
||||
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package org.keycloak.connections.mongo.updater.impl.updates;
|
||||
|
||||
import com.mongodb.BasicDBList;
|
||||
import com.mongodb.BasicDBObject;
|
||||
import com.mongodb.DBCollection;
|
||||
import com.mongodb.DBCursor;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class Update1_3_0 extends Update {
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "1.3.0";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(KeycloakSession session) {
|
||||
deleteEntries("clientSessions");
|
||||
deleteEntries("sessions");
|
||||
|
||||
removeField("realms", "passwordCredentialGrantAllowed");
|
||||
|
||||
updateIdentityProviders();
|
||||
}
|
||||
|
||||
private void updateIdentityProviders() {
|
||||
DBCollection realms = db.getCollection("realms");
|
||||
DBCursor realmsCursor = realms.find();
|
||||
|
||||
try {
|
||||
while (realmsCursor.hasNext()) {
|
||||
BasicDBObject realm = (BasicDBObject) realmsCursor.next();
|
||||
|
||||
BasicDBList identityProviders = (BasicDBList) realm.get("identityProviders");
|
||||
if (identityProviders != null) {
|
||||
for (Object ipObj : identityProviders) {
|
||||
BasicDBObject identityProvider = (BasicDBObject) ipObj;
|
||||
|
||||
boolean updateProfileFirstLogin = identityProvider.getBoolean("updateProfileFirstLogin");
|
||||
String upflMode = updateProfileFirstLogin ? IdentityProviderRepresentation.UPFLM_ON : IdentityProviderRepresentation.UPFLM_OFF;
|
||||
identityProvider.put("updateProfileFirstLoginMode", upflMode);
|
||||
identityProvider.removeField("updateProfileFirstLogin");
|
||||
|
||||
identityProvider.put("trustEmail", false);
|
||||
}
|
||||
}
|
||||
|
||||
realms.save(realm);
|
||||
}
|
||||
} finally {
|
||||
realmsCursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
package org.keycloak.connections.mongo.updater.impl.updates;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
|
||||
import com.mongodb.BasicDBList;
|
||||
import com.mongodb.BasicDBObject;
|
||||
import com.mongodb.DBCollection;
|
||||
import com.mongodb.DBCursor;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class Update1_4_0 extends Update {
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "1.4.0";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(KeycloakSession session) throws ClassNotFoundException {
|
||||
deleteEntries("clientSessions");
|
||||
deleteEntries("sessions");
|
||||
|
||||
// Remove warning
|
||||
removeField("realms", "authenticators");
|
||||
|
||||
updateUserAttributes();
|
||||
}
|
||||
|
||||
private void updateUserAttributes() {
|
||||
DBCollection users = db.getCollection("users");
|
||||
DBCursor usersCursor = users.find();
|
||||
|
||||
try {
|
||||
while (usersCursor.hasNext()) {
|
||||
BasicDBObject user = (BasicDBObject) usersCursor.next();
|
||||
|
||||
BasicDBObject attributes = (BasicDBObject) user.get("attributes");
|
||||
if (attributes != null) {
|
||||
for (Map.Entry<String, Object> attr : new HashSet<>(attributes.entrySet())) {
|
||||
String attrName = attr.getKey();
|
||||
Object attrValue = attr.getValue();
|
||||
if (attrValue != null && attrValue instanceof String) {
|
||||
BasicDBList asList = new BasicDBList();
|
||||
asList.add(attrValue);
|
||||
attributes.put(attrName, asList);
|
||||
}
|
||||
}
|
||||
|
||||
user.put("attributes", attributes);
|
||||
|
||||
users.save(user);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
usersCursor.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
+35
-37
@@ -1,37 +1,35 @@
|
||||
<?xml version="1.0"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>1.2.0.Beta1-SNAPSHOT</version>
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>keycloak-connections-mongo</artifactId>
|
||||
<name>Keycloak Connections Mongo</name>
|
||||
<description/>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-core</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-model-api</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mongodb</groupId>
|
||||
<artifactId>mongo-java-driver</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jboss.logging</groupId>
|
||||
<artifactId>jboss-logging</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
<?xml version="1.0"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>1.4.0.Final</version>
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>keycloak-connections-mongo</artifactId>
|
||||
<name>Keycloak Connections Mongo</name>
|
||||
<description/>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-model-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mongodb</groupId>
|
||||
<artifactId>mongo-java-driver</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jboss.logging</groupId>
|
||||
<artifactId>jboss-logging</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
+62
-20
@@ -5,6 +5,7 @@ import com.mongodb.MongoClient;
|
||||
import com.mongodb.MongoClientOptions;
|
||||
import com.mongodb.MongoCredential;
|
||||
import com.mongodb.ServerAddress;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.connections.mongo.api.MongoStore;
|
||||
@@ -16,7 +17,10 @@ import org.keycloak.models.KeycloakSessionFactory;
|
||||
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
@@ -33,13 +37,20 @@ public class DefaultMongoConnectionFactoryProvider implements MongoConnectionPro
|
||||
"org.keycloak.models.entities.RequiredCredentialEntity",
|
||||
"org.keycloak.models.entities.CredentialEntity",
|
||||
"org.keycloak.models.entities.FederatedIdentityEntity",
|
||||
"org.keycloak.models.mongo.keycloak.entities.MongoApplicationEntity",
|
||||
"org.keycloak.models.mongo.keycloak.entities.MongoOAuthClientEntity",
|
||||
"org.keycloak.models.mongo.keycloak.entities.MongoClientEntity",
|
||||
"org.keycloak.models.sessions.mongo.entities.MongoUsernameLoginFailureEntity",
|
||||
"org.keycloak.models.sessions.mongo.entities.MongoUserSessionEntity",
|
||||
"org.keycloak.models.sessions.mongo.entities.MongoClientSessionEntity",
|
||||
"org.keycloak.models.entities.UserFederationProviderEntity",
|
||||
"org.keycloak.models.entities.ProtocolMapperEntity"
|
||||
"org.keycloak.models.entities.UserFederationMapperEntity",
|
||||
"org.keycloak.models.entities.ProtocolMapperEntity",
|
||||
"org.keycloak.models.entities.IdentityProviderMapperEntity",
|
||||
"org.keycloak.models.mongo.keycloak.entities.MongoUserConsentEntity",
|
||||
"org.keycloak.models.mongo.keycloak.entities.MongoMigrationModelEntity",
|
||||
"org.keycloak.models.entities.AuthenticationExecutionEntity",
|
||||
"org.keycloak.models.entities.AuthenticationFlowEntity",
|
||||
"org.keycloak.models.entities.AuthenticatorConfigEntity",
|
||||
"org.keycloak.models.entities.RequiredActionProviderEntity",
|
||||
};
|
||||
|
||||
private static final Logger logger = Logger.getLogger(DefaultMongoConnectionFactoryProvider.class);
|
||||
@@ -48,7 +59,9 @@ public class DefaultMongoConnectionFactoryProvider implements MongoConnectionPro
|
||||
|
||||
private MongoStore mongoStore;
|
||||
private DB db;
|
||||
private Config.Scope config;
|
||||
protected Config.Scope config;
|
||||
|
||||
private Map<String,String> operationalInfo;
|
||||
|
||||
@Override
|
||||
public MongoConnectionProvider create(KeycloakSession session) {
|
||||
@@ -75,21 +88,9 @@ public class DefaultMongoConnectionFactoryProvider implements MongoConnectionPro
|
||||
synchronized (this) {
|
||||
if (client == null) {
|
||||
try {
|
||||
String host = config.get("host", ServerAddress.defaultHost());
|
||||
int port = config.getInt("port", ServerAddress.defaultPort());
|
||||
this.client = createMongoClient();
|
||||
|
||||
String dbName = config.get("db", "keycloak");
|
||||
|
||||
String user = config.get("user");
|
||||
String password = config.get("password");
|
||||
|
||||
MongoClientOptions clientOptions = getClientOptions();
|
||||
if (user != null && password != null) {
|
||||
MongoCredential credential = MongoCredential.createMongoCRCredential(user, dbName, password.toCharArray());
|
||||
client = new MongoClient(new ServerAddress(host, port), Collections.singletonList(credential), clientOptions);
|
||||
} else {
|
||||
client = new MongoClient(new ServerAddress(host, port), clientOptions);
|
||||
}
|
||||
|
||||
this.db = client.getDB(dbName);
|
||||
|
||||
String databaseSchema = config.get("databaseSchema");
|
||||
@@ -108,8 +109,6 @@ public class DefaultMongoConnectionFactoryProvider implements MongoConnectionPro
|
||||
}
|
||||
|
||||
this.mongoStore = new MongoStoreImpl(db, getManagedEntities());
|
||||
|
||||
logger.debugv("Initialized mongo model. host: %s, port: %d, db: %s", host, port, dbName);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
@@ -138,6 +137,44 @@ public class DefaultMongoConnectionFactoryProvider implements MongoConnectionPro
|
||||
return "default";
|
||||
}
|
||||
|
||||
/**
|
||||
* Override this method if you want more possibility to configure Mongo client. It can be also used to inject mongo client
|
||||
* from different source.
|
||||
*
|
||||
* This method can assume that "config" is already set and can use it.
|
||||
*
|
||||
* @return mongoClient instance, which will be shared for whole Keycloak
|
||||
*
|
||||
* @throws UnknownHostException
|
||||
*/
|
||||
protected MongoClient createMongoClient() throws UnknownHostException {
|
||||
String host = config.get("host", ServerAddress.defaultHost());
|
||||
int port = config.getInt("port", ServerAddress.defaultPort());
|
||||
String dbName = config.get("db", "keycloak");
|
||||
|
||||
String user = config.get("user");
|
||||
String password = config.get("password");
|
||||
|
||||
MongoClientOptions clientOptions = getClientOptions();
|
||||
|
||||
MongoClient client;
|
||||
if (user != null && password != null) {
|
||||
MongoCredential credential = MongoCredential.createMongoCRCredential(user, dbName, password.toCharArray());
|
||||
client = new MongoClient(new ServerAddress(host, port), Collections.singletonList(credential), clientOptions);
|
||||
} else {
|
||||
client = new MongoClient(new ServerAddress(host, port), clientOptions);
|
||||
}
|
||||
|
||||
operationalInfo = new LinkedHashMap<>();
|
||||
operationalInfo.put("mongoServerAddress", client.getAddress().toString());
|
||||
operationalInfo.put("mongoDatabaseName", dbName);
|
||||
operationalInfo.put("mongoUser", user);
|
||||
operationalInfo.put("mongoDriverVersion", client.getVersion());
|
||||
|
||||
logger.debugv("Initialized mongo model. host: %s, port: %d, db: %s", host, port, dbName);
|
||||
return client;
|
||||
}
|
||||
|
||||
protected MongoClientOptions getClientOptions() {
|
||||
MongoClientOptions.Builder builder = MongoClientOptions.builder();
|
||||
checkIntOption("connectionsPerHost", builder);
|
||||
@@ -180,5 +217,10 @@ public class DefaultMongoConnectionFactoryProvider implements MongoConnectionPro
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String,String> getOperationalInfo() {
|
||||
return operationalInfo;
|
||||
}
|
||||
|
||||
}
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
package org.keycloak.connections.mongo;
|
||||
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
import org.keycloak.provider.ServerInfoAwareProviderFactory;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public interface MongoConnectionProviderFactory extends ProviderFactory<MongoConnectionProvider> {
|
||||
public interface MongoConnectionProviderFactory extends ServerInfoAwareProviderFactory<MongoConnectionProvider> {
|
||||
}
|
||||
@@ -9,6 +9,11 @@ import org.keycloak.provider.Spi;
|
||||
*/
|
||||
public class MongoConnectionSpi implements Spi {
|
||||
|
||||
@Override
|
||||
public boolean isInternal() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "connectionsMongo";
|
||||
|
||||
+79
-48
@@ -1,48 +1,79 @@
|
||||
package org.keycloak.connections.mongo.api;
|
||||
|
||||
import com.mongodb.DBObject;
|
||||
import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public interface MongoStore {
|
||||
|
||||
/**
|
||||
* Insert new entity
|
||||
*
|
||||
* @param entity to insert
|
||||
*/
|
||||
void insertEntity(MongoIdentifiableEntity entity, MongoStoreInvocationContext context);
|
||||
|
||||
/**
|
||||
* Update existing entity
|
||||
*
|
||||
* @param entity to update
|
||||
*/
|
||||
void updateEntity(MongoIdentifiableEntity entity, MongoStoreInvocationContext context);
|
||||
|
||||
|
||||
<T extends MongoIdentifiableEntity> T loadEntity(Class<T> type, String id, MongoStoreInvocationContext context);
|
||||
|
||||
<T extends MongoIdentifiableEntity> T loadSingleEntity(Class<T> type, DBObject query, MongoStoreInvocationContext context);
|
||||
|
||||
<T extends MongoIdentifiableEntity> List<T> loadEntities(Class<T> type, DBObject query, MongoStoreInvocationContext context);
|
||||
|
||||
<T extends MongoIdentifiableEntity> List<T> loadEntities(Class<T> type, DBObject query, DBObject sort, int firstResult, int maxResults, MongoStoreInvocationContext context);
|
||||
|
||||
<T extends MongoIdentifiableEntity> int countEntities(Class<T> type, DBObject query, MongoStoreInvocationContext context);
|
||||
|
||||
boolean removeEntity(MongoIdentifiableEntity entity, MongoStoreInvocationContext context);
|
||||
|
||||
boolean removeEntity(Class<? extends MongoIdentifiableEntity> type, String id, MongoStoreInvocationContext context);
|
||||
|
||||
boolean removeEntities(Class<? extends MongoIdentifiableEntity> type, DBObject query, MongoStoreInvocationContext context);
|
||||
|
||||
<S> boolean pushItemToList(MongoIdentifiableEntity entity, String listPropertyName, S itemToPush, boolean skipIfAlreadyPresent, MongoStoreInvocationContext context);
|
||||
|
||||
<S> boolean pullItemFromList(MongoIdentifiableEntity entity, String listPropertyName, S itemToPull, MongoStoreInvocationContext context);
|
||||
|
||||
}
|
||||
package org.keycloak.connections.mongo.api;
|
||||
|
||||
import com.mongodb.DBObject;
|
||||
import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public interface MongoStore {
|
||||
|
||||
/**
|
||||
* Insert new entity
|
||||
*
|
||||
* @param entity to insert
|
||||
*/
|
||||
void insertEntity(MongoIdentifiableEntity entity, MongoStoreInvocationContext context);
|
||||
|
||||
/**
|
||||
* Update existing entity
|
||||
*
|
||||
* @param entity to update
|
||||
*/
|
||||
void updateEntity(MongoIdentifiableEntity entity, MongoStoreInvocationContext context);
|
||||
|
||||
/**
|
||||
* Bulk update of more entities of some type
|
||||
*
|
||||
* @param type
|
||||
* @param query
|
||||
* @param update
|
||||
* @param context
|
||||
* @return count of updated entities
|
||||
*/
|
||||
<T extends MongoIdentifiableEntity> int updateEntities(Class<T> type, DBObject query, DBObject update, MongoStoreInvocationContext context);
|
||||
|
||||
<T extends MongoIdentifiableEntity> T loadEntity(Class<T> type, String id, MongoStoreInvocationContext context);
|
||||
|
||||
<T extends MongoIdentifiableEntity> T loadSingleEntity(Class<T> type, DBObject query, MongoStoreInvocationContext context);
|
||||
|
||||
/**
|
||||
* @param type
|
||||
* @param query
|
||||
* @param context
|
||||
* @return query result or empty list if no results available for the query. Doesn't return null
|
||||
*/
|
||||
<T extends MongoIdentifiableEntity> List<T> loadEntities(Class<T> type, DBObject query, MongoStoreInvocationContext context);
|
||||
|
||||
/**
|
||||
* @param type
|
||||
* @param query
|
||||
* @param context
|
||||
* @return query result or empty list if no results available for the query. Doesn't return null
|
||||
*/
|
||||
<T extends MongoIdentifiableEntity> List<T> loadEntities(Class<T> type, DBObject query, DBObject sort, int firstResult, int maxResults, MongoStoreInvocationContext context);
|
||||
|
||||
<T extends MongoIdentifiableEntity> int countEntities(Class<T> type, DBObject query, MongoStoreInvocationContext context);
|
||||
|
||||
boolean removeEntity(MongoIdentifiableEntity entity, MongoStoreInvocationContext context);
|
||||
|
||||
boolean removeEntity(Class<? extends MongoIdentifiableEntity> type, String id, MongoStoreInvocationContext context);
|
||||
|
||||
/**
|
||||
*
|
||||
* @param type
|
||||
* @param query
|
||||
* @param callback if true, then store will first load all entities, then call "afterRemove" for every entity. If false, the entities are removed directly without load and calling "afterRemove" callback
|
||||
* false has better performance (especially if we are going to remove big number of entities)
|
||||
* @param context
|
||||
* @return count of removed entities
|
||||
*/
|
||||
int removeEntities(Class<? extends MongoIdentifiableEntity> type, DBObject query, boolean callback, MongoStoreInvocationContext context);
|
||||
|
||||
<S> boolean pushItemToList(MongoIdentifiableEntity entity, String listPropertyName, S itemToPush, boolean skipIfAlreadyPresent, MongoStoreInvocationContext context);
|
||||
|
||||
<S> boolean pullItemFromList(MongoIdentifiableEntity entity, String listPropertyName, S itemToPull, MongoStoreInvocationContext context);
|
||||
|
||||
}
|
||||
+2
@@ -22,6 +22,8 @@ public interface MongoStoreInvocationContext {
|
||||
|
||||
void beforeDBSearch(Class<? extends MongoIdentifiableEntity> entityType);
|
||||
|
||||
void beforeDBBulkUpdateOrRemove(Class<? extends MongoIdentifiableEntity> entityType);
|
||||
|
||||
void begin();
|
||||
|
||||
void commit();
|
||||
|
||||
+4
-3
@@ -1,5 +1,6 @@
|
||||
package org.keycloak.connections.mongo.api.types;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -14,9 +15,9 @@ public class MapperContext<T, S> {
|
||||
private final Class<? extends S> expectedReturnType;
|
||||
|
||||
// in case that expected return type is generic type (like "List<String>"), then genericTypes could contain list of expected generic arguments
|
||||
private final List<Class<?>> genericTypes;
|
||||
private final List<Type> genericTypes;
|
||||
|
||||
public MapperContext(T objectToConvert, Class<? extends S> expectedReturnType, List<Class<?>> genericTypes) {
|
||||
public MapperContext(T objectToConvert, Class<? extends S> expectedReturnType, List<Type> genericTypes) {
|
||||
this.objectToConvert = objectToConvert;
|
||||
this.expectedReturnType = expectedReturnType;
|
||||
this.genericTypes = genericTypes;
|
||||
@@ -30,7 +31,7 @@ public class MapperContext<T, S> {
|
||||
return expectedReturnType;
|
||||
}
|
||||
|
||||
public List<Class<?>> getGenericTypes() {
|
||||
public List<Type> getGenericTypes() {
|
||||
return genericTypes;
|
||||
}
|
||||
}
|
||||
+119
-111
@@ -1,111 +1,119 @@
|
||||
package org.keycloak.connections.mongo.api.types;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Registry of mappers, which allow to convert application object to database objects. MapperRegistry is main entry point to be used by application.
|
||||
* Application can create instance of MapperRegistry and then register required Mapper objects.
|
||||
*
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class MapperRegistry {
|
||||
|
||||
// TODO: Thread-safety support (maybe...)
|
||||
// Mappers of Application objects to DB objects
|
||||
private Map<Class<?>, Mapper<?, ?>> appObjectMappers = new HashMap<Class<?>, Mapper<?, ?>>();
|
||||
|
||||
// Mappers of DB objects to Application objects
|
||||
private Map<Class<?>, Map<Class<?>, Mapper<?, ?>>> dbObjectMappers = new HashMap<Class<?>, Map<Class<?>, Mapper<?,?>>>();
|
||||
|
||||
|
||||
/**
|
||||
* Add mapper for converting application objects to DB objects
|
||||
*
|
||||
* @param mapper
|
||||
*/
|
||||
public void addAppObjectMapper(Mapper<?, ?> mapper) {
|
||||
appObjectMappers.put(mapper.getTypeOfObjectToConvert(), mapper);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add mapper for converting DB objects to application objects
|
||||
*
|
||||
* @param mapper
|
||||
*/
|
||||
public void addDBObjectMapper(Mapper<?, ?> mapper) {
|
||||
Class<?> dbObjectType = mapper.getTypeOfObjectToConvert();
|
||||
Class<?> appObjectType = mapper.getExpectedReturnType();
|
||||
Map<Class<?>, Mapper<?, ?>> appObjects = dbObjectMappers.get(dbObjectType);
|
||||
if (appObjects == null) {
|
||||
appObjects = new HashMap<Class<?>, Mapper<?, ?>>();
|
||||
dbObjectMappers.put(dbObjectType, appObjects);
|
||||
}
|
||||
appObjects.put(appObjectType, mapper);
|
||||
}
|
||||
|
||||
|
||||
public <S> S convertDBObjectToApplicationObject(MapperContext<Object, S> context) {
|
||||
Object dbObject = context.getObjectToConvert();
|
||||
Class<?> expectedApplicationObjectType = context.getExpectedReturnType();
|
||||
|
||||
Class<?> dbObjectType = dbObject.getClass();
|
||||
Mapper<Object, S> mapper;
|
||||
|
||||
Map<Class<?>, Mapper<?, ?>> appObjects = dbObjectMappers.get(dbObjectType);
|
||||
if (appObjects == null) {
|
||||
throw new IllegalArgumentException("Not found any mappers for type " + dbObjectType);
|
||||
} else {
|
||||
if (appObjects.size() == 1) {
|
||||
mapper = (Mapper<Object, S>)appObjects.values().iterator().next();
|
||||
} else {
|
||||
// Try to find converter for requested application type
|
||||
mapper = (Mapper<Object, S>)getAppConverterForType(context.getExpectedReturnType(), appObjects);
|
||||
}
|
||||
}
|
||||
|
||||
if (mapper == null) {
|
||||
throw new IllegalArgumentException("Can't found mapper for type " + dbObjectType + " and expectedApplicationType " + expectedApplicationObjectType);
|
||||
}
|
||||
|
||||
return mapper.convertObject(context);
|
||||
}
|
||||
|
||||
|
||||
public <S> S convertApplicationObjectToDBObject(Object applicationObject, Class<S> expectedDBObjectType) {
|
||||
Class<?> appObjectType = applicationObject.getClass();
|
||||
Mapper<Object, S> mapper = (Mapper<Object, S>)getAppConverterForType(appObjectType, appObjectMappers);
|
||||
if (mapper == null) {
|
||||
throw new IllegalArgumentException("Can't found converter for type " + appObjectType + " in registered appObjectMappers");
|
||||
}
|
||||
if (!expectedDBObjectType.isAssignableFrom(mapper.getExpectedReturnType())) {
|
||||
throw new IllegalArgumentException("Converter " + mapper + " has return type " + mapper.getExpectedReturnType() +
|
||||
" but we need type " + expectedDBObjectType);
|
||||
}
|
||||
return mapper.convertObject(new MapperContext<Object, S>(applicationObject, expectedDBObjectType, null));
|
||||
}
|
||||
|
||||
// Try to find converter for given type or all it's supertypes
|
||||
private static Mapper<Object, ?> getAppConverterForType(Class<?> appObjectType, Map<Class<?>, Mapper<?, ?>> appObjectConverters) {
|
||||
Mapper<Object, ?> mapper = (Mapper<Object, ?>)appObjectConverters.get(appObjectType);
|
||||
if (mapper != null) {
|
||||
return mapper;
|
||||
} else {
|
||||
Class<?>[] interfaces = appObjectType.getInterfaces();
|
||||
for (Class<?> interface1 : interfaces) {
|
||||
mapper = getAppConverterForType(interface1, appObjectConverters);
|
||||
if (mapper != null) {
|
||||
return mapper;
|
||||
}
|
||||
}
|
||||
|
||||
Class<?> superType = appObjectType.getSuperclass();
|
||||
if (superType != null) {
|
||||
return getAppConverterForType(superType, appObjectConverters);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
package org.keycloak.connections.mongo.api.types;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Registry of mappers, which allow to convert application object to database objects. MapperRegistry is main entry point to be used by application.
|
||||
* Application can create instance of MapperRegistry and then register required Mapper objects.
|
||||
*
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class MapperRegistry {
|
||||
|
||||
// TODO: Thread-safety support (maybe...)
|
||||
// Mappers of Application objects to DB objects
|
||||
private Map<Class<?>, Mapper<?, ?>> appObjectMappers = new HashMap<Class<?>, Mapper<?, ?>>();
|
||||
|
||||
// Mappers of DB objects to Application objects
|
||||
private Map<Class<?>, Map<Class<?>, Mapper<?, ?>>> dbObjectMappers = new HashMap<Class<?>, Map<Class<?>, Mapper<?,?>>>();
|
||||
|
||||
|
||||
/**
|
||||
* Add mapper for converting application objects to DB objects
|
||||
*
|
||||
* @param mapper
|
||||
*/
|
||||
public void addAppObjectMapper(Mapper<?, ?> mapper) {
|
||||
appObjectMappers.put(mapper.getTypeOfObjectToConvert(), mapper);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add mapper for converting DB objects to application objects
|
||||
*
|
||||
* @param mapper
|
||||
*/
|
||||
public void addDBObjectMapper(Mapper<?, ?> mapper) {
|
||||
Class<?> dbObjectType = mapper.getTypeOfObjectToConvert();
|
||||
Class<?> appObjectType = mapper.getExpectedReturnType();
|
||||
Map<Class<?>, Mapper<?, ?>> appObjects = dbObjectMappers.get(dbObjectType);
|
||||
if (appObjects == null) {
|
||||
appObjects = new HashMap<Class<?>, Mapper<?, ?>>();
|
||||
dbObjectMappers.put(dbObjectType, appObjects);
|
||||
}
|
||||
appObjects.put(appObjectType, mapper);
|
||||
}
|
||||
|
||||
|
||||
public <S> S convertDBObjectToApplicationObject(MapperContext<Object, S> context) {
|
||||
if (context.getObjectToConvert() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Object dbObject = context.getObjectToConvert();
|
||||
Class<?> expectedApplicationObjectType = context.getExpectedReturnType();
|
||||
|
||||
Class<?> dbObjectType = dbObject.getClass();
|
||||
Mapper<Object, S> mapper;
|
||||
|
||||
Map<Class<?>, Mapper<?, ?>> appObjects = dbObjectMappers.get(dbObjectType);
|
||||
if (appObjects == null) {
|
||||
throw new IllegalArgumentException("Not found any mappers for type " + dbObjectType);
|
||||
} else {
|
||||
if (appObjects.size() == 1) {
|
||||
mapper = (Mapper<Object, S>)appObjects.values().iterator().next();
|
||||
} else {
|
||||
// Try to find converter for requested application type
|
||||
mapper = (Mapper<Object, S>)getAppConverterForType(context.getExpectedReturnType(), appObjects);
|
||||
}
|
||||
}
|
||||
|
||||
if (mapper == null) {
|
||||
throw new IllegalArgumentException("Can't found mapper for type " + dbObjectType + " and expectedApplicationType " + expectedApplicationObjectType);
|
||||
}
|
||||
|
||||
return mapper.convertObject(context);
|
||||
}
|
||||
|
||||
|
||||
public <S> S convertApplicationObjectToDBObject(Object applicationObject, Class<S> expectedDBObjectType) {
|
||||
if (applicationObject == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Class<?> appObjectType = applicationObject.getClass();
|
||||
Mapper<Object, S> mapper = (Mapper<Object, S>)getAppConverterForType(appObjectType, appObjectMappers);
|
||||
if (mapper == null) {
|
||||
throw new IllegalArgumentException("Can't found converter for type " + appObjectType + " in registered appObjectMappers");
|
||||
}
|
||||
if (!expectedDBObjectType.isAssignableFrom(mapper.getExpectedReturnType())) {
|
||||
throw new IllegalArgumentException("Converter " + mapper + " has return type " + mapper.getExpectedReturnType() +
|
||||
" but we need type " + expectedDBObjectType);
|
||||
}
|
||||
return mapper.convertObject(new MapperContext<Object, S>(applicationObject, expectedDBObjectType, null));
|
||||
}
|
||||
|
||||
// Try to find converter for given type or all it's supertypes
|
||||
private static Mapper<Object, ?> getAppConverterForType(Class<?> appObjectType, Map<Class<?>, Mapper<?, ?>> appObjectConverters) {
|
||||
Mapper<Object, ?> mapper = (Mapper<Object, ?>)appObjectConverters.get(appObjectType);
|
||||
if (mapper != null) {
|
||||
return mapper;
|
||||
} else {
|
||||
Class<?>[] interfaces = appObjectType.getInterfaces();
|
||||
for (Class<?> interface1 : interfaces) {
|
||||
mapper = getAppConverterForType(interface1, appObjectConverters);
|
||||
if (mapper != null) {
|
||||
return mapper;
|
||||
}
|
||||
}
|
||||
|
||||
Class<?> superType = appObjectType.getSuperclass();
|
||||
if (superType != null) {
|
||||
return getAppConverterForType(superType, appObjectConverters);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+40
-40
@@ -1,40 +1,40 @@
|
||||
package org.keycloak.connections.mongo.impl;
|
||||
|
||||
import org.keycloak.models.utils.reflection.Property;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class EntityInfo {
|
||||
|
||||
private final Class<?> entityClass;
|
||||
|
||||
private final String dbCollectionName;
|
||||
|
||||
private final Map<String, Property<Object>> properties;
|
||||
|
||||
public EntityInfo(Class<?> entityClass, String dbCollectionName, Map<String, Property<Object>> properties) {
|
||||
this.entityClass = entityClass;
|
||||
this.dbCollectionName = dbCollectionName;
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
public Class<?> getEntityClass() {
|
||||
return entityClass;
|
||||
}
|
||||
|
||||
public String getDbCollectionName() {
|
||||
return dbCollectionName;
|
||||
}
|
||||
|
||||
public Collection<Property<Object>> getProperties() {
|
||||
return properties.values();
|
||||
}
|
||||
|
||||
public Property<Object> getPropertyByName(String propertyName) {
|
||||
return properties.get(propertyName);
|
||||
}
|
||||
}
|
||||
package org.keycloak.connections.mongo.impl;
|
||||
|
||||
import org.keycloak.models.utils.reflection.Property;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class EntityInfo {
|
||||
|
||||
private final Class<?> entityClass;
|
||||
|
||||
private final String dbCollectionName;
|
||||
|
||||
private final Map<String, Property<Object>> properties;
|
||||
|
||||
public EntityInfo(Class<?> entityClass, String dbCollectionName, Map<String, Property<Object>> properties) {
|
||||
this.entityClass = entityClass;
|
||||
this.dbCollectionName = dbCollectionName;
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
public Class<?> getEntityClass() {
|
||||
return entityClass;
|
||||
}
|
||||
|
||||
public String getDbCollectionName() {
|
||||
return dbCollectionName;
|
||||
}
|
||||
|
||||
public Collection<Property<Object>> getProperties() {
|
||||
return properties.values();
|
||||
}
|
||||
|
||||
public Property<Object> getPropertyByName(String propertyName) {
|
||||
return properties.get(propertyName);
|
||||
}
|
||||
}
|
||||
+473
-449
@@ -1,449 +1,473 @@
|
||||
package org.keycloak.connections.mongo.impl;
|
||||
|
||||
import com.mongodb.BasicDBList;
|
||||
import com.mongodb.BasicDBObject;
|
||||
import com.mongodb.DB;
|
||||
import com.mongodb.DBCollection;
|
||||
import com.mongodb.DBCursor;
|
||||
import com.mongodb.DBObject;
|
||||
import com.mongodb.MongoException;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.connections.mongo.api.MongoCollection;
|
||||
import org.keycloak.connections.mongo.api.MongoEntity;
|
||||
import org.keycloak.connections.mongo.api.MongoIdentifiableEntity;
|
||||
import org.keycloak.connections.mongo.api.MongoStore;
|
||||
import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
|
||||
import org.keycloak.connections.mongo.api.context.MongoTask;
|
||||
import org.keycloak.connections.mongo.api.types.Mapper;
|
||||
import org.keycloak.connections.mongo.api.types.MapperContext;
|
||||
import org.keycloak.connections.mongo.api.types.MapperRegistry;
|
||||
import org.keycloak.connections.mongo.impl.types.BasicDBListMapper;
|
||||
import org.keycloak.connections.mongo.impl.types.BasicDBListToSetMapper;
|
||||
import org.keycloak.connections.mongo.impl.types.BasicDBObjectMapper;
|
||||
import org.keycloak.connections.mongo.impl.types.BasicDBObjectToMapMapper;
|
||||
import org.keycloak.connections.mongo.impl.types.EnumToStringMapper;
|
||||
import org.keycloak.connections.mongo.impl.types.ListMapper;
|
||||
import org.keycloak.connections.mongo.impl.types.MapMapper;
|
||||
import org.keycloak.connections.mongo.impl.types.MongoEntityMapper;
|
||||
import org.keycloak.connections.mongo.impl.types.SimpleMapper;
|
||||
import org.keycloak.connections.mongo.impl.types.StringToEnumMapper;
|
||||
import org.keycloak.models.ModelDuplicateException;
|
||||
import org.keycloak.models.ModelException;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.models.utils.reflection.Property;
|
||||
import org.keycloak.models.utils.reflection.PropertyQueries;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class MongoStoreImpl implements MongoStore {
|
||||
|
||||
private static final Class<?>[] SIMPLE_TYPES = { String.class, Integer.class, Boolean.class, Long.class, Double.class, Character.class, Date.class, byte[].class };
|
||||
|
||||
private final DB database;
|
||||
private static final Logger logger = Logger.getLogger(MongoStoreImpl.class);
|
||||
|
||||
private final MapperRegistry mapperRegistry;
|
||||
private ConcurrentMap<Class<?>, EntityInfo> entityInfoCache =
|
||||
new ConcurrentHashMap<Class<?>, EntityInfo>();
|
||||
|
||||
|
||||
public MongoStoreImpl(DB database, Class<?>[] managedEntityTypes) {
|
||||
this.database = database;
|
||||
|
||||
mapperRegistry = new MapperRegistry();
|
||||
|
||||
for (Class<?> simpleMapperClass : SIMPLE_TYPES) {
|
||||
SimpleMapper mapper = new SimpleMapper(simpleMapperClass);
|
||||
mapperRegistry.addAppObjectMapper(mapper);
|
||||
mapperRegistry.addDBObjectMapper(mapper);
|
||||
}
|
||||
|
||||
// Specific converter for ArrayList is added just for performance purposes to avoid recursive converter lookup (most of list idm will be ArrayList)
|
||||
mapperRegistry.addAppObjectMapper(new ListMapper(mapperRegistry, ArrayList.class));
|
||||
mapperRegistry.addAppObjectMapper(new ListMapper(mapperRegistry, List.class));
|
||||
mapperRegistry.addDBObjectMapper(new BasicDBListMapper(mapperRegistry));
|
||||
|
||||
mapperRegistry.addAppObjectMapper(new ListMapper(mapperRegistry, HashSet.class));
|
||||
mapperRegistry.addAppObjectMapper(new ListMapper(mapperRegistry, Set.class));
|
||||
mapperRegistry.addDBObjectMapper(new BasicDBListToSetMapper(mapperRegistry));
|
||||
|
||||
mapperRegistry.addAppObjectMapper(new MapMapper(HashMap.class));
|
||||
mapperRegistry.addAppObjectMapper(new MapMapper(Map.class));
|
||||
mapperRegistry.addDBObjectMapper(new BasicDBObjectToMapMapper());
|
||||
|
||||
// Enum converters
|
||||
mapperRegistry.addAppObjectMapper(new EnumToStringMapper());
|
||||
mapperRegistry.addDBObjectMapper(new StringToEnumMapper());
|
||||
|
||||
for (Class<?> type : managedEntityTypes) {
|
||||
getEntityInfo(type);
|
||||
mapperRegistry.addAppObjectMapper(new MongoEntityMapper(this, mapperRegistry, type));
|
||||
mapperRegistry.addDBObjectMapper(new BasicDBObjectMapper(this, mapperRegistry, type));
|
||||
}
|
||||
}
|
||||
|
||||
protected void dropDatabase() {
|
||||
this.database.dropDatabase();
|
||||
logger.info("Database " + this.database.getName() + " dropped in MongoDB");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void insertEntity(MongoIdentifiableEntity entity, MongoStoreInvocationContext context) {
|
||||
Class<? extends MongoEntity> clazz = entity.getClass();
|
||||
|
||||
// Find annotations for ID, for all the properties and for the name of the collection.
|
||||
EntityInfo entityInfo = getEntityInfo(clazz);
|
||||
|
||||
// Create instance of BasicDBObject and add all declared properties to it (properties with null value probably should be skipped)
|
||||
BasicDBObject dbObject = mapperRegistry.convertApplicationObjectToDBObject(entity, BasicDBObject.class);
|
||||
|
||||
DBCollection dbCollection = database.getCollection(entityInfo.getDbCollectionName());
|
||||
|
||||
String currentId = entity.getId();
|
||||
|
||||
// Generate random ID if not set already
|
||||
if (currentId == null) {
|
||||
currentId = KeycloakModelUtils.generateId();
|
||||
entity.setId(currentId);
|
||||
}
|
||||
|
||||
// Adding "_id"
|
||||
dbObject.put("_id", currentId);
|
||||
|
||||
try {
|
||||
dbCollection.insert(dbObject);
|
||||
} catch (MongoException e) {
|
||||
throw convertException(e);
|
||||
}
|
||||
|
||||
// Treat object as created in this transaction (It is already submited to transaction)
|
||||
context.addCreatedEntity(entity);
|
||||
}
|
||||
|
||||
public static ModelException convertException(MongoException e) {
|
||||
if (e instanceof MongoException.DuplicateKey) {
|
||||
return new ModelDuplicateException(e);
|
||||
} else {
|
||||
return new ModelException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateEntity(final MongoIdentifiableEntity entity, MongoStoreInvocationContext context) {
|
||||
MongoTask fullUpdateTask = new MongoTask() {
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
Class<? extends MongoEntity> clazz = entity.getClass();
|
||||
EntityInfo entityInfo = getEntityInfo(clazz);
|
||||
BasicDBObject dbObject = mapperRegistry.convertApplicationObjectToDBObject(entity, BasicDBObject.class);
|
||||
DBCollection dbCollection = database.getCollection(entityInfo.getDbCollectionName());
|
||||
|
||||
String currentId = entity.getId();
|
||||
|
||||
if (currentId == null) {
|
||||
throw new IllegalStateException("Can't update entity without id: " + entity);
|
||||
} else {
|
||||
BasicDBObject query = new BasicDBObject("_id", currentId);
|
||||
dbCollection.update(query, dbObject);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFullUpdate() {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
// update is just added to context and postponed
|
||||
context.addUpdateTask(entity, fullUpdateTask);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public <T extends MongoIdentifiableEntity> T loadEntity(Class<T> type, String id, MongoStoreInvocationContext context) {
|
||||
// First look if we already read the object with this oid and type during this transaction. If yes, use it instead of DB lookup
|
||||
T cached = context.getLoadedEntity(type, id);
|
||||
if (cached != null && type.isAssignableFrom(cached.getClass())) return cached;
|
||||
|
||||
DBCollection dbCollection = getDBCollectionForType(type);
|
||||
|
||||
BasicDBObject idQuery = new BasicDBObject("_id", id);
|
||||
DBObject dbObject = dbCollection.findOne(idQuery);
|
||||
|
||||
if (dbObject == null) return null;
|
||||
|
||||
MapperContext<Object, T> mapperContext = new MapperContext<Object, T>(dbObject, type, null);
|
||||
T converted = mapperRegistry.convertDBObjectToApplicationObject(mapperContext);
|
||||
|
||||
// Now add it to loaded objects
|
||||
context.addLoadedEntity(converted);
|
||||
|
||||
return converted;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public <T extends MongoIdentifiableEntity> T loadSingleEntity(Class<T> type, DBObject query, MongoStoreInvocationContext context) {
|
||||
// First we should execute all pending tasks before searching DB
|
||||
context.beforeDBSearch(type);
|
||||
|
||||
DBCollection dbCollection = getDBCollectionForType(type);
|
||||
DBObject dbObject = dbCollection.findOne(query);
|
||||
|
||||
if (dbObject == null) {
|
||||
return null;
|
||||
} else {
|
||||
return convertDBObjectToEntity(type, dbObject, context);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public <T extends MongoIdentifiableEntity> List<T> loadEntities(Class<T> type, DBObject query, MongoStoreInvocationContext context) {
|
||||
// First we should execute all pending tasks before searching DB
|
||||
context.beforeDBSearch(type);
|
||||
|
||||
DBCollection dbCollection = getDBCollectionForType(type);
|
||||
DBCursor cursor = dbCollection.find(query);
|
||||
|
||||
return convertCursor(type, cursor, context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends MongoIdentifiableEntity> List<T> loadEntities(Class<T> type, DBObject query, DBObject sort, int firstResult, int maxResults, MongoStoreInvocationContext context) {
|
||||
// First we should execute all pending tasks before searching DB
|
||||
context.beforeDBSearch(type);
|
||||
|
||||
DBCollection dbCollection = getDBCollectionForType(type);
|
||||
DBCursor cursor = dbCollection.find(query);
|
||||
if (firstResult != -1) {
|
||||
cursor.skip(firstResult);
|
||||
}
|
||||
if (maxResults != -1) {
|
||||
cursor.limit(maxResults);
|
||||
}
|
||||
if (sort != null) {
|
||||
cursor.sort(sort);
|
||||
}
|
||||
|
||||
return convertCursor(type, cursor, context);
|
||||
}
|
||||
|
||||
public <T extends MongoIdentifiableEntity> int countEntities(Class<T> type, DBObject query, MongoStoreInvocationContext context) {
|
||||
context.beforeDBSearch(type);
|
||||
|
||||
DBCollection dbCollection = getDBCollectionForType(type);
|
||||
Long count = dbCollection.count(query);
|
||||
|
||||
// For now, assume that int is sufficient
|
||||
return count.intValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean removeEntity(MongoIdentifiableEntity entity, MongoStoreInvocationContext context) {
|
||||
return removeEntity(entity.getClass(), entity.getId(), context);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean removeEntity(Class<? extends MongoIdentifiableEntity> type, String id, MongoStoreInvocationContext context) {
|
||||
MongoIdentifiableEntity found = loadEntity(type, id, context);
|
||||
if (found == null) {
|
||||
return false;
|
||||
} else {
|
||||
DBCollection dbCollection = getDBCollectionForType(type);
|
||||
BasicDBObject dbQuery = new BasicDBObject("_id", id);
|
||||
dbCollection.remove(dbQuery);
|
||||
//logger.debugf("Entity of type: %s , id: %s removed from MongoDB.", type, id);
|
||||
|
||||
context.addRemovedEntity(found);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean removeEntities(Class<? extends MongoIdentifiableEntity> type, DBObject query, MongoStoreInvocationContext context) {
|
||||
List<? extends MongoIdentifiableEntity> foundObjects = loadEntities(type, query, context);
|
||||
if (foundObjects.size() == 0) {
|
||||
return false;
|
||||
} else {
|
||||
DBCollection dbCollection = getDBCollectionForType(type);
|
||||
dbCollection.remove(query);
|
||||
//logger.debug("Removed %d" + foundObjects.size() + " entities of type: " + type + ", query: " + query);
|
||||
|
||||
for (MongoIdentifiableEntity found : foundObjects) {
|
||||
context.addRemovedEntity(found);;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S> boolean pushItemToList(final MongoIdentifiableEntity entity, final String listPropertyName, S itemToPush, boolean skipIfAlreadyPresent, MongoStoreInvocationContext context) {
|
||||
final Class<? extends MongoEntity> type = entity.getClass();
|
||||
EntityInfo entityInfo = getEntityInfo(type);
|
||||
|
||||
// Add item to list directly in this object
|
||||
Property<Object> listProperty = entityInfo.getPropertyByName(listPropertyName);
|
||||
if (listProperty == null) {
|
||||
throw new IllegalArgumentException("Property " + listPropertyName + " doesn't exist on object " + entity);
|
||||
}
|
||||
|
||||
List<S> list = (List<S>)listProperty.getValue(entity);
|
||||
if (list == null) {
|
||||
list = new ArrayList<S>();
|
||||
listProperty.setValue(entity, list);
|
||||
}
|
||||
|
||||
// Skip if item is already in list
|
||||
if (skipIfAlreadyPresent && list.contains(itemToPush)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Update java object
|
||||
list.add(itemToPush);
|
||||
|
||||
// Add update of list to pending tasks
|
||||
final List<S> listt = list;
|
||||
context.addUpdateTask(entity, new MongoTask() {
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
// Now DB update of new list with usage of $set
|
||||
BasicDBList dbList = mapperRegistry.convertApplicationObjectToDBObject(listt, BasicDBList.class);
|
||||
|
||||
BasicDBObject query = new BasicDBObject("_id", entity.getId());
|
||||
BasicDBObject listObject = new BasicDBObject(listPropertyName, dbList);
|
||||
BasicDBObject setCommand = new BasicDBObject("$set", listObject);
|
||||
getDBCollectionForType(type).update(query, setCommand);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFullUpdate() {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public <S> boolean pullItemFromList(final MongoIdentifiableEntity entity, final String listPropertyName, final S itemToPull, MongoStoreInvocationContext context) {
|
||||
final Class<? extends MongoEntity> type = entity.getClass();
|
||||
EntityInfo entityInfo = getEntityInfo(type);
|
||||
|
||||
// Remove item from list directly in this object
|
||||
Property<Object> listProperty = entityInfo.getPropertyByName(listPropertyName);
|
||||
if (listProperty == null) {
|
||||
throw new IllegalArgumentException("Property " + listPropertyName + " doesn't exist on object " + entity);
|
||||
}
|
||||
List<S> list = (List<S>)listProperty.getValue(entity);
|
||||
|
||||
// If list is null, we skip both object and DB update
|
||||
if (list == null || !list.contains(itemToPull)) {
|
||||
return false;
|
||||
} else {
|
||||
|
||||
// Update java object
|
||||
list.remove(itemToPull);
|
||||
|
||||
// Add update of list to pending tasks
|
||||
context.addUpdateTask(entity, new MongoTask() {
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
// Pull item from DB
|
||||
Object dbItemToPull = mapperRegistry.convertApplicationObjectToDBObject(itemToPull, Object.class);
|
||||
BasicDBObject query = new BasicDBObject("_id", entity.getId());
|
||||
BasicDBObject pullObject = new BasicDBObject(listPropertyName, dbItemToPull);
|
||||
BasicDBObject pullCommand = new BasicDBObject("$pull", pullObject);
|
||||
getDBCollectionForType(type).update(query, pullCommand);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFullUpdate() {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Possibility to add user-defined mappers
|
||||
public void addAppObjectConverter(Mapper<?, ?> mapper) {
|
||||
mapperRegistry.addAppObjectMapper(mapper);
|
||||
}
|
||||
|
||||
public void addDBObjectConverter(Mapper<?, ?> mapper) {
|
||||
mapperRegistry.addDBObjectMapper(mapper);
|
||||
}
|
||||
|
||||
public EntityInfo getEntityInfo(Class<?> entityClass) {
|
||||
EntityInfo entityInfo = entityInfoCache.get(entityClass);
|
||||
if (entityInfo == null) {
|
||||
Map<String, Property<Object>> properties = PropertyQueries.createQuery(entityClass).getWritableResultList();
|
||||
|
||||
MongoCollection classAnnotation = entityClass.getAnnotation(MongoCollection.class);
|
||||
|
||||
String dbCollectionName = classAnnotation==null ? null : classAnnotation.collectionName();
|
||||
entityInfo = new EntityInfo(entityClass, dbCollectionName, properties);
|
||||
|
||||
EntityInfo existing = entityInfoCache.putIfAbsent(entityClass, entityInfo);
|
||||
if (existing != null) {
|
||||
entityInfo = existing;
|
||||
}
|
||||
}
|
||||
|
||||
return entityInfo;
|
||||
}
|
||||
|
||||
protected <T extends MongoIdentifiableEntity> List<T> convertCursor(Class<T> type, DBCursor cursor, MongoStoreInvocationContext context) {
|
||||
List<T> result = new ArrayList<T>();
|
||||
|
||||
try {
|
||||
for (DBObject dbObject : cursor) {
|
||||
T entity = convertDBObjectToEntity(type, dbObject, context);
|
||||
result.add(entity);
|
||||
}
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected <T extends MongoIdentifiableEntity> T convertDBObjectToEntity(Class<T> type, DBObject dbObject, MongoStoreInvocationContext context) {
|
||||
// First look if we already have loaded object cached. If yes, we will use cached instance
|
||||
String id = dbObject.get("_id").toString();
|
||||
T object = context.getLoadedEntity(type, id);
|
||||
|
||||
if (object == null) {
|
||||
// So convert and use fresh instance from DB
|
||||
MapperContext<Object, T> mapperContext = new MapperContext<Object, T>(dbObject, type, null);
|
||||
object = mapperRegistry.convertDBObjectToApplicationObject(mapperContext);
|
||||
context.addLoadedEntity(object);
|
||||
}
|
||||
return object;
|
||||
}
|
||||
|
||||
protected DBCollection getDBCollectionForType(Class<?> type) {
|
||||
EntityInfo entityInfo = getEntityInfo(type);
|
||||
String dbCollectionName = entityInfo.getDbCollectionName();
|
||||
return dbCollectionName==null ? null : database.getCollection(dbCollectionName);
|
||||
}
|
||||
}
|
||||
package org.keycloak.connections.mongo.impl;
|
||||
|
||||
import com.mongodb.BasicDBList;
|
||||
import com.mongodb.BasicDBObject;
|
||||
import com.mongodb.DB;
|
||||
import com.mongodb.DBCollection;
|
||||
import com.mongodb.DBCursor;
|
||||
import com.mongodb.DBObject;
|
||||
import com.mongodb.MongoException;
|
||||
import com.mongodb.WriteResult;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.connections.mongo.api.MongoCollection;
|
||||
import org.keycloak.connections.mongo.api.MongoEntity;
|
||||
import org.keycloak.connections.mongo.api.MongoIdentifiableEntity;
|
||||
import org.keycloak.connections.mongo.api.MongoStore;
|
||||
import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
|
||||
import org.keycloak.connections.mongo.api.context.MongoTask;
|
||||
import org.keycloak.connections.mongo.api.types.Mapper;
|
||||
import org.keycloak.connections.mongo.api.types.MapperContext;
|
||||
import org.keycloak.connections.mongo.api.types.MapperRegistry;
|
||||
import org.keycloak.connections.mongo.impl.types.BasicDBListMapper;
|
||||
import org.keycloak.connections.mongo.impl.types.BasicDBListToSetMapper;
|
||||
import org.keycloak.connections.mongo.impl.types.BasicDBObjectMapper;
|
||||
import org.keycloak.connections.mongo.impl.types.BasicDBObjectToMapMapper;
|
||||
import org.keycloak.connections.mongo.impl.types.EnumToStringMapper;
|
||||
import org.keycloak.connections.mongo.impl.types.ListMapper;
|
||||
import org.keycloak.connections.mongo.impl.types.MapMapper;
|
||||
import org.keycloak.connections.mongo.impl.types.MongoEntityMapper;
|
||||
import org.keycloak.connections.mongo.impl.types.SimpleMapper;
|
||||
import org.keycloak.connections.mongo.impl.types.StringToEnumMapper;
|
||||
import org.keycloak.models.ModelDuplicateException;
|
||||
import org.keycloak.models.ModelException;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.models.utils.reflection.Property;
|
||||
import org.keycloak.models.utils.reflection.PropertyQueries;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class MongoStoreImpl implements MongoStore {
|
||||
|
||||
private static final Class<?>[] SIMPLE_TYPES = { String.class, Integer.class, Boolean.class, Long.class, Double.class, Character.class, Date.class, byte[].class };
|
||||
|
||||
private final DB database;
|
||||
private static final Logger logger = Logger.getLogger(MongoStoreImpl.class);
|
||||
|
||||
private final MapperRegistry mapperRegistry;
|
||||
private ConcurrentMap<Class<?>, EntityInfo> entityInfoCache =
|
||||
new ConcurrentHashMap<Class<?>, EntityInfo>();
|
||||
|
||||
|
||||
public MongoStoreImpl(DB database, Class<?>[] managedEntityTypes) {
|
||||
this.database = database;
|
||||
|
||||
mapperRegistry = new MapperRegistry();
|
||||
|
||||
for (Class<?> simpleMapperClass : SIMPLE_TYPES) {
|
||||
SimpleMapper mapper = new SimpleMapper(simpleMapperClass);
|
||||
mapperRegistry.addAppObjectMapper(mapper);
|
||||
mapperRegistry.addDBObjectMapper(mapper);
|
||||
}
|
||||
|
||||
// Specific converter for ArrayList is added just for performance purposes to avoid recursive converter lookup (most of list idm will be ArrayList)
|
||||
mapperRegistry.addAppObjectMapper(new ListMapper(mapperRegistry, ArrayList.class));
|
||||
mapperRegistry.addAppObjectMapper(new ListMapper(mapperRegistry, List.class));
|
||||
mapperRegistry.addDBObjectMapper(new BasicDBListMapper(mapperRegistry));
|
||||
|
||||
mapperRegistry.addAppObjectMapper(new ListMapper(mapperRegistry, HashSet.class));
|
||||
mapperRegistry.addAppObjectMapper(new ListMapper(mapperRegistry, Set.class));
|
||||
mapperRegistry.addDBObjectMapper(new BasicDBListToSetMapper(mapperRegistry));
|
||||
|
||||
mapperRegistry.addAppObjectMapper(new MapMapper(mapperRegistry, HashMap.class));
|
||||
mapperRegistry.addAppObjectMapper(new MapMapper(mapperRegistry, Map.class));
|
||||
mapperRegistry.addDBObjectMapper(new BasicDBObjectToMapMapper(mapperRegistry));
|
||||
|
||||
// Enum converters
|
||||
mapperRegistry.addAppObjectMapper(new EnumToStringMapper());
|
||||
mapperRegistry.addDBObjectMapper(new StringToEnumMapper());
|
||||
|
||||
for (Class<?> type : managedEntityTypes) {
|
||||
getEntityInfo(type);
|
||||
mapperRegistry.addAppObjectMapper(new MongoEntityMapper(this, mapperRegistry, type));
|
||||
mapperRegistry.addDBObjectMapper(new BasicDBObjectMapper(this, mapperRegistry, type));
|
||||
}
|
||||
}
|
||||
|
||||
protected void dropDatabase() {
|
||||
this.database.dropDatabase();
|
||||
logger.info("Database " + this.database.getName() + " dropped in MongoDB");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void insertEntity(MongoIdentifiableEntity entity, MongoStoreInvocationContext context) {
|
||||
Class<? extends MongoEntity> clazz = entity.getClass();
|
||||
|
||||
// Find annotations for ID, for all the properties and for the name of the collection.
|
||||
EntityInfo entityInfo = getEntityInfo(clazz);
|
||||
|
||||
// Create instance of BasicDBObject and add all declared properties to it (properties with null value probably should be skipped)
|
||||
BasicDBObject dbObject = mapperRegistry.convertApplicationObjectToDBObject(entity, BasicDBObject.class);
|
||||
|
||||
DBCollection dbCollection = database.getCollection(entityInfo.getDbCollectionName());
|
||||
|
||||
String currentId = entity.getId();
|
||||
|
||||
// Generate random ID if not set already
|
||||
if (currentId == null) {
|
||||
currentId = KeycloakModelUtils.generateId();
|
||||
entity.setId(currentId);
|
||||
}
|
||||
|
||||
// Adding "_id"
|
||||
dbObject.put("_id", currentId);
|
||||
|
||||
try {
|
||||
dbCollection.insert(dbObject);
|
||||
} catch (MongoException e) {
|
||||
throw convertException(e);
|
||||
}
|
||||
|
||||
// Treat object as created in this transaction (It is already submitted to transaction)
|
||||
context.addCreatedEntity(entity);
|
||||
}
|
||||
|
||||
public static ModelException convertException(MongoException e) {
|
||||
if (e instanceof MongoException.DuplicateKey) {
|
||||
return new ModelDuplicateException(e);
|
||||
} else {
|
||||
return new ModelException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateEntity(final MongoIdentifiableEntity entity, MongoStoreInvocationContext context) {
|
||||
MongoTask fullUpdateTask = new MongoTask() {
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
Class<? extends MongoEntity> clazz = entity.getClass();
|
||||
EntityInfo entityInfo = getEntityInfo(clazz);
|
||||
BasicDBObject dbObject = mapperRegistry.convertApplicationObjectToDBObject(entity, BasicDBObject.class);
|
||||
DBCollection dbCollection = database.getCollection(entityInfo.getDbCollectionName());
|
||||
|
||||
String currentId = entity.getId();
|
||||
|
||||
if (currentId == null) {
|
||||
throw new IllegalStateException("Can't update entity without id: " + entity);
|
||||
} else {
|
||||
BasicDBObject query = new BasicDBObject("_id", currentId);
|
||||
dbCollection.update(query, dbObject);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFullUpdate() {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
// update is just added to context and postponed
|
||||
context.addUpdateTask(entity, fullUpdateTask);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends MongoIdentifiableEntity> int updateEntities(Class<T> type, DBObject query, DBObject update, MongoStoreInvocationContext context) {
|
||||
context.beforeDBBulkUpdateOrRemove(type);
|
||||
|
||||
DBCollection collection = getDBCollectionForType(type);
|
||||
WriteResult wr = collection.update(query, update, false, true);
|
||||
|
||||
logger.debugf("Updated %d collections of type %s", wr.getN(), type);
|
||||
return wr.getN();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends MongoIdentifiableEntity> T loadEntity(Class<T> type, String id, MongoStoreInvocationContext context) {
|
||||
// First look if we already read the object with this oid and type during this transaction. If yes, use it instead of DB lookup
|
||||
T cached = context.getLoadedEntity(type, id);
|
||||
if (cached != null && type.isAssignableFrom(cached.getClass())) return cached;
|
||||
|
||||
DBCollection dbCollection = getDBCollectionForType(type);
|
||||
|
||||
BasicDBObject idQuery = new BasicDBObject("_id", id);
|
||||
DBObject dbObject = dbCollection.findOne(idQuery);
|
||||
|
||||
if (dbObject == null) return null;
|
||||
|
||||
MapperContext<Object, T> mapperContext = new MapperContext<Object, T>(dbObject, type, null);
|
||||
T converted = mapperRegistry.convertDBObjectToApplicationObject(mapperContext);
|
||||
|
||||
// Now add it to loaded objects
|
||||
context.addLoadedEntity(converted);
|
||||
|
||||
return converted;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public <T extends MongoIdentifiableEntity> T loadSingleEntity(Class<T> type, DBObject query, MongoStoreInvocationContext context) {
|
||||
// First we should execute all pending tasks before searching DB
|
||||
context.beforeDBSearch(type);
|
||||
|
||||
DBCollection dbCollection = getDBCollectionForType(type);
|
||||
DBObject dbObject = dbCollection.findOne(query);
|
||||
|
||||
if (dbObject == null) {
|
||||
return null;
|
||||
} else {
|
||||
return convertDBObjectToEntity(type, dbObject, context);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public <T extends MongoIdentifiableEntity> List<T> loadEntities(Class<T> type, DBObject query, MongoStoreInvocationContext context) {
|
||||
// First we should execute all pending tasks before searching DB
|
||||
context.beforeDBSearch(type);
|
||||
|
||||
DBCollection dbCollection = getDBCollectionForType(type);
|
||||
DBCursor cursor = dbCollection.find(query);
|
||||
|
||||
return convertCursor(type, cursor, context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends MongoIdentifiableEntity> List<T> loadEntities(Class<T> type, DBObject query, DBObject sort, int firstResult, int maxResults, MongoStoreInvocationContext context) {
|
||||
// First we should execute all pending tasks before searching DB
|
||||
context.beforeDBSearch(type);
|
||||
|
||||
DBCollection dbCollection = getDBCollectionForType(type);
|
||||
DBCursor cursor = dbCollection.find(query);
|
||||
if (firstResult != -1) {
|
||||
cursor.skip(firstResult);
|
||||
}
|
||||
if (maxResults != -1) {
|
||||
cursor.limit(maxResults);
|
||||
}
|
||||
if (sort != null) {
|
||||
cursor.sort(sort);
|
||||
}
|
||||
|
||||
return convertCursor(type, cursor, context);
|
||||
}
|
||||
|
||||
public <T extends MongoIdentifiableEntity> int countEntities(Class<T> type, DBObject query, MongoStoreInvocationContext context) {
|
||||
context.beforeDBSearch(type);
|
||||
|
||||
DBCollection dbCollection = getDBCollectionForType(type);
|
||||
Long count = dbCollection.count(query);
|
||||
|
||||
// For now, assume that int is sufficient
|
||||
return count.intValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean removeEntity(MongoIdentifiableEntity entity, MongoStoreInvocationContext context) {
|
||||
return removeEntity(entity.getClass(), entity.getId(), context);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean removeEntity(Class<? extends MongoIdentifiableEntity> type, String id, MongoStoreInvocationContext context) {
|
||||
MongoIdentifiableEntity found = loadEntity(type, id, context);
|
||||
if (found == null) {
|
||||
return false;
|
||||
} else {
|
||||
DBCollection dbCollection = getDBCollectionForType(type);
|
||||
BasicDBObject dbQuery = new BasicDBObject("_id", id);
|
||||
dbCollection.remove(dbQuery);
|
||||
//logger.debugf("Entity of type: %s , id: %s removed from MongoDB.", type, id);
|
||||
|
||||
context.addRemovedEntity(found);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int removeEntities(Class<? extends MongoIdentifiableEntity> type, DBObject query, boolean callback, MongoStoreInvocationContext context) {
|
||||
if (callback) {
|
||||
List<? extends MongoIdentifiableEntity> foundObjects = loadEntities(type, query, context);
|
||||
if (foundObjects.size() == 0) {
|
||||
return 0;
|
||||
} else {
|
||||
DBCollection dbCollection = getDBCollectionForType(type);
|
||||
dbCollection.remove(query);
|
||||
|
||||
logger.debugf("Removed %d entities of type: %s, query: %s", foundObjects.size(), type, query);
|
||||
|
||||
for (MongoIdentifiableEntity found : foundObjects) {
|
||||
context.addRemovedEntity(found);;
|
||||
}
|
||||
return foundObjects.size();
|
||||
}
|
||||
} else {
|
||||
|
||||
context.beforeDBBulkUpdateOrRemove(type);
|
||||
|
||||
DBCollection dbCollection = getDBCollectionForType(type);
|
||||
WriteResult writeResult = dbCollection.remove(query);
|
||||
int removedCount = writeResult.getN();
|
||||
|
||||
logger.debugf("Removed directly %d entities of type: %s, query: %s", removedCount, type, query);
|
||||
return removedCount;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S> boolean pushItemToList(final MongoIdentifiableEntity entity, final String listPropertyName, S itemToPush, boolean skipIfAlreadyPresent, MongoStoreInvocationContext context) {
|
||||
final Class<? extends MongoEntity> type = entity.getClass();
|
||||
EntityInfo entityInfo = getEntityInfo(type);
|
||||
|
||||
// Add item to list directly in this object
|
||||
Property<Object> listProperty = entityInfo.getPropertyByName(listPropertyName);
|
||||
if (listProperty == null) {
|
||||
throw new IllegalArgumentException("Property " + listPropertyName + " doesn't exist on object " + entity);
|
||||
}
|
||||
|
||||
List<S> list = (List<S>)listProperty.getValue(entity);
|
||||
if (list == null) {
|
||||
list = new ArrayList<S>();
|
||||
listProperty.setValue(entity, list);
|
||||
}
|
||||
|
||||
// Skip if item is already in list
|
||||
if (skipIfAlreadyPresent && list.contains(itemToPush)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Update java object
|
||||
list.add(itemToPush);
|
||||
|
||||
// Add update of list to pending tasks
|
||||
final List<S> listt = list;
|
||||
context.addUpdateTask(entity, new MongoTask() {
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
// Now DB update of new list with usage of $set
|
||||
BasicDBList dbList = mapperRegistry.convertApplicationObjectToDBObject(listt, BasicDBList.class);
|
||||
|
||||
BasicDBObject query = new BasicDBObject("_id", entity.getId());
|
||||
BasicDBObject listObject = new BasicDBObject(listPropertyName, dbList);
|
||||
BasicDBObject setCommand = new BasicDBObject("$set", listObject);
|
||||
getDBCollectionForType(type).update(query, setCommand);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFullUpdate() {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public <S> boolean pullItemFromList(final MongoIdentifiableEntity entity, final String listPropertyName, final S itemToPull, MongoStoreInvocationContext context) {
|
||||
final Class<? extends MongoEntity> type = entity.getClass();
|
||||
EntityInfo entityInfo = getEntityInfo(type);
|
||||
|
||||
// Remove item from list directly in this object
|
||||
Property<Object> listProperty = entityInfo.getPropertyByName(listPropertyName);
|
||||
if (listProperty == null) {
|
||||
throw new IllegalArgumentException("Property " + listPropertyName + " doesn't exist on object " + entity);
|
||||
}
|
||||
List<S> list = (List<S>)listProperty.getValue(entity);
|
||||
|
||||
// If list is null, we skip both object and DB update
|
||||
if (list == null || !list.contains(itemToPull)) {
|
||||
return false;
|
||||
} else {
|
||||
|
||||
// Update java object
|
||||
list.remove(itemToPull);
|
||||
|
||||
// Add update of list to pending tasks
|
||||
context.addUpdateTask(entity, new MongoTask() {
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
// Pull item from DB
|
||||
Object dbItemToPull = mapperRegistry.convertApplicationObjectToDBObject(itemToPull, Object.class);
|
||||
BasicDBObject query = new BasicDBObject("_id", entity.getId());
|
||||
BasicDBObject pullObject = new BasicDBObject(listPropertyName, dbItemToPull);
|
||||
BasicDBObject pullCommand = new BasicDBObject("$pull", pullObject);
|
||||
getDBCollectionForType(type).update(query, pullCommand);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFullUpdate() {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Possibility to add user-defined mappers
|
||||
public void addAppObjectConverter(Mapper<?, ?> mapper) {
|
||||
mapperRegistry.addAppObjectMapper(mapper);
|
||||
}
|
||||
|
||||
public void addDBObjectConverter(Mapper<?, ?> mapper) {
|
||||
mapperRegistry.addDBObjectMapper(mapper);
|
||||
}
|
||||
|
||||
public EntityInfo getEntityInfo(Class<?> entityClass) {
|
||||
EntityInfo entityInfo = entityInfoCache.get(entityClass);
|
||||
if (entityInfo == null) {
|
||||
Map<String, Property<Object>> properties = PropertyQueries.createQuery(entityClass).getWritableResultList();
|
||||
|
||||
MongoCollection classAnnotation = entityClass.getAnnotation(MongoCollection.class);
|
||||
|
||||
String dbCollectionName = classAnnotation==null ? null : classAnnotation.collectionName();
|
||||
entityInfo = new EntityInfo(entityClass, dbCollectionName, properties);
|
||||
|
||||
EntityInfo existing = entityInfoCache.putIfAbsent(entityClass, entityInfo);
|
||||
if (existing != null) {
|
||||
entityInfo = existing;
|
||||
}
|
||||
}
|
||||
|
||||
return entityInfo;
|
||||
}
|
||||
|
||||
protected <T extends MongoIdentifiableEntity> List<T> convertCursor(Class<T> type, DBCursor cursor, MongoStoreInvocationContext context) {
|
||||
List<T> result = new ArrayList<T>();
|
||||
|
||||
try {
|
||||
for (DBObject dbObject : cursor) {
|
||||
T entity = convertDBObjectToEntity(type, dbObject, context);
|
||||
result.add(entity);
|
||||
}
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected <T extends MongoIdentifiableEntity> T convertDBObjectToEntity(Class<T> type, DBObject dbObject, MongoStoreInvocationContext context) {
|
||||
// First look if we already have loaded object cached. If yes, we will use cached instance
|
||||
String id = dbObject.get("_id").toString();
|
||||
T object = context.getLoadedEntity(type, id);
|
||||
|
||||
if (object == null) {
|
||||
// So convert and use fresh instance from DB
|
||||
MapperContext<Object, T> mapperContext = new MapperContext<Object, T>(dbObject, type, null);
|
||||
object = mapperRegistry.convertDBObjectToApplicationObject(mapperContext);
|
||||
context.addLoadedEntity(object);
|
||||
}
|
||||
return object;
|
||||
}
|
||||
|
||||
protected DBCollection getDBCollectionForType(Class<?> type) {
|
||||
EntityInfo entityInfo = getEntityInfo(type);
|
||||
String dbCollectionName = entityInfo.getDbCollectionName();
|
||||
return dbCollectionName==null ? null : database.getCollection(dbCollectionName);
|
||||
}
|
||||
}
|
||||
+4
@@ -45,6 +45,10 @@ public class SimpleMongoStoreInvocationContext implements MongoStoreInvocationCo
|
||||
public void beforeDBSearch(Class<? extends MongoIdentifiableEntity> entityType) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeDBBulkUpdateOrRemove(Class<? extends MongoIdentifiableEntity> entityType) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void begin() {
|
||||
}
|
||||
|
||||
+18
-4
@@ -50,10 +50,6 @@ public class TransactionMongoStoreInvocationContext implements MongoStoreInvocat
|
||||
|
||||
@Override
|
||||
public void addUpdateTask(MongoIdentifiableEntity entityToUpdate, MongoTask task) {
|
||||
if (!loadedObjects.containsValue(entityToUpdate)) {
|
||||
throw new IllegalStateException("Entity " + entityToUpdate + " not found in loaded objects");
|
||||
}
|
||||
|
||||
Set<MongoTask> currentObjectTasks = pendingUpdateTasks.get(entityToUpdate);
|
||||
if (currentObjectTasks == null) {
|
||||
currentObjectTasks = new LinkedHashSet<MongoTask>();
|
||||
@@ -106,6 +102,24 @@ public class TransactionMongoStoreInvocationContext implements MongoStoreInvocat
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeDBBulkUpdateOrRemove(Class<? extends MongoIdentifiableEntity> entityType) {
|
||||
beforeDBSearch(entityType);
|
||||
Set<String> toRemove = new HashSet<String>();
|
||||
|
||||
for (Map.Entry<String, MongoIdentifiableEntity> entry : loadedObjects.entrySet()) {
|
||||
MongoIdentifiableEntity entity = entry.getValue();
|
||||
if (entity.getClass().equals(entityType)) {
|
||||
toRemove.add(entry.getKey());
|
||||
}
|
||||
}
|
||||
|
||||
// Now remove all loadedObjects
|
||||
for (String objectId : toRemove) {
|
||||
loadedObjects.remove(objectId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void begin() {
|
||||
loadedObjects.clear();
|
||||
|
||||
+45
-45
@@ -1,45 +1,45 @@
|
||||
package org.keycloak.connections.mongo.impl.types;
|
||||
|
||||
import com.mongodb.BasicDBList;
|
||||
import org.keycloak.connections.mongo.api.types.Mapper;
|
||||
import org.keycloak.connections.mongo.api.types.MapperContext;
|
||||
import org.keycloak.connections.mongo.api.types.MapperRegistry;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class BasicDBListMapper implements Mapper<BasicDBList, List> {
|
||||
|
||||
private final MapperRegistry mapperRegistry;
|
||||
|
||||
public BasicDBListMapper(MapperRegistry mapperRegistry) {
|
||||
this.mapperRegistry = mapperRegistry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List convertObject(MapperContext<BasicDBList, List> context) {
|
||||
BasicDBList dbList = context.getObjectToConvert();
|
||||
ArrayList<Object> appObjects = new ArrayList<Object>();
|
||||
Class<?> expectedListElementType = context.getGenericTypes().get(0);
|
||||
|
||||
for (Object dbObject : dbList) {
|
||||
MapperContext<Object, Object> newContext = new MapperContext<Object, Object>(dbObject, expectedListElementType, null);
|
||||
appObjects.add(mapperRegistry.convertDBObjectToApplicationObject(newContext));
|
||||
}
|
||||
return appObjects;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends BasicDBList> getTypeOfObjectToConvert() {
|
||||
return BasicDBList.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<List> getExpectedReturnType() {
|
||||
return List.class;
|
||||
}
|
||||
}
|
||||
package org.keycloak.connections.mongo.impl.types;
|
||||
|
||||
import com.mongodb.BasicDBList;
|
||||
import org.keycloak.connections.mongo.api.types.Mapper;
|
||||
import org.keycloak.connections.mongo.api.types.MapperContext;
|
||||
import org.keycloak.connections.mongo.api.types.MapperRegistry;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class BasicDBListMapper implements Mapper<BasicDBList, List> {
|
||||
|
||||
private final MapperRegistry mapperRegistry;
|
||||
|
||||
public BasicDBListMapper(MapperRegistry mapperRegistry) {
|
||||
this.mapperRegistry = mapperRegistry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List convertObject(MapperContext<BasicDBList, List> context) {
|
||||
BasicDBList dbList = context.getObjectToConvert();
|
||||
ArrayList<Object> appObjects = new ArrayList<Object>();
|
||||
Class<?> expectedListElementType = (Class<?>) context.getGenericTypes().get(0);
|
||||
|
||||
for (Object dbObject : dbList) {
|
||||
MapperContext<Object, Object> newContext = new MapperContext<Object, Object>(dbObject, expectedListElementType, null);
|
||||
appObjects.add(mapperRegistry.convertDBObjectToApplicationObject(newContext));
|
||||
}
|
||||
return appObjects;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends BasicDBList> getTypeOfObjectToConvert() {
|
||||
return BasicDBList.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<List> getExpectedReturnType() {
|
||||
return List.class;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -23,7 +23,7 @@ public class BasicDBListToSetMapper implements Mapper<BasicDBList, Set> {
|
||||
public Set convertObject(MapperContext<BasicDBList, Set> context) {
|
||||
BasicDBList dbList = context.getObjectToConvert();
|
||||
Set<Object> appObjects = new HashSet<Object>();
|
||||
Class<?> expectedListElementType = context.getGenericTypes().get(0);
|
||||
Class<?> expectedListElementType = (Class<?>) context.getGenericTypes().get(0);
|
||||
|
||||
for (Object dbObject : dbList) {
|
||||
MapperContext<Object, Object> newContext = new MapperContext<Object, Object>(dbObject, expectedListElementType, null);
|
||||
|
||||
+2
-4
@@ -14,6 +14,7 @@ import org.keycloak.util.reflections.Types;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -87,10 +88,7 @@ public class BasicDBObjectMapper<S> implements Mapper<BasicDBObject, S> {
|
||||
ParameterizedType parameterized = (ParameterizedType) type;
|
||||
Type[] genericTypeArguments = parameterized.getActualTypeArguments();
|
||||
|
||||
List<Class<?>> genericTypes = new ArrayList<Class<?>>();
|
||||
for (Type genericType : genericTypeArguments) {
|
||||
genericTypes.add((Class<?>)genericType);
|
||||
}
|
||||
List<Type> genericTypes = Arrays.asList(genericTypeArguments);
|
||||
|
||||
Class<?> expectedReturnType = (Class<?>)parameterized.getRawType();
|
||||
context = new MapperContext<Object, Object>(valueFromDB, expectedReturnType, genericTypes);
|
||||
|
||||
Loaded 100 of 3109 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user