├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── deleteuser.png ├── deployment-to-docker.sh ├── deployment.sh ├── dist ├── HikariCP-4.0.3.jar ├── checker-qual-3.5.0.jar ├── commons-codec-1.15.jar ├── commons-io-2.6.jar ├── commons-lang3-3.12.0.jar ├── guava-21.0.jar ├── hamcrest-core-1.3.jar ├── jbcrypt-0.4.jar ├── jtds-1.3.1.jar ├── junit-4.13.2.jar ├── lombok-1.18.22.jar ├── mysql-connector-java-8.0.12.jar ├── ojdbc8-19.3.0.0.jar ├── ons-19.3.0.0.jar ├── oraclepki-19.3.0.0.jar ├── osdt_cert-19.3.0.0.jar ├── osdt_core-19.3.0.0.jar ├── postgresql-42.2.19.jar ├── protobuf-java-2.6.0.jar ├── simplefan-19.3.0.0.jar ├── singular-user-storage-provider.jar ├── slf4j-api-2.0.0-alpha1.jar └── ucp-19.3.0.0.jar ├── pom.xml ├── screen.png ├── screen2.png └── src └── main ├── java └── org │ └── opensingular │ └── dbuserprovider │ ├── DBUserStorageException.java │ ├── DBUserStorageProvider.java │ ├── DBUserStorageProviderFactory.java │ ├── model │ ├── QueryConfigurations.java │ └── UserAdapter.java │ ├── persistence │ ├── DataSourceProvider.java │ ├── RDBMS.java │ └── UserRepository.java │ └── util │ ├── PBKDF2SHA256HashingUtil.java │ ├── PagingUtil.java │ └── PreparedStatementParameterCollector.java └── resources └── META-INF └── services └── org.keycloak.storage.UserStorageProviderFactory /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | * text=auto 3 | **/*.h2.db 4 | **/*.iml 5 | **/*.mv.db 6 | **/*.tmp 7 | **/*.trace.db 8 | **/*.ucls 9 | **/.externalToolBuilders/ 10 | **/overlays/ 11 | **/rebel.xml 12 | **/singulardb.lock.db 13 | **/target/ 14 | *.bak 15 | *.bat text eol=crlf 16 | *.crt binary 17 | *.css text 18 | *.dll binary 19 | *.doc binary 20 | *.docx binary 21 | *.dylib binary 22 | *.eot binary 23 | *.exe binary 24 | *.gif binary 25 | *.html text 26 | *.jar binary 27 | *.java text 28 | *.jpg binary 29 | *.js text 30 | *.otf binary 31 | *.pdf binary 32 | *.png binary 33 | *.properties text 34 | *.sh text 35 | *.so binary 36 | *.so.0 binary 37 | *.so.0.0.0 binary 38 | *.so.0.1 binary 39 | *.so.0.1.0 binary 40 | *.so.0.12.4 binary 41 | *.so.1 binary 42 | *.so.1.0.0 binary 43 | *.so.1.1.0 binary 44 | *.so.1.2.7 binary 45 | *.so.1.3.0 binary 46 | *.so.1.7.0 binary 47 | *.so.12 binary 48 | *.so.6 binary 49 | *.so.6.0.0 binary 50 | *.so.6.10.0 binary 51 | *.so.6.3.0 binary 52 | *.so.6.4.0 binary 53 | *.svg binary 54 | *.swp 55 | *.tmp 56 | *.truststore binary 57 | *.ttf binary 58 | *.woff binary 59 | *.woff2 binary 60 | *.xml text 61 | *.zip binary 62 | *~.nib 63 | .DS_Store 64 | .DS_Store? 65 | .Spotlight-V100 66 | .Trashes 67 | ._* 68 | .classpath 69 | .idea/ 70 | .loadpath 71 | .metadata 72 | .project 73 | .settings/ 74 | Thumbs.db 75 | _confHomol/ 76 | atlassian-ide-plugin.xml 77 | bin/** 78 | buildall.sh 79 | classes/ 80 | ehthumbs.db 81 | flow/test/singulardb.trace.db.old 82 | local.properties 83 | out/** 84 | pom.xml.next 85 | pom.xml.releaseBackup 86 | pom.xml.tag 87 | pom.xml.versionsBackup 88 | release.properties 89 | resources/ui-static-resources/src/main/webapp/resources/comum/* linguist-vendored 90 | tmp/** 91 | tmp/**/* 92 | wkhtmltoimage binary 93 | wkhtmltopdf binary 94 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | keycloak 2 | **/target 3 | .DS_Store 4 | .idea 5 | .vscode/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # singular-keycloak-database-federation 2 | 3 | 4 | 5 | ### Compatible with Keycloak 17+ quarkus based. 6 | 7 | ### ** Keycloak 19+ ** KNOWN ISSUE: 8 | 9 | #### New Theme breaks custom providers, to overcome this problem, follow these steps: 10 | 11 | 12 | - Click "Realm Settings" on the left menu 13 | - Then click the tab "Themes" 14 | - And, for the selection input labeled "Admin console theme", select "keycloak" 15 | - Logoff and login again 16 | - Now, if you try to configure this provider again, keycloak should render all configuration fields and everything else should work fine. 17 | 18 | See issue #19 for further information. 19 | 20 | 21 | 22 | **For older versions look at older_versions branch. 23 | 24 | 25 | Keycloak User Storage SPI for Relational Databases (Keycloak User Federation, supports postgresql, mysql, oracle and mysql). 26 | 27 | - Keycloak User federation provider with SQL 28 | - Keycloak User federation using existing database 29 | - Keycloak database user provider 30 | - Keycloak MSSQL Database Integration 31 | - Keycloak SQL Server Database Integration 32 | - Keycloak Oracle Database Integration 33 | - Keycloak Postgres Database Integration 34 | - Keycloak blowfish bcrypt support 35 | 36 | 37 | 38 | ## Usage 39 | 40 | Fully compatible with Singular Studio NOCODE. See https://www.studio.opensingular.com/ 41 | 42 | 43 | ## Configuration 44 | 45 | Keycloak User Federation Screen Shot 46 | 47 | ![Sample Screenshot](screen.png) 48 | 49 | There is a new configuration that allows keycloak to remove a user entry from its local database (this option has no effect on the source database). It can be useful when you need to reload user data. 50 | This option can be configured by the following switch: 51 | 52 | ![Sample Screenshot](deleteuser.png) 53 | 54 | ## Limitations 55 | 56 | - Do not allow user information update, including password update 57 | - Do not supports user roles our groups 58 | 59 | ## Custom attributes 60 | 61 | Just add a mapper to client mappers with the same name as the returned column alias in your queries.Use mapper type "User Attribute". See the example below: 62 | 63 | ![Sample Screenshot 2](screen2.png) 64 | 65 | 66 | ## Build 67 | 68 | - mvn clean package 69 | 70 | ## Deployment 71 | 72 | 1) Copy every `.jar` from dist/ folder to /providers folder under your keycloak installation root. 73 | - i.e, on a default keycloak setup, copy all `.jar` files to /providers 74 | 2) run : 75 | $ ./bin/kc.sh start-dev 76 | OR if you are using a production configuration: 77 | $ ./bin/kc.sh build 78 | $ ./bin/kc.sh start 79 | 80 | ## For futher information see: 81 | - https://github.com/keycloak/keycloak/issues/9833 82 | - https://www.keycloak.org/docs/latest/server_development/#packaging-and-deployment 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /deleteuser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opensingular/singular-keycloak-database-federation/59e9af5bd80266e6b62c57e1abc918c780062cf2/deleteuser.png -------------------------------------------------------------------------------- /deployment-to-docker.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | read -p "Enter container name of Keycloak: " containerName 3 | containerId=$(docker ps -aqf "name=$containerName$") 4 | echo "container id= $containerId" 5 | 6 | mvn clean package && docker cp ./dist/. "$containerId":/opt/keycloak/providers 7 | -------------------------------------------------------------------------------- /deployment.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | read -p "Enter absolute path of Keycloak folder: " pathKeycloak 3 | 4 | mvn clean package && cp ./dist/* "$pathKeycloak"/providers -------------------------------------------------------------------------------- /dist/HikariCP-4.0.3.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opensingular/singular-keycloak-database-federation/59e9af5bd80266e6b62c57e1abc918c780062cf2/dist/HikariCP-4.0.3.jar -------------------------------------------------------------------------------- /dist/checker-qual-3.5.0.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opensingular/singular-keycloak-database-federation/59e9af5bd80266e6b62c57e1abc918c780062cf2/dist/checker-qual-3.5.0.jar -------------------------------------------------------------------------------- /dist/commons-codec-1.15.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opensingular/singular-keycloak-database-federation/59e9af5bd80266e6b62c57e1abc918c780062cf2/dist/commons-codec-1.15.jar -------------------------------------------------------------------------------- /dist/commons-io-2.6.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opensingular/singular-keycloak-database-federation/59e9af5bd80266e6b62c57e1abc918c780062cf2/dist/commons-io-2.6.jar -------------------------------------------------------------------------------- /dist/commons-lang3-3.12.0.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opensingular/singular-keycloak-database-federation/59e9af5bd80266e6b62c57e1abc918c780062cf2/dist/commons-lang3-3.12.0.jar -------------------------------------------------------------------------------- /dist/guava-21.0.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opensingular/singular-keycloak-database-federation/59e9af5bd80266e6b62c57e1abc918c780062cf2/dist/guava-21.0.jar -------------------------------------------------------------------------------- /dist/hamcrest-core-1.3.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opensingular/singular-keycloak-database-federation/59e9af5bd80266e6b62c57e1abc918c780062cf2/dist/hamcrest-core-1.3.jar -------------------------------------------------------------------------------- /dist/jbcrypt-0.4.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opensingular/singular-keycloak-database-federation/59e9af5bd80266e6b62c57e1abc918c780062cf2/dist/jbcrypt-0.4.jar -------------------------------------------------------------------------------- /dist/jtds-1.3.1.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opensingular/singular-keycloak-database-federation/59e9af5bd80266e6b62c57e1abc918c780062cf2/dist/jtds-1.3.1.jar -------------------------------------------------------------------------------- /dist/junit-4.13.2.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opensingular/singular-keycloak-database-federation/59e9af5bd80266e6b62c57e1abc918c780062cf2/dist/junit-4.13.2.jar -------------------------------------------------------------------------------- /dist/lombok-1.18.22.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opensingular/singular-keycloak-database-federation/59e9af5bd80266e6b62c57e1abc918c780062cf2/dist/lombok-1.18.22.jar -------------------------------------------------------------------------------- /dist/mysql-connector-java-8.0.12.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opensingular/singular-keycloak-database-federation/59e9af5bd80266e6b62c57e1abc918c780062cf2/dist/mysql-connector-java-8.0.12.jar -------------------------------------------------------------------------------- /dist/ojdbc8-19.3.0.0.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opensingular/singular-keycloak-database-federation/59e9af5bd80266e6b62c57e1abc918c780062cf2/dist/ojdbc8-19.3.0.0.jar -------------------------------------------------------------------------------- /dist/ons-19.3.0.0.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opensingular/singular-keycloak-database-federation/59e9af5bd80266e6b62c57e1abc918c780062cf2/dist/ons-19.3.0.0.jar -------------------------------------------------------------------------------- /dist/oraclepki-19.3.0.0.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opensingular/singular-keycloak-database-federation/59e9af5bd80266e6b62c57e1abc918c780062cf2/dist/oraclepki-19.3.0.0.jar -------------------------------------------------------------------------------- /dist/osdt_cert-19.3.0.0.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opensingular/singular-keycloak-database-federation/59e9af5bd80266e6b62c57e1abc918c780062cf2/dist/osdt_cert-19.3.0.0.jar -------------------------------------------------------------------------------- /dist/osdt_core-19.3.0.0.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opensingular/singular-keycloak-database-federation/59e9af5bd80266e6b62c57e1abc918c780062cf2/dist/osdt_core-19.3.0.0.jar -------------------------------------------------------------------------------- /dist/postgresql-42.2.19.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opensingular/singular-keycloak-database-federation/59e9af5bd80266e6b62c57e1abc918c780062cf2/dist/postgresql-42.2.19.jar -------------------------------------------------------------------------------- /dist/protobuf-java-2.6.0.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opensingular/singular-keycloak-database-federation/59e9af5bd80266e6b62c57e1abc918c780062cf2/dist/protobuf-java-2.6.0.jar -------------------------------------------------------------------------------- /dist/simplefan-19.3.0.0.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opensingular/singular-keycloak-database-federation/59e9af5bd80266e6b62c57e1abc918c780062cf2/dist/simplefan-19.3.0.0.jar -------------------------------------------------------------------------------- /dist/singular-user-storage-provider.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opensingular/singular-keycloak-database-federation/59e9af5bd80266e6b62c57e1abc918c780062cf2/dist/singular-user-storage-provider.jar -------------------------------------------------------------------------------- /dist/slf4j-api-2.0.0-alpha1.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opensingular/singular-keycloak-database-federation/59e9af5bd80266e6b62c57e1abc918c780062cf2/dist/slf4j-api-2.0.0-alpha1.jar -------------------------------------------------------------------------------- /dist/ucp-19.3.0.0.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opensingular/singular-keycloak-database-federation/59e9af5bd80266e6b62c57e1abc918c780062cf2/dist/ucp-19.3.0.0.jar -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 4.0.0 7 | 8 | 9 | singular-user-storage-provider 10 | org.opensingular 11 | 2.4 12 | 13 | 14 | 15 | org.keycloak 16 | keycloak-core 17 | ${keycloak.version} 18 | provided 19 | 20 | 21 | 22 | org.keycloak 23 | keycloak-server-spi 24 | ${keycloak.version} 25 | provided 26 | 27 | 28 | 29 | org.jboss.logging 30 | jboss-logging 31 | ${jboss-logging.version} 32 | provided 33 | 34 | 35 | 36 | 37 | com.google.guava 38 | guava 39 | 21.0 40 | 41 | 42 | 43 | com.google.auto.service 44 | auto-service 45 | true 46 | 47 | 48 | 49 | org.projectlombok 50 | lombok 51 | ${lombok.version} 52 | true 53 | 54 | 55 | 56 | 57 | com.zaxxer 58 | HikariCP 59 | 4.0.3 60 | 61 | 62 | 63 | 64 | org.apache.commons 65 | commons-lang3 66 | 3.12.0 67 | 68 | 69 | 70 | commons-codec 71 | commons-codec 72 | 1.15 73 | 74 | 75 | 76 | 77 | commons-io 78 | commons-io 79 | 2.6 80 | 81 | 82 | 83 | net.sourceforge.jtds 84 | jtds 85 | 1.3.1 86 | 87 | 88 | 89 | mysql 90 | mysql-connector-java 91 | 8.0.12 92 | 93 | 94 | 95 | org.postgresql 96 | postgresql 97 | 42.2.19 98 | 99 | 100 | 101 | com.ibm.db2.jcc 102 | db2jcc 103 | db2jcc4 104 | 105 | 106 | 107 | com.oracle.ojdbc 108 | ojdbc8 109 | 19.3.0.0 110 | 111 | 112 | 113 | at.favre.lib 114 | bcrypt 115 | 0.9.0 116 | 117 | 118 | junit 119 | junit 120 | 4.13.2 121 | test 122 | 123 | 124 | 125 | org.hibernate 126 | hibernate-core 127 | 5.4.18.Final 128 | provided 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | singular-user-storage-provider 137 | 138 | 139 | org.wildfly.plugins 140 | wildfly-maven-plugin 141 | 2.0.2.Final 142 | 143 | false 144 | 145 | 146 | 147 | maven-dependency-plugin 148 | 149 | 150 | package 151 | 152 | copy-dependencies 153 | 154 | 155 | provided 156 | ${project.basedir}/dist 157 | 158 | 159 | 160 | 161 | 162 | org.apache.maven.plugins 163 | maven-jar-plugin 164 | 3.2.2 165 | 166 | ${project.basedir}/dist 167 | 168 | 169 | 170 | 171 | 172 | 173 | UTF-8 174 | 1.8 175 | 1.8 176 | 177 | 1.18.22 178 | 3.3.1.Final 179 | 17.0.1 180 | 1.0-rc5 181 | target/keycloak 182 | 183 | 184 | 185 | 186 | 187 | 188 | com.google.auto.service 189 | auto-service 190 | ${auto-service.version} 191 | provided 192 | true 193 | 194 | 195 | 196 | 197 | -------------------------------------------------------------------------------- /screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opensingular/singular-keycloak-database-federation/59e9af5bd80266e6b62c57e1abc918c780062cf2/screen.png -------------------------------------------------------------------------------- /screen2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opensingular/singular-keycloak-database-federation/59e9af5bd80266e6b62c57e1abc918c780062cf2/screen2.png -------------------------------------------------------------------------------- /src/main/java/org/opensingular/dbuserprovider/DBUserStorageException.java: -------------------------------------------------------------------------------- 1 | package org.opensingular.dbuserprovider; 2 | 3 | public class DBUserStorageException extends RuntimeException { 4 | 5 | public DBUserStorageException(String message, Throwable cause) { 6 | super(message, cause); 7 | } 8 | 9 | public DBUserStorageException(Throwable cause) { 10 | super(cause); 11 | } 12 | 13 | public DBUserStorageException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { 14 | super(message, cause, enableSuppression, writableStackTrace); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/org/opensingular/dbuserprovider/DBUserStorageProvider.java: -------------------------------------------------------------------------------- 1 | package org.opensingular.dbuserprovider; 2 | 3 | import lombok.extern.jbosslog.JBossLog; 4 | import org.keycloak.component.ComponentModel; 5 | import org.keycloak.credential.CredentialInput; 6 | import org.keycloak.credential.CredentialInputUpdater; 7 | import org.keycloak.credential.CredentialInputValidator; 8 | import org.keycloak.models.cache.CachedUserModel; 9 | import org.keycloak.models.*; 10 | import org.keycloak.models.credential.PasswordCredentialModel; 11 | import org.keycloak.storage.StorageId; 12 | import org.keycloak.storage.UserStorageProvider; 13 | import org.keycloak.storage.user.UserLookupProvider; 14 | import org.keycloak.storage.user.UserQueryProvider; 15 | import org.keycloak.storage.user.UserRegistrationProvider; 16 | import org.opensingular.dbuserprovider.model.QueryConfigurations; 17 | import org.opensingular.dbuserprovider.model.UserAdapter; 18 | import org.opensingular.dbuserprovider.persistence.DataSourceProvider; 19 | import org.opensingular.dbuserprovider.persistence.UserRepository; 20 | import org.opensingular.dbuserprovider.util.PagingUtil; 21 | 22 | import java.util.Collections; 23 | import java.util.List; 24 | import java.util.Map; 25 | import java.util.Set; 26 | import java.util.stream.Collectors; 27 | 28 | @JBossLog 29 | public class DBUserStorageProvider implements UserStorageProvider, 30 | UserLookupProvider, UserQueryProvider, CredentialInputUpdater, CredentialInputValidator, UserRegistrationProvider { 31 | 32 | private final KeycloakSession session; 33 | private final ComponentModel model; 34 | private final UserRepository repository; 35 | private final boolean allowDatabaseToOverwriteKeycloak; 36 | 37 | DBUserStorageProvider(KeycloakSession session, ComponentModel model, DataSourceProvider dataSourceProvider, QueryConfigurations queryConfigurations) { 38 | this.session = session; 39 | this.model = model; 40 | this.repository = new UserRepository(dataSourceProvider, queryConfigurations); 41 | this.allowDatabaseToOverwriteKeycloak = queryConfigurations.getAllowDatabaseToOverwriteKeycloak(); 42 | } 43 | 44 | 45 | private List toUserModel(RealmModel realm, List> users) { 46 | return users.stream() 47 | .map(m -> new UserAdapter(session, realm, model, m, allowDatabaseToOverwriteKeycloak)).collect(Collectors.toList()); 48 | } 49 | 50 | 51 | @Override 52 | public boolean supportsCredentialType(String credentialType) { 53 | return PasswordCredentialModel.TYPE.equals(credentialType); 54 | } 55 | 56 | @Override 57 | public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) { 58 | return supportsCredentialType(credentialType); 59 | } 60 | 61 | @Override 62 | public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) { 63 | 64 | log.infov("isValid user credential: userId={0}", user.getId()); 65 | 66 | if (!supportsCredentialType(input.getType()) || !(input instanceof UserCredentialModel)) { 67 | return false; 68 | } 69 | 70 | UserCredentialModel cred = (UserCredentialModel) input; 71 | 72 | UserModel dbUser = user; 73 | // If the cache just got loaded in the last 500 millisec (i.e. probably part of the actual flow), there is no point in reloading the user.) 74 | if (allowDatabaseToOverwriteKeycloak && user instanceof CachedUserModel && (System.currentTimeMillis() - ((CachedUserModel) user).getCacheTimestamp()) > 500) { 75 | dbUser = this.getUserById(user.getId(), realm); 76 | 77 | if (dbUser == null) { 78 | ((CachedUserModel) user).invalidate(); 79 | return false; 80 | } 81 | 82 | // For now, we'll just invalidate the cache if username or email has changed. Eventually we could check all (or a parametered list of) attributes fetched from the DB. 83 | if (!java.util.Objects.equals(user.getUsername(), dbUser.getUsername()) || !java.util.Objects.equals(user.getEmail(), dbUser.getEmail())) { 84 | ((CachedUserModel) user).invalidate(); 85 | } 86 | } 87 | return repository.validateCredentials(dbUser.getUsername(), cred.getChallengeResponse()); 88 | } 89 | 90 | @Override 91 | public boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input) { 92 | 93 | log.infov("updating credential: realm={0} user={1}", realm.getId(), user.getUsername()); 94 | 95 | if (!supportsCredentialType(input.getType()) || !(input instanceof UserCredentialModel)) { 96 | return false; 97 | } 98 | 99 | UserCredentialModel cred = (UserCredentialModel) input; 100 | return repository.updateCredentials(user.getUsername(), cred.getChallengeResponse()); 101 | } 102 | 103 | @Override 104 | public void disableCredentialType(RealmModel realm, UserModel user, String credentialType) { 105 | } 106 | 107 | @Override 108 | public Set getDisableableCredentialTypes(RealmModel realm, UserModel user) { 109 | return Collections.emptySet(); 110 | } 111 | 112 | @Override 113 | public void preRemove(RealmModel realm) { 114 | 115 | log.infov("pre-remove realm"); 116 | } 117 | 118 | @Override 119 | public void preRemove(RealmModel realm, GroupModel group) { 120 | 121 | log.infov("pre-remove group"); 122 | } 123 | 124 | @Override 125 | public void preRemove(RealmModel realm, RoleModel role) { 126 | 127 | log.infov("pre-remove role"); 128 | } 129 | 130 | @Override 131 | public void close() { 132 | log.debugv("closing"); 133 | } 134 | 135 | @Override 136 | public UserModel getUserById(String id, RealmModel realm) { 137 | 138 | log.infov("lookup user by id: realm={0} userId={1}", realm.getId(), id); 139 | 140 | String externalId = StorageId.externalId(id); 141 | Map user = repository.findUserById(externalId); 142 | 143 | if (user == null) { 144 | log.debugv("findUserById returned null, skipping creation of UserAdapter, expect login error"); 145 | return null; 146 | } else { 147 | return new UserAdapter(session, realm, model, user, allowDatabaseToOverwriteKeycloak); 148 | } 149 | } 150 | 151 | @Override 152 | public UserModel getUserByUsername(String username, RealmModel realm) { 153 | 154 | log.infov("lookup user by username: realm={0} username={1}", realm.getId(), username); 155 | 156 | return repository.findUserByUsername(username).map(u -> new UserAdapter(session, realm, model, u, allowDatabaseToOverwriteKeycloak)).orElse(null); 157 | } 158 | 159 | @Override 160 | public UserModel getUserByEmail(String email, RealmModel realm) { 161 | 162 | log.infov("lookup user by username: realm={0} email={1}", realm.getId(), email); 163 | 164 | return getUserByUsername(email, realm); 165 | } 166 | 167 | @Override 168 | public int getUsersCount(RealmModel realm) { 169 | return repository.getUsersCount(null); 170 | } 171 | 172 | @Override 173 | public int getUsersCount(RealmModel realm, Set groupIds) { 174 | return repository.getUsersCount(null); 175 | } 176 | 177 | @Override 178 | public int getUsersCount(RealmModel realm, String search) { 179 | return repository.getUsersCount(search); 180 | } 181 | 182 | @Override 183 | public int getUsersCount(RealmModel realm, String search, Set groupIds) { 184 | return repository.getUsersCount(search); 185 | } 186 | 187 | @Override 188 | public int getUsersCount(RealmModel realm, Map params) { 189 | return repository.getUsersCount(null); 190 | } 191 | 192 | @Override 193 | public int getUsersCount(RealmModel realm, Map params, Set groupIds) { 194 | return repository.getUsersCount(null); 195 | } 196 | 197 | @Override 198 | public int getUsersCount(RealmModel realm, boolean includeServiceAccount) { 199 | return repository.getUsersCount(null); 200 | } 201 | 202 | @Override 203 | public List getUsers(RealmModel realm) { 204 | log.infov("list users: realm={0}", realm.getId()); 205 | return internalSearchForUser(null, realm, null); 206 | } 207 | 208 | @Override 209 | public List getUsers(RealmModel realm, int firstResult, int maxResults) { 210 | 211 | log.infov("list users: realm={0} firstResult={1} maxResults={2}", realm.getId(), firstResult, maxResults); 212 | return internalSearchForUser(null, realm, new PagingUtil.Pageable(firstResult, maxResults)); 213 | } 214 | 215 | @Override 216 | public List searchForUser(String search, RealmModel realm) { 217 | log.infov("search for users: realm={0} search={1}", realm.getId(), search); 218 | return internalSearchForUser(search, realm, null); 219 | } 220 | 221 | @Override 222 | public List searchForUser(String search, RealmModel realm, int firstResult, int maxResults) { 223 | log.infov("search for users: realm={0} search={1} firstResult={2} maxResults={3}", realm.getId(), search, firstResult, maxResults); 224 | return internalSearchForUser(search, realm, new PagingUtil.Pageable(firstResult, maxResults)); 225 | } 226 | 227 | @Override 228 | public List searchForUser(Map params, RealmModel realm) { 229 | log.infov("search for users with params: realm={0} params={1}", realm.getId(), params); 230 | return internalSearchForUser(params.values().stream().findFirst().orElse(null), realm, null); 231 | } 232 | 233 | private List internalSearchForUser(String search, RealmModel realm, PagingUtil.Pageable pageable) { 234 | return toUserModel(realm, repository.findUsers(search, pageable)); 235 | } 236 | 237 | @Override 238 | public List searchForUser(Map params, RealmModel realm, int firstResult, int maxResults) { 239 | log.infov("search for users with params: realm={0} params={1} firstResult={2} maxResults={3}", realm.getId(), params, firstResult, maxResults); 240 | return internalSearchForUser(params.values().stream().findFirst().orElse(null), realm, new PagingUtil.Pageable(firstResult, maxResults)); 241 | } 242 | 243 | @Override 244 | public List getGroupMembers(RealmModel realm, GroupModel group, int firstResult, int maxResults) { 245 | log.infov("search for group members with params: realm={0} groupId={1} firstResult={2} maxResults={3}", realm.getId(), group.getId(), firstResult, maxResults); 246 | return Collections.emptyList(); 247 | } 248 | 249 | @Override 250 | public List getGroupMembers(RealmModel realm, GroupModel group) { 251 | log.infov("search for group members: realm={0} groupId={1} firstResult={2} maxResults={3}", realm.getId(), group.getId()); 252 | return Collections.emptyList(); 253 | } 254 | 255 | @Override 256 | public List searchForUserByUserAttribute(String attrName, String attrValue, RealmModel realm) { 257 | log.infov("search for group members: realm={0} attrName={1} attrValue={2}", realm.getId(), attrName, attrValue); 258 | return Collections.emptyList(); 259 | } 260 | 261 | 262 | @Override 263 | public UserModel addUser(RealmModel realm, String username) { 264 | // from documentation: "If your provider has a configuration switch to turn off adding a user, returning null from this method will skip the provider and call the next one." 265 | return null; 266 | } 267 | 268 | 269 | @Override 270 | public boolean removeUser(RealmModel realm, UserModel user) { 271 | boolean userRemoved = repository.removeUser(); 272 | 273 | if (userRemoved) { 274 | log.infov("deleted keycloak user: realm={0} userId={1} username={2}", realm.getId(), user.getId(), user.getUsername()); 275 | } 276 | 277 | return userRemoved; 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /src/main/java/org/opensingular/dbuserprovider/DBUserStorageProviderFactory.java: -------------------------------------------------------------------------------- 1 | package org.opensingular.dbuserprovider; 2 | 3 | import com.google.auto.service.AutoService; 4 | import lombok.extern.jbosslog.JBossLog; 5 | import org.keycloak.Config; 6 | import org.keycloak.component.ComponentModel; 7 | import org.keycloak.component.ComponentValidationException; 8 | import org.keycloak.models.KeycloakSession; 9 | import org.keycloak.models.RealmModel; 10 | import org.keycloak.provider.ProviderConfigProperty; 11 | import org.keycloak.provider.ProviderConfigurationBuilder; 12 | import org.keycloak.storage.UserStorageProviderFactory; 13 | import org.opensingular.dbuserprovider.model.QueryConfigurations; 14 | import org.opensingular.dbuserprovider.persistence.DataSourceProvider; 15 | import org.opensingular.dbuserprovider.persistence.RDBMS; 16 | 17 | import java.util.HashMap; 18 | import java.util.List; 19 | import java.util.Map; 20 | 21 | @JBossLog 22 | @AutoService(UserStorageProviderFactory.class) 23 | public class DBUserStorageProviderFactory implements UserStorageProviderFactory { 24 | 25 | private static final String PARAMETER_PLACEHOLDER_HELP = "Use '?' as parameter placeholder character (replaced only once). "; 26 | private static final String DEFAULT_HELP_TEXT = "Select to query all users you must return at least: \"id\". " + 27 | " \"username\"," + 28 | " \"email\" (optional)," + 29 | " \"firstName\" (optional)," + 30 | " \"lastName\" (optional). Any other parameter can be mapped by aliases to a realm scope"; 31 | private static final String PARAMETER_HELP = " The %s is passed as query parameter."; 32 | 33 | 34 | private Map providerConfigPerInstance = new HashMap<>(); 35 | 36 | @Override 37 | public void init(Config.Scope config) { 38 | } 39 | 40 | @Override 41 | public void close() { 42 | for (Map.Entry pc : providerConfigPerInstance.entrySet()) { 43 | pc.getValue().dataSourceProvider.close(); 44 | } 45 | } 46 | 47 | @Override 48 | public DBUserStorageProvider create(KeycloakSession session, ComponentModel model) { 49 | ProviderConfig providerConfig = providerConfigPerInstance.computeIfAbsent(model.getId(), s -> configure(model)); 50 | return new DBUserStorageProvider(session, model, providerConfig.dataSourceProvider, providerConfig.queryConfigurations); 51 | } 52 | 53 | private synchronized ProviderConfig configure(ComponentModel model) { 54 | log.infov("Creating configuration for model: id={0} name={1}", model.getId(), model.getName()); 55 | ProviderConfig providerConfig = new ProviderConfig(); 56 | String user = model.get("user"); 57 | String password = model.get("password"); 58 | String url = model.get("url"); 59 | RDBMS rdbms = RDBMS.getByDescription(model.get("rdbms")); 60 | providerConfig.dataSourceProvider.configure(url, rdbms, user, password, model.getName()); 61 | providerConfig.queryConfigurations = new QueryConfigurations( 62 | model.get("count"), 63 | model.get("listAll"), 64 | model.get("findById"), 65 | model.get("findByUsername"), 66 | model.get("findBySearchTerm"), 67 | model.get("findPasswordHash"), 68 | model.get("hashFunction"), 69 | rdbms, 70 | model.get("allowKeycloakDelete", false), 71 | model.get("allowDatabaseToOverwriteKeycloak", false) 72 | ); 73 | return providerConfig; 74 | } 75 | 76 | @Override 77 | public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model) throws ComponentValidationException { 78 | try { 79 | ProviderConfig old = providerConfigPerInstance.put(model.getId(), configure(model)); 80 | if (old != null) { 81 | old.dataSourceProvider.close(); 82 | } 83 | } catch (Exception e) { 84 | throw new ComponentValidationException(e.getMessage(), e); 85 | } 86 | } 87 | 88 | @Override 89 | public String getId() { 90 | return "singular-db-user-provider"; 91 | } 92 | 93 | @Override 94 | public List getConfigProperties() { 95 | return ProviderConfigurationBuilder.create() 96 | //DATABASE 97 | .property() 98 | .name("url") 99 | .label("JDBC URL") 100 | .helpText("JDBC Connection String") 101 | .type(ProviderConfigProperty.STRING_TYPE) 102 | .defaultValue("jdbc:jtds:sqlserver://server-name/database_name;instance=instance_name") 103 | .add() 104 | .property() 105 | .name("user") 106 | .label("JDBC Connection User") 107 | .helpText("JDBC Connection User") 108 | .type(ProviderConfigProperty.STRING_TYPE) 109 | .defaultValue("user") 110 | .add() 111 | .property() 112 | .name("password") 113 | .label("JDBC Connection Password") 114 | .helpText("JDBC Connection Password") 115 | .type(ProviderConfigProperty.PASSWORD) 116 | .defaultValue("password") 117 | .add() 118 | .property() 119 | .name("rdbms") 120 | .label("RDBMS") 121 | .helpText("Relational Database Management System") 122 | .type(ProviderConfigProperty.LIST_TYPE) 123 | .options(RDBMS.getAllDescriptions()) 124 | .defaultValue(RDBMS.SQL_SERVER.getDesc()) 125 | .add() 126 | .property() 127 | .name("allowKeycloakDelete") 128 | .label("Allow Keycloak's User Delete") 129 | .helpText("By default, clicking Delete on a user in Keycloak is not allowed. Activate this option to allow to Delete Keycloak's version of the user (does not touch the user record in the linked RDBMS), e.g. to clear synching issues and allow the user to be synced from scratch from the RDBMS on next use, in Production or for testing.") 130 | .type(ProviderConfigProperty.BOOLEAN_TYPE) 131 | .defaultValue("false") 132 | .add() 133 | .property() 134 | .name("allowDatabaseToOverwriteKeycloak") 135 | .label("Allow DB Attributes to Overwrite Keycloak") 136 | // Technical details for the following comment: we aggregate both the existing Keycloak version and the DB version of an attribute in a Set, but since e.g. email is not a list of values on the Keycloak User, the new email is never set on it. 137 | .helpText("By default, once a user is loaded in Keycloak, its attributes (e.g. 'email') stay as they are in Keycloak even if an attribute of the same name now returns a different value through the query. Activate this option to have all attributes set in the SQL query to always overwrite the existing user attributes in Keycloak (e.g. if Keycloak user has email 'test@test.com' but the query fetches a field named 'email' that has a value 'example@exemple.com', the Keycloak user will now have email attribute = 'example@exemple.com'). This behavior works with NO_CAHCE configuration. In case you set this flag under a cached configuration, the user attributes will be reload if: 1) the cached value is older than 500ms and 2) username or e-mail does not match cached values.") 138 | .type(ProviderConfigProperty.BOOLEAN_TYPE) 139 | .defaultValue("false") 140 | .add() 141 | 142 | //QUERIES 143 | 144 | .property() 145 | .name("count") 146 | .label("User count SQL query") 147 | .helpText("SQL query returning the total count of users") 148 | .type(ProviderConfigProperty.STRING_TYPE) 149 | .defaultValue("select count(*) from users") 150 | .add() 151 | 152 | .property() 153 | .name("listAll") 154 | .label("List All Users SQL query") 155 | .helpText(DEFAULT_HELP_TEXT) 156 | .type(ProviderConfigProperty.STRING_TYPE) 157 | .defaultValue("select \"id\"," + 158 | " \"username\"," + 159 | " \"email\"," + 160 | " \"firstName\"," + 161 | " \"lastName\"," + 162 | " \"cpf\"," + 163 | " \"fullName\" from users ") 164 | .add() 165 | 166 | .property() 167 | .name("findById") 168 | .label("Find user by id SQL query") 169 | .helpText(DEFAULT_HELP_TEXT + String.format(PARAMETER_HELP, "user id") + PARAMETER_PLACEHOLDER_HELP) 170 | .type(ProviderConfigProperty.STRING_TYPE) 171 | .defaultValue("select \"id\"," + 172 | " \"username\"," + 173 | " \"email\"," + 174 | " \"firstName\"," + 175 | " \"lastName\"," + 176 | " \"cpf\"," + 177 | " \"fullName\" from users where \"id\" = ? ") 178 | .add() 179 | 180 | .property() 181 | .name("findByUsername") 182 | .label("Find user by username SQL query") 183 | .helpText(DEFAULT_HELP_TEXT + String.format(PARAMETER_HELP, "user username") + PARAMETER_PLACEHOLDER_HELP) 184 | .type(ProviderConfigProperty.STRING_TYPE) 185 | .defaultValue("select \"id\"," + 186 | " \"username\"," + 187 | " \"email\"," + 188 | " \"firstName\"," + 189 | " \"lastName\"," + 190 | " \"cpf\"," + 191 | " \"fullName\" from users where \"username\" = ? ") 192 | .add() 193 | 194 | .property() 195 | .name("findBySearchTerm") 196 | .label("Find user by search term SQL query") 197 | .helpText(DEFAULT_HELP_TEXT + String.format(PARAMETER_HELP, "search term") + PARAMETER_PLACEHOLDER_HELP) 198 | .type(ProviderConfigProperty.STRING_TYPE) 199 | .defaultValue("select \"id\"," + 200 | " \"username\"," + 201 | " \"email\"," + 202 | " \"firstName\"," + 203 | " \"lastName\"," + 204 | " \"cpf\"," + 205 | " \"fullName\" from users where upper(\"username\") like (?) or upper(\"email\") like (?) or upper(\"fullName\") like (?)") 206 | .add() 207 | 208 | .property() 209 | .name("findPasswordHash") 210 | .label("Find password hash (blowfish or hash digest hex) SQL query") 211 | .helpText(DEFAULT_HELP_TEXT + String.format(PARAMETER_HELP, "user username") + PARAMETER_PLACEHOLDER_HELP) 212 | .type(ProviderConfigProperty.STRING_TYPE) 213 | .defaultValue("select hash_pwd from users where \"username\" = ? ") 214 | .add() 215 | .property() 216 | .name("hashFunction") 217 | .label("Password hash function") 218 | .helpText("Hash type used to match passwrod (md* e sha* uses hex hash digest)") 219 | .type(ProviderConfigProperty.LIST_TYPE) 220 | .options("Blowfish (bcrypt)", "MD2", "MD5", "SHA-1", "SHA-256", "SHA3-224", "SHA3-256", "SHA3-384", "SHA3-512", "SHA-384", "SHA-512/224", "SHA-512/256", "SHA-512", "PBKDF2-SHA256") 221 | .defaultValue("SHA-1") 222 | .add() 223 | .build(); 224 | } 225 | 226 | private static class ProviderConfig { 227 | private DataSourceProvider dataSourceProvider = new DataSourceProvider(); 228 | private QueryConfigurations queryConfigurations; 229 | } 230 | 231 | 232 | } 233 | -------------------------------------------------------------------------------- /src/main/java/org/opensingular/dbuserprovider/model/QueryConfigurations.java: -------------------------------------------------------------------------------- 1 | package org.opensingular.dbuserprovider.model; 2 | 3 | import org.opensingular.dbuserprovider.persistence.RDBMS; 4 | 5 | public class QueryConfigurations { 6 | 7 | private String count; 8 | private String listAll; 9 | private String findById; 10 | private String findByUsername; 11 | private String findBySearchTerm; 12 | private String findPasswordHash; 13 | private String hashFunction; 14 | private RDBMS RDBMS; 15 | private boolean allowKeycloakDelete; 16 | private boolean allowDatabaseToOverwriteKeycloak; 17 | 18 | public QueryConfigurations(String count, String listAll, String findById, String findByUsername, String findBySearchTerm, String findPasswordHash, String hashFunction, RDBMS RDBMS, boolean allowKeycloakDelete, boolean allowDatabaseToOverwriteKeycloak) { 19 | this.count = count; 20 | this.listAll = listAll; 21 | this.findById = findById; 22 | this.findByUsername = findByUsername; 23 | this.findBySearchTerm = findBySearchTerm; 24 | this.findPasswordHash = findPasswordHash; 25 | this.hashFunction = hashFunction; 26 | this.RDBMS = RDBMS; 27 | this.allowKeycloakDelete = allowKeycloakDelete; 28 | this.allowDatabaseToOverwriteKeycloak = allowDatabaseToOverwriteKeycloak; 29 | } 30 | 31 | public RDBMS getRDBMS() { 32 | return RDBMS; 33 | } 34 | 35 | public String getCount() { 36 | return count; 37 | } 38 | 39 | public String getListAll() { 40 | return listAll; 41 | } 42 | 43 | public String getFindById() { 44 | return findById; 45 | } 46 | 47 | public String getFindByUsername() { 48 | return findByUsername; 49 | } 50 | 51 | public String getFindBySearchTerm() { 52 | return findBySearchTerm; 53 | } 54 | 55 | public String getFindPasswordHash() { 56 | return findPasswordHash; 57 | } 58 | 59 | public String getHashFunction() { 60 | return hashFunction; 61 | } 62 | 63 | public boolean isBlowfish() { 64 | return hashFunction.toLowerCase().contains("blowfish"); 65 | } 66 | 67 | public boolean getAllowKeycloakDelete() { 68 | return allowKeycloakDelete; 69 | } 70 | 71 | public boolean getAllowDatabaseToOverwriteKeycloak() { 72 | return allowDatabaseToOverwriteKeycloak; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/org/opensingular/dbuserprovider/model/UserAdapter.java: -------------------------------------------------------------------------------- 1 | package org.opensingular.dbuserprovider.model; 2 | 3 | import lombok.extern.jbosslog.JBossLog; 4 | import org.apache.commons.lang3.StringUtils; 5 | import org.keycloak.component.ComponentModel; 6 | import org.keycloak.models.KeycloakSession; 7 | import org.keycloak.models.RealmModel; 8 | import org.keycloak.storage.StorageId; 9 | import org.keycloak.storage.adapter.AbstractUserAdapterFederatedStorage; 10 | 11 | import java.util.HashSet; 12 | import java.util.List; 13 | import java.util.Map; 14 | import java.util.Map.Entry; 15 | import java.util.Objects; 16 | import java.util.Set; 17 | import java.util.stream.Collectors; 18 | 19 | @JBossLog 20 | public class UserAdapter extends AbstractUserAdapterFederatedStorage { 21 | 22 | private final String keycloakId; 23 | private String username; 24 | 25 | public UserAdapter(KeycloakSession session, RealmModel realm, ComponentModel model, Map data, boolean allowDatabaseToOverwriteKeycloak) { 26 | super(session, realm, model); 27 | this.keycloakId = StorageId.keycloakId(model, data.get("id")); 28 | this.username = data.get("username"); 29 | try { 30 | Map> attributes = this.getAttributes(); 31 | for (Entry e : data.entrySet()) { 32 | Set newValues = new HashSet<>(); 33 | if (!allowDatabaseToOverwriteKeycloak) { 34 | List attribute = attributes.get(e.getKey()); 35 | if (attribute != null) { 36 | newValues.addAll(attribute); 37 | } 38 | } 39 | newValues.add(StringUtils.trimToNull(e.getValue())); 40 | this.setAttribute(e.getKey(), newValues.stream().filter(Objects::nonNull).collect(Collectors.toList())); 41 | } 42 | } catch(Exception e) { 43 | log.errorv(e, "UserAdapter constructor, username={0}", this.username); 44 | } 45 | } 46 | 47 | 48 | @Override 49 | public String getId() { 50 | return keycloakId; 51 | } 52 | 53 | @Override 54 | public String getUsername() { 55 | return username; 56 | } 57 | 58 | @Override 59 | public void setUsername(String username) { 60 | this.username = username; 61 | } 62 | 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/org/opensingular/dbuserprovider/persistence/DataSourceProvider.java: -------------------------------------------------------------------------------- 1 | package org.opensingular.dbuserprovider.persistence; 2 | 3 | 4 | import com.zaxxer.hikari.HikariConfig; 5 | import com.zaxxer.hikari.HikariDataSource; 6 | import lombok.extern.jbosslog.JBossLog; 7 | import org.apache.commons.lang3.StringUtils; 8 | 9 | import javax.sql.DataSource; 10 | import java.io.Closeable; 11 | import java.text.SimpleDateFormat; 12 | import java.util.Date; 13 | import java.util.Optional; 14 | import java.util.concurrent.ExecutorService; 15 | import java.util.concurrent.Executors; 16 | 17 | @JBossLog 18 | public class DataSourceProvider implements Closeable { 19 | 20 | private static final SimpleDateFormat SIMPLE_DATE_FORMAT = new SimpleDateFormat("dd-MM-YYYY HH:mm:ss"); 21 | private ExecutorService executor = Executors.newFixedThreadPool(1); 22 | private HikariDataSource hikariDataSource; 23 | 24 | public DataSourceProvider() { 25 | } 26 | 27 | 28 | synchronized Optional getDataSource() { 29 | return Optional.ofNullable(hikariDataSource); 30 | } 31 | 32 | 33 | public void configure(String url, RDBMS rdbms, String user, String pass, String name) { 34 | HikariConfig hikariConfig = new HikariConfig(); 35 | hikariConfig.setUsername(user); 36 | hikariConfig.setPassword(pass); 37 | hikariConfig.setPoolName(StringUtils.capitalize("SINGULAR-USER-PROVIDER-" + name + SIMPLE_DATE_FORMAT.format(new Date()))); 38 | hikariConfig.setJdbcUrl(url); 39 | hikariConfig.setConnectionTestQuery(rdbms.getTestString()); 40 | hikariConfig.setDriverClassName(rdbms.getDriver()); 41 | HikariDataSource newDS = new HikariDataSource(hikariConfig); 42 | newDS.validate(); 43 | HikariDataSource old = this.hikariDataSource; 44 | this.hikariDataSource = newDS; 45 | disposeOldDataSource(old); 46 | } 47 | 48 | private void disposeOldDataSource(HikariDataSource old) { 49 | executor.submit(() -> { 50 | try { 51 | if (old != null) { 52 | old.close(); 53 | } 54 | } catch (Exception e) { 55 | log.error(e.getMessage(), e); 56 | } 57 | }); 58 | } 59 | 60 | @Override 61 | public void close() { 62 | executor.shutdownNow(); 63 | if (hikariDataSource != null) { 64 | hikariDataSource.close(); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/org/opensingular/dbuserprovider/persistence/RDBMS.java: -------------------------------------------------------------------------------- 1 | package org.opensingular.dbuserprovider.persistence; 2 | 3 | 4 | import org.hibernate.dialect.Dialect; 5 | import org.hibernate.dialect.MySQL57Dialect; 6 | import org.hibernate.dialect.Oracle12cDialect; 7 | import org.hibernate.dialect.PostgreSQL10Dialect; 8 | import org.hibernate.dialect.SQLServer2012Dialect; 9 | import org.hibernate.dialect.DB2Dialect; 10 | 11 | import java.util.Arrays; 12 | import java.util.List; 13 | import java.util.stream.Collectors; 14 | 15 | public enum RDBMS { 16 | POSTGRESQL("PostgreSQL 10+", org.postgresql.Driver.class.getName(), "SELECT 1", new PostgreSQL10Dialect()), 17 | MYSQL("MySQL 5.7+", com.mysql.cj.jdbc.Driver.class.getName(), "SELECT 1", new MySQL57Dialect()), 18 | ORACLE("Oracle 12+", oracle.jdbc.OracleDriver.class.getName(), "SELECT 1 FROM DUAL", new Oracle12cDialect()), 19 | IBMDB2("IBM DB2", com.ibm.db2.jcc.DB2Driver.class.getName(), "select * from sysibm.sysdummy1", new DB2Dialect()), 20 | SQL_SERVER("MS SQL Server 2012+ (jtds)", net.sourceforge.jtds.jdbc.Driver.class.getName(), "SELECT 1", new SQLServer2012Dialect()); 21 | 22 | private final String desc; 23 | private final String driver; 24 | private final String testString; 25 | private final Dialect dialect; 26 | 27 | RDBMS(String desc, String driver, String testString, Dialect dialect) { 28 | this.desc = desc; 29 | this.driver = driver; 30 | this.testString = testString; 31 | this.dialect = dialect; 32 | } 33 | 34 | public static RDBMS getByDescription(String desc) { 35 | for (RDBMS value : values()) { 36 | if (value.desc.equals(desc)) { 37 | return value; 38 | } 39 | } 40 | return null; 41 | } 42 | 43 | public Dialect getDialect() { 44 | return dialect; 45 | } 46 | 47 | public static List getAllDescriptions() { 48 | return Arrays.stream(values()).map(RDBMS::getDesc).collect(Collectors.toList()); 49 | } 50 | 51 | public String getDesc() { 52 | return desc; 53 | } 54 | 55 | public String getDriver() { 56 | return driver; 57 | } 58 | 59 | public String getTestString() { 60 | return testString; 61 | } 62 | 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/org/opensingular/dbuserprovider/persistence/UserRepository.java: -------------------------------------------------------------------------------- 1 | package org.opensingular.dbuserprovider.persistence; 2 | 3 | import lombok.extern.jbosslog.JBossLog; 4 | import org.apache.commons.codec.binary.Hex; 5 | import org.apache.commons.codec.binary.StringUtils; 6 | import org.apache.commons.codec.digest.DigestUtils; 7 | import org.apache.commons.lang3.NotImplementedException; 8 | import at.favre.lib.crypto.bcrypt.BCrypt; 9 | import org.opensingular.dbuserprovider.DBUserStorageException; 10 | import org.opensingular.dbuserprovider.model.QueryConfigurations; 11 | import org.opensingular.dbuserprovider.util.PBKDF2SHA256HashingUtil; 12 | import org.opensingular.dbuserprovider.util.PagingUtil; 13 | import org.opensingular.dbuserprovider.util.PagingUtil.Pageable; 14 | 15 | import javax.sql.DataSource; 16 | import java.security.MessageDigest; 17 | import java.sql.Connection; 18 | import java.sql.PreparedStatement; 19 | import java.sql.ResultSet; 20 | import java.sql.SQLException; 21 | import java.util.*; 22 | import java.util.function.Function; 23 | 24 | 25 | @JBossLog 26 | public class UserRepository { 27 | 28 | 29 | private DataSourceProvider dataSourceProvider; 30 | private QueryConfigurations queryConfigurations; 31 | 32 | public UserRepository(DataSourceProvider dataSourceProvider, QueryConfigurations queryConfigurations) { 33 | this.dataSourceProvider = dataSourceProvider; 34 | this.queryConfigurations = queryConfigurations; 35 | } 36 | 37 | 38 | private T doQuery(String query, Pageable pageable, Function resultTransformer, Object... params) { 39 | Optional dataSourceOpt = dataSourceProvider.getDataSource(); 40 | if (dataSourceOpt.isPresent()) { 41 | DataSource dataSource = dataSourceOpt.get(); 42 | try (Connection c = dataSource.getConnection()) { 43 | if (pageable != null) { 44 | query = PagingUtil.formatScriptWithPageable(query, pageable, queryConfigurations.getRDBMS()); 45 | } 46 | log.infov("Query: {0} params: {1} ", query, Arrays.toString(params)); 47 | try (PreparedStatement statement = c.prepareStatement(query)) { 48 | if (params != null) { 49 | for (int i = 1; i <= params.length; i++) { 50 | statement.setObject(i, params[i - 1]); 51 | } 52 | } 53 | try (ResultSet rs = statement.executeQuery()) { 54 | return resultTransformer.apply(rs); 55 | } 56 | } 57 | } catch (SQLException e) { 58 | log.error(e.getMessage(), e); 59 | } 60 | return null; 61 | } 62 | return null; 63 | } 64 | 65 | private List> readMap(ResultSet rs) { 66 | try { 67 | List> data = new ArrayList<>(); 68 | Set columnsFound = new HashSet<>(); 69 | for (int i = 1; i <= rs.getMetaData().getColumnCount(); i++) { 70 | String columnLabel = rs.getMetaData().getColumnLabel(i); 71 | columnsFound.add(columnLabel); 72 | } 73 | while (rs.next()) { 74 | Map result = new HashMap<>(); 75 | for (String col : columnsFound) { 76 | result.put(col, rs.getString(col)); 77 | } 78 | data.add(result); 79 | } 80 | return data; 81 | } catch (Exception e) { 82 | throw new DBUserStorageException(e.getMessage(), e); 83 | } 84 | } 85 | 86 | 87 | private Integer readInt(ResultSet rs) { 88 | try { 89 | return rs.next() ? rs.getInt(1) : null; 90 | } catch (Exception e) { 91 | throw new DBUserStorageException(e.getMessage(), e); 92 | } 93 | } 94 | 95 | private Boolean readBoolean(ResultSet rs) { 96 | try { 97 | return rs.next() ? rs.getBoolean(1) : null; 98 | } catch (Exception e) { 99 | throw new DBUserStorageException(e.getMessage(), e); 100 | } 101 | } 102 | 103 | private String readString(ResultSet rs) { 104 | try { 105 | return rs.next() ? rs.getString(1) : null; 106 | } catch (Exception e) { 107 | throw new DBUserStorageException(e.getMessage(), e); 108 | } 109 | } 110 | 111 | public List> getAllUsers() { 112 | return doQuery(queryConfigurations.getListAll(), null, this::readMap); 113 | } 114 | 115 | public int getUsersCount(String search) { 116 | if (search == null || search.isEmpty()) { 117 | return Optional.ofNullable(doQuery(queryConfigurations.getCount(), null, this::readInt)).orElse(0); 118 | } else { 119 | String query = String.format("select count(*) from (%s) count", queryConfigurations.getFindBySearchTerm()); 120 | return Optional.ofNullable(doQuery(query, null, this::readInt, search)).orElse(0); 121 | } 122 | } 123 | 124 | 125 | public Map findUserById(String id) { 126 | return Optional.ofNullable(doQuery(queryConfigurations.getFindById(), null, this::readMap, id)) 127 | .orElse(Collections.emptyList()) 128 | .stream().findFirst().orElse(null); 129 | } 130 | 131 | public Optional> findUserByUsername(String username) { 132 | return Optional.ofNullable(doQuery(queryConfigurations.getFindByUsername(), null, this::readMap, username)) 133 | .orElse(Collections.emptyList()) 134 | .stream().findFirst(); 135 | } 136 | 137 | public List> findUsers(String search, PagingUtil.Pageable pageable) { 138 | if (search == null || search.isEmpty()) { 139 | return doQuery(queryConfigurations.getListAll(), pageable, this::readMap); 140 | } 141 | return doQuery(queryConfigurations.getFindBySearchTerm(), pageable, this::readMap, search); 142 | } 143 | 144 | public boolean validateCredentials(String username, String password) { 145 | String hash = Optional.ofNullable(doQuery(queryConfigurations.getFindPasswordHash(), null, this::readString, username)).orElse(""); 146 | if (queryConfigurations.isBlowfish()) { 147 | return !hash.isEmpty() && BCrypt.verifyer().verify(password.toCharArray(), hash).verified; 148 | } else { 149 | String hashFunction = queryConfigurations.getHashFunction(); 150 | 151 | if(hashFunction.equals("PBKDF2-SHA256")){ 152 | String[] components = hash.split("\\$"); 153 | return new PBKDF2SHA256HashingUtil(password, components[2], Integer.valueOf(components[1])).validatePassword(components[3]); 154 | } 155 | 156 | MessageDigest digest = DigestUtils.getDigest(hashFunction); 157 | byte[] pwdBytes = StringUtils.getBytesUtf8(password); 158 | return Objects.equals(Hex.encodeHexString(digest.digest(pwdBytes)), hash); 159 | } 160 | } 161 | 162 | public boolean updateCredentials(String username, String password) { 163 | throw new NotImplementedException("Password update not supported"); 164 | } 165 | 166 | public boolean removeUser() { 167 | return queryConfigurations.getAllowKeycloakDelete(); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/main/java/org/opensingular/dbuserprovider/util/PBKDF2SHA256HashingUtil.java: -------------------------------------------------------------------------------- 1 | package org.opensingular.dbuserprovider.util; 2 | 3 | import java.util.Base64; 4 | import java.util.Objects; 5 | 6 | import javax.crypto.SecretKey; 7 | import javax.crypto.SecretKeyFactory; 8 | import javax.crypto.spec.PBEKeySpec; 9 | 10 | public class PBKDF2SHA256HashingUtil { 11 | 12 | private char[] password; 13 | private byte[] salt; 14 | private int iterations; 15 | private static final int keyLength = 256; 16 | /** 17 | * @param password 18 | * @param salt 19 | * @param iterations 20 | */ 21 | public PBKDF2SHA256HashingUtil(String password, String salt, int iterations){ 22 | this.password = password.toCharArray(); 23 | this.salt = salt.getBytes(); 24 | this.iterations = iterations; 25 | } 26 | 27 | public boolean validatePassword(String passwordHash){ 28 | return Objects.equals(passwordHash, hashPassword()); 29 | } 30 | 31 | private String hashPassword(){ 32 | try { 33 | SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); 34 | PBEKeySpec spec = new PBEKeySpec(this.password, this.salt, this.iterations, keyLength); 35 | SecretKey key = skf.generateSecret(spec); 36 | return Base64.getEncoder().encodeToString(key.getEncoded()); 37 | } catch (Exception e) { 38 | return ""; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/org/opensingular/dbuserprovider/util/PagingUtil.java: -------------------------------------------------------------------------------- 1 | package org.opensingular.dbuserprovider.util; 2 | 3 | import org.hibernate.dialect.Dialect; 4 | import org.hibernate.dialect.pagination.LimitHandler; 5 | import org.hibernate.engine.spi.RowSelection; 6 | import org.opensingular.dbuserprovider.DBUserStorageException; 7 | import org.opensingular.dbuserprovider.persistence.RDBMS; 8 | 9 | import java.sql.SQLException; 10 | import java.util.Map; 11 | import java.util.regex.Matcher; 12 | import java.util.regex.Pattern; 13 | 14 | public class PagingUtil { 15 | 16 | @SuppressWarnings("RegExpRedundantEscape") 17 | private static final Pattern SINGLE_QUESTION_MARK_REGEX = Pattern.compile("(^|[^\\?])(\\?)([^\\?]|$)"); 18 | 19 | 20 | public static class Pageable { 21 | private final int firstResult; 22 | private final int maxResults; 23 | 24 | public Pageable(int firstResult, int maxResults) { 25 | this.firstResult = firstResult; 26 | this.maxResults = maxResults; 27 | } 28 | } 29 | 30 | public static String formatScriptWithPageable(String query, Pageable pageable, RDBMS RDBMS) { 31 | 32 | final Dialect dialect = RDBMS.getDialect(); 33 | 34 | RowSelection rowSelection = new RowSelection(); 35 | rowSelection.setFetchSize(pageable.maxResults); 36 | rowSelection.setFirstRow(pageable.firstResult); 37 | rowSelection.setMaxRows(pageable.maxResults); 38 | 39 | String escapedSQL = escapeQuestionMarks(query); 40 | 41 | StringBuilder processedSQL; 42 | try { 43 | LimitHandler limitHandler = dialect.getLimitHandler(); 44 | processedSQL = new StringBuilder(limitHandler.processSql(escapedSQL, rowSelection)); 45 | int col = 1; 46 | PreparedStatementParameterCollector collector = new PreparedStatementParameterCollector(); 47 | col += limitHandler.bindLimitParametersAtStartOfQuery(rowSelection, collector, col); 48 | limitHandler.bindLimitParametersAtEndOfQuery(rowSelection, collector, col); 49 | 50 | Map parameters = collector.getParameters(); 51 | for (int i = 1; i <= parameters.keySet().size(); i++) { 52 | Matcher matcher = SINGLE_QUESTION_MARK_REGEX.matcher(processedSQL); 53 | if (matcher.find()) { 54 | String str = String.valueOf(parameters.get(i)); 55 | processedSQL.replace(matcher.start(2), matcher.end(2), str); 56 | } 57 | } 58 | return unescapeQuestionMarks(processedSQL.toString()); 59 | 60 | } catch (SQLException e) { 61 | throw new DBUserStorageException(e.getMessage(), e); 62 | } 63 | } 64 | 65 | 66 | private static String unescapeQuestionMarks(String sql) { 67 | return sql.replaceAll("\\?\\?", "?"); 68 | } 69 | 70 | private static String escapeQuestionMarks(String sql) { 71 | return sql.replaceAll("\\?", "??"); 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/org/opensingular/dbuserprovider/util/PreparedStatementParameterCollector.java: -------------------------------------------------------------------------------- 1 | package org.opensingular.dbuserprovider.util; 2 | 3 | import java.io.InputStream; 4 | import java.io.Reader; 5 | import java.math.BigDecimal; 6 | import java.net.URL; 7 | import java.sql.Array; 8 | import java.sql.Blob; 9 | import java.sql.Clob; 10 | import java.sql.Connection; 11 | import java.sql.Date; 12 | import java.sql.NClob; 13 | import java.sql.ParameterMetaData; 14 | import java.sql.PreparedStatement; 15 | import java.sql.Ref; 16 | import java.sql.ResultSet; 17 | import java.sql.ResultSetMetaData; 18 | import java.sql.RowId; 19 | import java.sql.SQLWarning; 20 | import java.sql.SQLXML; 21 | import java.sql.Time; 22 | import java.sql.Timestamp; 23 | import java.util.Calendar; 24 | import java.util.HashMap; 25 | import java.util.Map; 26 | 27 | public class PreparedStatementParameterCollector implements PreparedStatement { 28 | 29 | private Map parameters = new HashMap<>(); 30 | 31 | Map getParameters() { 32 | return parameters; 33 | } 34 | 35 | @Override 36 | public ResultSet executeQuery() { 37 | return null; 38 | } 39 | 40 | @Override 41 | public int executeUpdate() { 42 | return 0; 43 | } 44 | 45 | @Override 46 | public void setNull(int parameterIndex, int sqlType) { 47 | parameters.put(parameterIndex, null); 48 | } 49 | 50 | @Override 51 | public void setBoolean(int parameterIndex, boolean x) { 52 | parameters.put(parameterIndex, x); 53 | } 54 | 55 | @Override 56 | public void setByte(int parameterIndex, byte x) { 57 | parameters.put(parameterIndex, x); 58 | } 59 | 60 | @Override 61 | public void setShort(int parameterIndex, short x) { 62 | parameters.put(parameterIndex, x); 63 | } 64 | 65 | @Override 66 | public void setInt(int parameterIndex, int x) { 67 | parameters.put(parameterIndex, x); 68 | } 69 | 70 | @Override 71 | public void setLong(int parameterIndex, long x) { 72 | parameters.put(parameterIndex, x); 73 | } 74 | 75 | @Override 76 | public void setFloat(int parameterIndex, float x) { 77 | parameters.put(parameterIndex, x); 78 | } 79 | 80 | @Override 81 | public void setDouble(int parameterIndex, double x) { 82 | parameters.put(parameterIndex, x); 83 | } 84 | 85 | @Override 86 | public void setBigDecimal(int parameterIndex, BigDecimal x) { 87 | parameters.put(parameterIndex, x); 88 | } 89 | 90 | @Override 91 | public void setString(int parameterIndex, String x) { 92 | parameters.put(parameterIndex, x); 93 | } 94 | 95 | @Override 96 | public void setBytes(int parameterIndex, byte[] x) { 97 | parameters.put(parameterIndex, x); 98 | } 99 | 100 | @Override 101 | public void setDate(int parameterIndex, Date x) { 102 | parameters.put(parameterIndex, x); 103 | } 104 | 105 | @Override 106 | public void setTime(int parameterIndex, Time x) { 107 | parameters.put(parameterIndex, x); 108 | } 109 | 110 | @Override 111 | public void setTimestamp(int parameterIndex, Timestamp x) { 112 | parameters.put(parameterIndex, x); 113 | } 114 | 115 | @Override 116 | public void setAsciiStream(int parameterIndex, InputStream x, int length) { 117 | parameters.put(parameterIndex, x); 118 | } 119 | 120 | @Override 121 | public void setUnicodeStream(int parameterIndex, InputStream x, int length) { 122 | parameters.put(parameterIndex, x); 123 | } 124 | 125 | @Override 126 | public void setBinaryStream(int parameterIndex, InputStream x, int length) { 127 | parameters.put(parameterIndex, x); 128 | } 129 | 130 | @Override 131 | public void clearParameters() { 132 | parameters.clear(); 133 | } 134 | 135 | @Override 136 | public void setObject(int parameterIndex, Object x, int targetSqlType) { 137 | parameters.put(parameterIndex, x); 138 | } 139 | 140 | @Override 141 | public void setObject(int parameterIndex, Object x) { 142 | parameters.put(parameterIndex, x); 143 | } 144 | 145 | @Override 146 | public boolean execute() { 147 | return false; 148 | } 149 | 150 | @Override 151 | public void addBatch() { 152 | 153 | } 154 | 155 | @Override 156 | public void setCharacterStream(int parameterIndex, Reader reader, int length) { 157 | parameters.put(parameterIndex, reader); 158 | } 159 | 160 | @Override 161 | public void setRef(int parameterIndex, Ref x) { 162 | parameters.put(parameterIndex, x); 163 | } 164 | 165 | @Override 166 | public void setBlob(int parameterIndex, Blob x) { 167 | parameters.put(parameterIndex, x); 168 | } 169 | 170 | @Override 171 | public void setClob(int parameterIndex, Clob x) { 172 | parameters.put(parameterIndex, x); 173 | } 174 | 175 | @Override 176 | public void setArray(int parameterIndex, Array x) { 177 | parameters.put(parameterIndex, x); 178 | } 179 | 180 | @Override 181 | public ResultSetMetaData getMetaData() { 182 | return null; 183 | } 184 | 185 | @Override 186 | public void setDate(int parameterIndex, Date x, Calendar cal) { 187 | parameters.put(parameterIndex, x); 188 | } 189 | 190 | @Override 191 | public void setTime(int parameterIndex, Time x, Calendar cal) { 192 | parameters.put(parameterIndex, x); 193 | } 194 | 195 | @Override 196 | public void setTimestamp(int parameterIndex, Timestamp x, Calendar cal) { 197 | parameters.put(parameterIndex, x); 198 | } 199 | 200 | @Override 201 | public void setNull(int parameterIndex, int sqlType, String typeName) { 202 | parameters.put(parameterIndex, null); 203 | } 204 | 205 | @Override 206 | public void setURL(int parameterIndex, URL x) { 207 | parameters.put(parameterIndex, x); 208 | } 209 | 210 | @Override 211 | public ParameterMetaData getParameterMetaData() { 212 | return null; 213 | } 214 | 215 | @Override 216 | public void setRowId(int parameterIndex, RowId x) { 217 | parameters.put(parameterIndex, x); 218 | } 219 | 220 | @Override 221 | public void setNString(int parameterIndex, String value) { 222 | parameters.put(parameterIndex, value); 223 | } 224 | 225 | @Override 226 | public void setNCharacterStream(int parameterIndex, Reader value, long length) { 227 | parameters.put(parameterIndex, value); 228 | } 229 | 230 | @Override 231 | public void setNClob(int parameterIndex, NClob value) { 232 | parameters.put(parameterIndex, value); 233 | } 234 | 235 | @Override 236 | public void setClob(int parameterIndex, Reader reader, long length) { 237 | parameters.put(parameterIndex, reader); 238 | } 239 | 240 | @Override 241 | public void setBlob(int parameterIndex, InputStream inputStream, long length) { 242 | parameters.put(parameterIndex, inputStream); 243 | } 244 | 245 | @Override 246 | public void setNClob(int parameterIndex, Reader reader, long length) { 247 | parameters.put(parameterIndex, reader); 248 | } 249 | 250 | @Override 251 | public void setSQLXML(int parameterIndex, SQLXML xmlObject) { 252 | parameters.put(parameterIndex, xmlObject); 253 | } 254 | 255 | @Override 256 | public void setObject(int parameterIndex, Object x, int targetSqlType, int scaleOrLength) { 257 | parameters.put(parameterIndex, x); 258 | } 259 | 260 | @Override 261 | public void setAsciiStream(int parameterIndex, InputStream x, long length) { 262 | parameters.put(parameterIndex, x); 263 | } 264 | 265 | @Override 266 | public void setBinaryStream(int parameterIndex, InputStream x, long length) { 267 | parameters.put(parameterIndex, x); 268 | } 269 | 270 | @Override 271 | public void setCharacterStream(int parameterIndex, Reader reader, long length) { 272 | parameters.put(parameterIndex, reader); 273 | } 274 | 275 | @Override 276 | public void setAsciiStream(int parameterIndex, InputStream x) { 277 | parameters.put(parameterIndex, x); 278 | } 279 | 280 | @Override 281 | public void setBinaryStream(int parameterIndex, InputStream x) { 282 | parameters.put(parameterIndex, x); 283 | } 284 | 285 | @Override 286 | public void setCharacterStream(int parameterIndex, Reader reader) { 287 | parameters.put(parameterIndex, reader); 288 | } 289 | 290 | @Override 291 | public void setNCharacterStream(int parameterIndex, Reader value) { 292 | parameters.put(parameterIndex, value); 293 | } 294 | 295 | @Override 296 | public void setClob(int parameterIndex, Reader reader) { 297 | parameters.put(parameterIndex, reader); 298 | } 299 | 300 | @Override 301 | public void setBlob(int parameterIndex, InputStream inputStream) { 302 | parameters.put(parameterIndex, inputStream); 303 | } 304 | 305 | @Override 306 | public void setNClob(int parameterIndex, Reader reader) { 307 | parameters.put(parameterIndex, reader); 308 | } 309 | 310 | @Override 311 | public ResultSet executeQuery(String sql) { 312 | return null; 313 | } 314 | 315 | @Override 316 | public int executeUpdate(String sql) { 317 | return 0; 318 | } 319 | 320 | @Override 321 | public void close() { 322 | 323 | } 324 | 325 | @Override 326 | public int getMaxFieldSize() { 327 | return 0; 328 | } 329 | 330 | @Override 331 | public void setMaxFieldSize(int max) { 332 | 333 | } 334 | 335 | @Override 336 | public int getMaxRows() { 337 | return 0; 338 | } 339 | 340 | @Override 341 | public void setMaxRows(int max) { 342 | 343 | } 344 | 345 | @Override 346 | public void setEscapeProcessing(boolean enable) { 347 | 348 | } 349 | 350 | @Override 351 | public int getQueryTimeout() { 352 | return 0; 353 | } 354 | 355 | @Override 356 | public void setQueryTimeout(int seconds) { 357 | 358 | } 359 | 360 | @Override 361 | public void cancel() { 362 | 363 | } 364 | 365 | @Override 366 | public SQLWarning getWarnings() { 367 | return null; 368 | } 369 | 370 | @Override 371 | public void clearWarnings() { 372 | 373 | } 374 | 375 | @Override 376 | public void setCursorName(String name) { 377 | 378 | } 379 | 380 | @Override 381 | public boolean execute(String sql) { 382 | return false; 383 | } 384 | 385 | @Override 386 | public ResultSet getResultSet() { 387 | return null; 388 | } 389 | 390 | @Override 391 | public int getUpdateCount() { 392 | return 0; 393 | } 394 | 395 | @Override 396 | public boolean getMoreResults() { 397 | return false; 398 | } 399 | 400 | @Override 401 | public void setFetchDirection(int direction) { 402 | 403 | } 404 | 405 | @Override 406 | public int getFetchDirection() { 407 | //noinspection MagicConstant 408 | return 0; 409 | } 410 | 411 | @Override 412 | public void setFetchSize(int rows) { 413 | 414 | } 415 | 416 | @Override 417 | public int getFetchSize() { 418 | return 0; 419 | } 420 | 421 | @Override 422 | public int getResultSetConcurrency() { 423 | //noinspection MagicConstant 424 | return 0; 425 | } 426 | 427 | @Override 428 | public int getResultSetType() { 429 | //noinspection MagicConstant 430 | return 0; 431 | } 432 | 433 | @Override 434 | public void addBatch(String sql) { 435 | 436 | } 437 | 438 | @Override 439 | public void clearBatch() { 440 | 441 | } 442 | 443 | @Override 444 | public int[] executeBatch() { 445 | return new int[0]; 446 | } 447 | 448 | @Override 449 | public Connection getConnection() { 450 | return null; 451 | } 452 | 453 | @Override 454 | public boolean getMoreResults(int current) { 455 | return false; 456 | } 457 | 458 | @Override 459 | public ResultSet getGeneratedKeys() { 460 | return null; 461 | } 462 | 463 | @Override 464 | public int executeUpdate(String sql, int autoGeneratedKeys) { 465 | return 0; 466 | } 467 | 468 | @Override 469 | public int executeUpdate(String sql, int[] columnIndexes) { 470 | return 0; 471 | } 472 | 473 | @Override 474 | public int executeUpdate(String sql, String[] columnNames) { 475 | return 0; 476 | } 477 | 478 | @Override 479 | public boolean execute(String sql, int autoGeneratedKeys) { 480 | return false; 481 | } 482 | 483 | @Override 484 | public boolean execute(String sql, int[] columnIndexes) { 485 | return false; 486 | } 487 | 488 | @Override 489 | public boolean execute(String sql, String[] columnNames) { 490 | return false; 491 | } 492 | 493 | @Override 494 | public int getResultSetHoldability() { 495 | return 0; 496 | } 497 | 498 | @Override 499 | public boolean isClosed() { 500 | return false; 501 | } 502 | 503 | @Override 504 | public void setPoolable(boolean poolable) { 505 | 506 | } 507 | 508 | @Override 509 | public boolean isPoolable() { 510 | return false; 511 | } 512 | 513 | @Override 514 | public void closeOnCompletion() { 515 | 516 | } 517 | 518 | @Override 519 | public boolean isCloseOnCompletion() { 520 | return false; 521 | } 522 | 523 | @Override 524 | public T unwrap(Class iface) { 525 | return null; 526 | } 527 | 528 | @Override 529 | public boolean isWrapperFor(Class iface) { 530 | return false; 531 | } 532 | } 533 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory: -------------------------------------------------------------------------------- 1 | org.opensingular.dbuserprovider.DBUserStorageProviderFactory 2 | --------------------------------------------------------------------------------