Checkpointing current work; can update client role mappings for a user.
Showing
4 changed files
with
330 additions
and
1 deletions
... | @@ -35,7 +35,8 @@ tasks.withType(GroovyCompile) { options.compilerArgs << "-proc:none" } | ... | @@ -35,7 +35,8 @@ tasks.withType(GroovyCompile) { options.compilerArgs << "-proc:none" } |
35 | 35 | ||
36 | dependencies { | 36 | dependencies { |
37 | implementation project(':framework') | 37 | implementation project(':framework') |
38 | implementation 'org.keycloak:keycloak-servlet-filter-adapter:15.0.2' | 38 | implementation 'org.keycloak:keycloak-servlet-filter-adapter:21.0.1' |
39 | implementation 'org.keycloak:keycloak-admin-client:21.0.1' | ||
39 | } | 40 | } |
40 | 41 | ||
41 | task cleanLib(type: Delete) { delete fileTree(dir: projectDir.absolutePath+'/lib', include: '*') } | 42 | task cleanLib(type: Delete) { delete fileTree(dir: projectDir.absolutePath+'/lib', include: '*') } | ... | ... |
entity/KeycloakEntities.xml
0 → 100644
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <!-- | ||
3 | This software is in the public domain under CC0 1.0 Universal plus a | ||
4 | Grant of Patent License. | ||
5 | |||
6 | To the extent possible under law, the author(s) have dedicated all | ||
7 | copyright and related and neighboring rights to this software to the | ||
8 | public domain worldwide. This software is distributed without any | ||
9 | warranty. | ||
10 | |||
11 | You should have received a copy of the CC0 Public Domain Dedication | ||
12 | along with this software (see the LICENSE.md file). If not, see | ||
13 | <http://creativecommons.org/publicdomain/zero/1.0/>. | ||
14 | --> | ||
15 | <entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
16 | xsi:noNamespaceSchemaLocation="http://moqui.org/xsd/entity-definition-3.xsd"> | ||
17 | |||
18 | <extend-entity entity-name="UserAccount" package="moqui.security"> | ||
19 | <index name="USERACCOUNT_EXTERNAL_USER" unique="true"><index-field name="externalUserId"/></index> | ||
20 | </extend-entity> | ||
21 | </entities> |
service/keycloak/KeycloakServices.groovy
0 → 100644
1 | import groovy.json.JsonOutput | ||
2 | import groovy.transform.Field | ||
3 | |||
4 | import java.sql.Timestamp | ||
5 | import java.util.Base64 | ||
6 | import java.util.UUID | ||
7 | import javax.crypto.Cipher; | ||
8 | import javax.crypto.SecretKey; | ||
9 | import javax.crypto.SecretKeyFactory; | ||
10 | import javax.crypto.spec.PBEKeySpec; | ||
11 | import javax.crypto.spec.PBEParameterSpec; | ||
12 | |||
13 | import org.slf4j.Logger | ||
14 | import org.slf4j.LoggerFactory | ||
15 | |||
16 | import groovy.json.JsonSlurper | ||
17 | |||
18 | import org.moqui.util.RestClient | ||
19 | import org.moqui.util.ObjectUtilities | ||
20 | import org.moqui.context.ExecutionContext | ||
21 | import org.moqui.entity.EntityCondition | ||
22 | import org.moqui.entity.EntityFind | ||
23 | import org.moqui.entity.EntityList | ||
24 | import org.moqui.entity.EntityValue | ||
25 | |||
26 | import org.keycloak.OAuth2Constants | ||
27 | import org.keycloak.admin.client.Keycloak | ||
28 | import org.keycloak.admin.client.KeycloakBuilder | ||
29 | import org.keycloak.admin.client.resource.ClientResource | ||
30 | import org.keycloak.admin.client.resource.RealmResource | ||
31 | import org.keycloak.admin.client.resource.RoleMappingResource | ||
32 | import org.keycloak.admin.client.resource.RoleScopeResource | ||
33 | import org.keycloak.admin.client.resource.RolesResource | ||
34 | import org.keycloak.admin.client.resource.ServerInfoResource | ||
35 | import org.keycloak.admin.client.resource.UserResource | ||
36 | import org.keycloak.admin.client.resource.UsersResource | ||
37 | import org.keycloak.representations.idm.ClientMappingsRepresentation | ||
38 | import org.keycloak.representations.idm.MappingsRepresentation | ||
39 | import org.keycloak.representations.idm.RoleRepresentation | ||
40 | import org.keycloak.representations.idm.UserRepresentation | ||
41 | import org.keycloak.util.JsonSerialization | ||
42 | |||
43 | @Field Logger logger = LoggerFactory.getLogger('KeycloakServices') | ||
44 | |||
45 | Long timestampToKeycloak(Timestamp timestamp) { | ||
46 | return timestamp.getTime() | ||
47 | } | ||
48 | |||
49 | String keycloakToJson(Object o) { | ||
50 | return JsonSerialization.writeValueAsString(o) | ||
51 | } | ||
52 | |||
53 | Keycloak buildKeycloak() { | ||
54 | return KeycloakBuilder.builder() | ||
55 | .serverUrl('http://keycloak') | ||
56 | .realm('master') | ||
57 | .grantType(OAuth2Constants.CLIENT_CREDENTIALS) | ||
58 | .clientId('moqui') | ||
59 | .clientSecret('iXsnjGEbIVT8DQky2yCU9NQhnqDYyi7g') | ||
60 | .build() | ||
61 | } | ||
62 | |||
63 | Map<String, Object> buildClientConsent() { | ||
64 | return [ | ||
65 | clientId: null, | ||
66 | createdDate: null, // int64 | ||
67 | grantedClientScopes: [], // array[string] | ||
68 | lastUpdatedData: null, // int64 | ||
69 | ] | ||
70 | } | ||
71 | |||
72 | Map<String, Object> buildCredential() { | ||
73 | return [ | ||
74 | createdDate: null, // int64 | ||
75 | credentialData: null, // string | ||
76 | id: null, // string | ||
77 | priority: null, // int32 | ||
78 | secretData: null, // string | ||
79 | temporary: null, // boolean | ||
80 | type: null, // string | ||
81 | userLabel: null, // string | ||
82 | value: null, // string | ||
83 | ] | ||
84 | } | ||
85 | |||
86 | Map<String, Object> buildFederatedIdentiy() { | ||
87 | return [ | ||
88 | identityProvider: null, // string | ||
89 | userId: null, // string | ||
90 | userName: null, // string | ||
91 | ] | ||
92 | } | ||
93 | |||
94 | Map<String, Object> buildKeycloakUser(String userId) { | ||
95 | EntityValue userLogin = ec.entity.find('UserLogin').condition('userId', userId) | ||
96 | |||
97 | Map<String, Object> keycloakUser = [ | ||
98 | access: [:], | ||
99 | attributes: [:], | ||
100 | clientConsents: [], | ||
101 | clientRoles: [:], | ||
102 | createdTimestamp: null, // int64 | ||
103 | credentials: [], | ||
104 | disableableCredentialTypes: [], // array[string] | ||
105 | email: null, // string | ||
106 | emailVerified: null, // boolean | ||
107 | enabled: null, // boolean | ||
108 | federatedIdentities: [], | ||
109 | federationLink: null, // string | ||
110 | firstName: null, | ||
111 | groups: [], // array[string] | ||
112 | id: null, // string | ||
113 | lastName: null, | ||
114 | notBefore: null, // int32 | ||
115 | origin: null, // string | ||
116 | realmRoles: [], // array[string] | ||
117 | requiredActions: [], // array[string] | ||
118 | self: null, // string | ||
119 | serviceClientId: null, // string | ||
120 | username: null, // string | ||
121 | ] | ||
122 | |||
123 | return keycloakUser | ||
124 | } | ||
125 | |||
126 | Map<String, Object> onUpdateEmailAddress() { | ||
127 | |||
128 | |||
129 | } | ||
130 | |||
131 | Map<String, Object> joinUserAccountToKeycloak() { | ||
132 | String userId = context.userId | ||
133 | |||
134 | String externalUserId = context.externalUserId | ||
135 | if (externalUserId && externalUserId.startsWith('keycloak:')) { | ||
136 | // Already joined | ||
137 | return [:] | ||
138 | } | ||
139 | |||
140 | Keycloak keycloak = buildKeycloak() | ||
141 | |||
142 | try { | ||
143 | RealmResource realm = keycloak.realm('master') | ||
144 | UsersResource users = realm.users() | ||
145 | for (UserRepresentation user: users.list()) { | ||
146 | logger.info('keycloak user: ' + keycloakToJson(user)) | ||
147 | } | ||
148 | } finally { | ||
149 | keycloak.close() | ||
150 | } | ||
151 | |||
152 | } | ||
153 | |||
154 | Map<String, Object> onUpdateUserAccount() { | ||
155 | String userId = context.userId | ||
156 | String externalUserId = context.externalUserId | ||
157 | if (!(externalUserId && externalUserId.startsWith('keycloak:'))) { | ||
158 | return [:] | ||
159 | } | ||
160 | String keycloakUserId = externalUserId.substring('keycloak:'.length()) | ||
161 | |||
162 | |||
163 | } | ||
164 | |||
165 | Map<String, RoleRepresentation> getClientRoles(RealmResource realm, ClientResource clientResource, Collection<String> roleList) { | ||
166 | //logger.info('keycloak client: ' + keycloakToJson(clientResource)) | ||
167 | |||
168 | logger.info("roleList(${roleList.size()})=${roleList}") | ||
169 | Map<String, RoleRepresentation> result = [:] | ||
170 | |||
171 | for (String role: roleList) { | ||
172 | result[role] = null | ||
173 | } | ||
174 | RolesResource rolesResource = clientResource.roles() | ||
175 | for (RoleRepresentation roleRep: rolesResource.list()) { | ||
176 | String name = roleRep.getName() | ||
177 | if (result.containsKey(name)) { | ||
178 | result[name] = roleRep | ||
179 | } | ||
180 | } | ||
181 | |||
182 | for (Map.Entry<String, RoleRepresentation> resultEntry: result.entrySet()) { | ||
183 | logger.info("got role mapping: (${resultEntry.getKey()})=(${resultEntry.getValue()})") | ||
184 | } | ||
185 | for (Map.Entry<String, RoleRepresentation> resultEntry: result.entrySet()) { | ||
186 | String name = resultEntry.getKey() | ||
187 | if (resultEntry.getValue() == null) { | ||
188 | rolesResource.create(new RoleRepresentation(name, "", false)) | ||
189 | resultEntry.setValue(rolesResource.get(name)) | ||
190 | } | ||
191 | } | ||
192 | |||
193 | return result | ||
194 | } | ||
195 | |||
196 | |||
197 | void updateUser(RealmResource realm, String keycloakClientId, String keycloakUserId, String userId) { | ||
198 | String clientId = realm.clients().findByClientId(keycloakClientId).get(0).getId() | ||
199 | logger.info("keycloakClientId=${keycloakClientId} clientId=${clientId}") | ||
200 | ClientResource clientResource = realm.clients().get(clientId) | ||
201 | |||
202 | UsersResource usersResource = realm.users() | ||
203 | UserResource userResource = usersResource.get(keycloakUserId) | ||
204 | |||
205 | Timestamp now = ec.user.nowTimestamp | ||
206 | |||
207 | List<EntityValue> userPermissionList = ec.entity.find('UserPermissionCheck') | ||
208 | .condition('userId', userId) | ||
209 | .useCache(true) | ||
210 | .disableAuthz() | ||
211 | .list() | ||
212 | .filterByDate('groupFromDate', 'groupThruDate', now) | ||
213 | .filterByDate('permissionFromDate', 'permissionThruDate', now) | ||
214 | List<String> moquiPermissions = userPermisionList*.userPermissionId.collect { permission -> 'permission:' + permission } | ||
215 | List<String> moquiGroups = ec.user.userGroupIdSet.collect { group -> 'group:' + group } | ||
216 | |||
217 | Map<String, RoleRepresentation> wantedClientRoles = getClientRoles(realm, clientResource, moquiPermissions + moquiGroups) | ||
218 | |||
219 | UserRepresentation userRep = userResource.toRepresentation() | ||
220 | Map<String, List<String>> attributes = new HashMap(userRep.getAttributes() ?: [:]) | ||
221 | |||
222 | attributes['moqui:now'] = [now.toString()] | ||
223 | |||
224 | |||
225 | logger.info("user[$userId]} attributes: " + attributes) | ||
226 | userRep.setAttributes(attributes) | ||
227 | |||
228 | RoleMappingResource roleMappingResource = userResource.roles() | ||
229 | ClientMappingsRepresentation clientMappingsRespresentation = roleMappingResource.getAll().getClientMappings()[keycloakClientId] | ||
230 | for (RoleRepresentation existingRoleRep: clientMappingsRespresentation.getMappings()) { | ||
231 | if (!wantedClientRoles.remove(existingRoleRep.getName())) { | ||
232 | toRemove.add(existingRoleRep) | ||
233 | } | ||
234 | } | ||
235 | RoleScopeResource clientRoleScopeResource = roleMappingResource.clientLevel(clientId) | ||
236 | clientRoleScopeResource.remove(toRemove) | ||
237 | clientRoleScopeResource.add(wantedClientRoles.values() as List) | ||
238 | |||
239 | userResource.update(userRep) | ||
240 | } | ||
241 | |||
242 | |||
243 | Map<String, Object> pushKeycloakUser() { | ||
244 | String userId = context.userId | ||
245 | EntityValue userLogin = ec.entity.find('UserLogin').condition('userId', userId).one() | ||
246 | |||
247 | |||
248 | List<EntityValue> logins | ||
249 | return [:] | ||
250 | |||
251 | /* | ||
252 | <view-entity entity-name="UserPermissionCheck" package="moqui.security"> | ||
253 | <member-entity entity-alias="UGM" entity-name="moqui.security.UserGroupMember"/> | ||
254 | <member-relationship entity-alias="UGP" join-from-alias="UGM" relationship="permissions"/> | ||
255 | <alias name="userGroupId" entity-alias="UGM"/> | ||
256 | <alias name="userId" entity-alias="UGM"/> | ||
257 | <alias name="userPermissionId" entity-alias="UGP"/> | ||
258 | <alias name="groupFromDate" entity-alias="UGM" field="fromDate"/> | ||
259 | <alias name="groupThruDate" entity-alias="UGM" field="thruDate"/> | ||
260 | <alias name="permissionFromDate" entity-alias="UGP" field="fromDate"/> | ||
261 | <alias name="permissionThruDate" entity-alias="UGP" field="thruDate"/> | ||
262 | </view-entity> | ||
263 | */ | ||
264 | } | ||
265 | |||
266 | Map<String, Object> getKeycloakUsers() { | ||
267 | String keycloakClientId = 'moqui' | ||
268 | |||
269 | Keycloak keycloak = buildKeycloak() | ||
270 | |||
271 | try { | ||
272 | RealmResource realm = keycloak.realm('master') | ||
273 | /* | ||
274 | ServerInfoResource serverInfo = keycloak.serverInfo() | ||
275 | logger.info('keycloak serverInfo: ' + keycloakToJson(serverInfo.getInfo())) | ||
276 | logger.info('keycloak keys: ' + keycloakToJson(realm.keys().getKeyMetadata())) | ||
277 | UsersResource users = realm.users() | ||
278 | for (UserRepresentation user: users.list()) { | ||
279 | logger.info('keycloak user: ' + keycloakToJson(user)) | ||
280 | } | ||
281 | */ | ||
282 | updateUser(realm, keycloakClientId, 'c6a4cb53-4533-4236-89e5-058967b9b90a', '100000') | ||
283 | |||
284 | } finally { | ||
285 | keycloak.close() | ||
286 | } | ||
287 | } | ||
288 |
service/keycloak/KeycloakServices.xml
0 → 100644
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <!-- | ||
3 | This software is in the public domain under CC0 1.0 Universal plus a | ||
4 | Grant of Patent License. | ||
5 | |||
6 | To the extent possible under law, the author(s) have dedicated all | ||
7 | copyright and related and neighboring rights to this software to the | ||
8 | public domain worldwide. This software is distributed without any | ||
9 | warranty. | ||
10 | |||
11 | You should have received a copy of the CC0 Public Domain Dedication | ||
12 | along with this software (see the LICENSE.md file). If not, see | ||
13 | <http://creativecommons.org/publicdomain/zero/1.0/>. | ||
14 | --> | ||
15 | <services xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://moqui.org/xsd/service-definition-3.xsd"> | ||
16 | |||
17 | <service verb="get" noun="KeycloakUsers" type="script" location="component://moqui-keycloak/service/keycloak/KeycloakServices.groovy" method="getKeycloakUsers"> | ||
18 | </service> | ||
19 | </services> |
-
Please register or sign in to post a comment