Hook based pushing of account data to keycloak.
Showing
8 changed files
with
834 additions
and
147 deletions
entity/Keycloak.eecas.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 | <eecas xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://moqui.org/xsd/entity-eca-3.xsd"> | ||
16 | <!-- | ||
17 | moqui.security.UserGroup | ||
18 | moqui.security.UserGroupMember | ||
19 | moqui.security.UserGroupPermission | ||
20 | moqui.security.UserPermission | ||
21 | mantle.party.PartyClassification | ||
22 | |||
23 | moqui.security.UserPermission -> userPermission:$id | ||
24 | mantle.party.PartyClassification -> partyClassification:$id | ||
25 | mantle.party.RoleType -> roleType:$id | ||
26 | |||
27 | moqui.security.UserAccount | ||
28 | |||
29 | moqui.security.UserAccount | ||
30 | mantle.party.Party | ||
31 | mantle.party.PartyClassificationAppl | ||
32 | mantle.party.PartyRole | ||
33 | mantle.party.Person | ||
34 | mantle.party.RoleType | ||
35 | mantle.party.contact.ContactMech | ||
36 | mantle.party.contact.TelecomNumber | ||
37 | mantle.party.contact.PostalAddress | ||
38 | mantle.party.contact.PartyContactMech | ||
39 | |||
40 | Future use: | ||
41 | mantle.party.PartyRelationship | ||
42 | --> | ||
43 | <eeca id="ContactMech" entity="mantle.party.contact.ContactMech" on-create="true" on-update="true" on-delete="true" run-on-error="false" get-entire-entity="false" get-original-value="false"> | ||
44 | <actions><service-call name="keycloak.HookServices.handle#EntityUpdate" in-map="[entityName: 'ContactMech', value: contactMechId]"/></actions> | ||
45 | </eeca> | ||
46 | <eeca id="Party" entity="mantle.party.Party" on-create="true" on-update="true" on-delete="true" run-on-error="false" get-entire-entity="false" get-original-value="false"> | ||
47 | <actions><service-call name="keycloak.HookServices.handle#EntityUpdate" in-map="[entityName: 'Party', value: partyId]"/></actions> | ||
48 | </eeca> | ||
49 | <eeca id="PartyClassification" entity="mantle.party.PartyClassification" on-create="true" on-update="true" on-delete="true" run-on-error="false" get-entire-entity="false" get-original-value="false"> | ||
50 | <actions><service-call name="keycloak.HookServices.handle#EntityUpdate" in-map="[entityName: 'PartyClassification', value: partyClassificationId]"/></actions> | ||
51 | </eeca> | ||
52 | <eeca id="PartyClassificationAppl" entity="mantle.party.PartyClassificationAppl" on-create="true" on-update="true" on-delete="true" run-on-error="false" get-entire-entity="false" get-original-value="false"> | ||
53 | <actions><service-call name="keycloak.HookServices.handle#EntityUpdate" in-map="[entityName: 'Party', value: partyId]"/></actions> | ||
54 | </eeca> | ||
55 | <eeca id="PartyContactMech" entity="mantle.party.contact.PartyContactMech" on-create="true" on-update="true" on-delete="true" run-on-error="false" get-entire-entity="false" get-original-value="false"> | ||
56 | <actions><service-call name="keycloak.HookServices.handle#EntityUpdate" in-map="[entityName: 'Party', value: partyId]"/></actions> | ||
57 | </eeca> | ||
58 | <eeca id="PartyRole" entity="mantle.party.PartyRole" on-create="true" on-update="true" on-delete="true" run-on-error="false" get-entire-entity="false" get-original-value="false"> | ||
59 | <actions><service-call name="keycloak.HookServices.handle#EntityUpdate" in-map="[entityName: 'Party', value: partyId]"/></actions> | ||
60 | </eeca> | ||
61 | <eeca id="Person" entity="mantle.party.Person" on-create="true" on-update="true" on-delete="true" run-on-error="false" get-entire-entity="false" get-original-value="false"> | ||
62 | <actions><service-call name="keycloak.HookServices.handle#EntityUpdate" in-map="[entityName: 'Party', value: partyId]"/></actions> | ||
63 | </eeca> | ||
64 | <eeca id="PostalAddress" entity="mantle.party.contact.PostalAddress" on-create="true" on-update="true" on-delete="true" run-on-error="false" get-entire-entity="false" get-original-value="false"> | ||
65 | <actions><service-call name="keycloak.HookServices.handle#EntityUpdate" in-map="[entityName: 'ContactMech', value: contactMechId]"/></actions> | ||
66 | </eeca> | ||
67 | <eeca id="RoleType" entity="mantle.party.RoleType" on-create="true" on-update="true" on-delete="true" run-on-error="false" get-entire-entity="false" get-original-value="false"> | ||
68 | <actions><service-call name="keycloak.HookServices.handle#EntityUpdate" in-map="[entityName: 'RoleType', value: roleTypeId]"/></actions> | ||
69 | </eeca> | ||
70 | <eeca id="TelecomNumber" entity="mantle.party.contact.TelecomNumber" on-create="true" on-update="true" on-delete="true" run-on-error="false" get-entire-entity="false" get-original-value="false"> | ||
71 | <actions><service-call name="keycloak.HookServices.handle#EntityUpdate" in-map="[entityName: 'ContactMech', value: contactMechId]"/></actions> | ||
72 | </eeca> | ||
73 | <eeca id="UserAccount" entity="moqui.security.UserAccount" on-create="true" on-update="true" on-delete="true" run-on-error="false" get-entire-entity="false" get-original-value="false"> | ||
74 | <actions><service-call name="keycloak.HookServices.handle#EntityUpdate" in-map="[entityName: 'UserAccount', value: userId]"/></actions> | ||
75 | </eeca> | ||
76 | <eeca id="UserGroup" entity="moqui.security.UserGroup" on-create="true" on-update="true" on-delete="true" run-on-error="false" get-entire-entity="false" get-original-value="false"> | ||
77 | <actions><service-call name="keycloak.HookServices.handle#EntityUpdate" in-map="[entityName: 'UserGroup', value: userGroupId]"/></actions> | ||
78 | </eeca> | ||
79 | <eeca id="UserGroupMember" entity="moqui.security.UserGroupMember" on-create="true" on-update="true" on-delete="true" run-on-error="false" get-entire-entity="false" get-original-value="false"> | ||
80 | <actions><service-call name="keycloak.HookServices.handle#EntityUpdate" in-map="[entityName: 'UserAccount', value: userId]"/></actions> | ||
81 | </eeca> | ||
82 | <eeca id="UserGroupPermission" entity="moqui.security.UserGroupPermission" on-create="true" on-update="true" on-delete="true" run-on-error="false" get-entire-entity="false" get-original-value="false"> | ||
83 | <actions><service-call name="keycloak.HookServices.handle#EntityUpdate" in-map="[entityName: 'UserGroup', value: userGroupId]"/></actions> | ||
84 | </eeca> | ||
85 | <eeca id="UserPermission" entity="moqui.security.UserPermission" on-create="true" on-update="true" on-delete="true" run-on-error="false" get-entire-entity="false" get-original-value="false"> | ||
86 | <actions><service-call name="keycloak.HookServices.handle#EntityUpdate" in-map="[entityName: 'UserPermission', value: userPermissionId]"/></actions> | ||
87 | </eeca> | ||
88 | </eecas> |
1 | <screen-extend xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://moqui.org/xsd/xml-screen-3.xsd"> | ||
2 | |||
3 | <form-single name="UpdateUserAccount"> | ||
4 | <field name="externalUserId"><default-field><text-line size="60"/></default-field></field> | ||
5 | </form-single> | ||
6 | |||
7 | </screen-extend> |
1 | <screen-extend xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://moqui.org/xsd/xml-screen-3.xsd"> | ||
2 | |||
3 | <form-single name="UpdateUserAccount"> | ||
4 | <field name="externalUserId"><default-field><text-line size="60"/></default-field></field> | ||
5 | <field-layout> | ||
6 | <fields-not-referenced/> | ||
7 | <field-row><field-ref name="externalUserId"/></field-row> | ||
8 | <field-row><field-ref name="requirePasswordChange"/><field-ref name="disabled"/></field-row> | ||
9 | <field-row><field-ref name="successiveFailedLogins"/><field-ref name="disabledDateTime"/></field-row> | ||
10 | <field-ref name="submitButton"/> | ||
11 | </field-layout> | ||
12 | </form-single> | ||
13 | <!-- | ||
14 | |||
15 | |||
16 | --> | ||
17 | </screen-extend> |
service/Keycloak.secas.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 | <secas xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://moqui.org/xsd/service-eca-3.xsd"> | ||
16 | <!-- Invoice GL Posting --> | ||
17 | <seca id="updatePasswordInternal" service="org.moqui.impl.UserServices.update#PasswordInternal" when="post-service" run-on-error="false"> | ||
18 | <condition><expression>updateSuccessful</expression></condition> | ||
19 | <actions><service-call name="keycloak.HookServices.handle#UpdatePasswordInternal" in-map="context"/></actions> | ||
20 | </seca> | ||
21 | <!-- | ||
22 | <service verb="update" noun="PasswordInternal" authenticate="false" allow-remote="false"> | ||
23 | <in-parameters> | ||
24 | <parameter name="userId" required="true"/> | ||
25 | <parameter name="newPassword" required="true"/> | ||
26 | <parameter name="newPasswordVerify" required="true"/> | ||
27 | <parameter name="requirePasswordChange" default-value="N"/> | ||
28 | </in-parameters> | ||
29 | <out-parameters> | ||
30 | <parameter name="passwordIssues" type="Boolean"/> | ||
31 | <parameter name="updateSuccessful" type="Boolean"/> | ||
32 | </out-parameters> | ||
33 | --> | ||
34 | </secas> |
service/keycloak/HookServices.groovy
0 → 100644
1 | import javax.transaction.Synchronization | ||
2 | import javax.transaction.xa.XAResource | ||
3 | import javax.transaction.xa.Xid | ||
4 | |||
5 | import groovy.transform.Field | ||
6 | |||
7 | import org.slf4j.Logger | ||
8 | import org.slf4j.LoggerFactory | ||
9 | |||
10 | import org.moqui.util.RestClient | ||
11 | import org.moqui.util.ObjectUtilities | ||
12 | import org.moqui.context.ExecutionContext | ||
13 | import org.moqui.entity.EntityCondition | ||
14 | import org.moqui.entity.EntityFind | ||
15 | import org.moqui.entity.EntityList | ||
16 | import org.moqui.entity.EntityValue | ||
17 | |||
18 | @Field Logger logger = LoggerFactory.getLogger(getClass().getName()) | ||
19 | //'HookServices') | ||
20 | |||
21 | class HookSynchronization implements Synchronization { | ||
22 | private final ExecutionContext ec; | ||
23 | |||
24 | private static class EntityToken { | ||
25 | protected final String entityName | ||
26 | protected final String keyName | ||
27 | protected final Object keyValue | ||
28 | protected final Map<String, Object> extraParameters = new HashMap<String, Object>() | ||
29 | |||
30 | protected EntityToken(String entityName, String keyName, Object keyValue) { | ||
31 | this.entityName = entityName | ||
32 | this.keyName = keyName | ||
33 | this.keyValue = keyValue | ||
34 | } | ||
35 | |||
36 | public boolean equals(Object other) { | ||
37 | if (!(other instanceof EntityToken)) { | ||
38 | return false | ||
39 | } | ||
40 | EntityToken that = (EntityToken) other | ||
41 | return entityName.equals(that.entityName) && keyName.equals(that.keyName) && keyValue.equals(that.keyValue) | ||
42 | } | ||
43 | |||
44 | public int hashCode() { | ||
45 | return entityName.hashCode() ^ keyName.hashCode() ^ keyValue.hashCode() | ||
46 | } | ||
47 | } | ||
48 | |||
49 | private Map<EntityToken, Map<String, Object>> updates = new LinkedHashMap<>() | ||
50 | |||
51 | protected HookSynchronization(ExecutionContext ec) { | ||
52 | this.ec = ec | ||
53 | } | ||
54 | |||
55 | protected void add(String entityName, String keyName, Object keyValue, Map<String, Object> extraParameters) { | ||
56 | EntityToken entityToken = new EntityToken(entityName, keyName, keyValue) | ||
57 | // This always move the value to the end of the list | ||
58 | Map<String, Object> existingExtraParameters = updates.remove(entityToken) | ||
59 | Map<String, Object> newExtraParameters = [:] | ||
60 | if (existingExtraParameters) { | ||
61 | newExtraParameters.putAll(existingExtraParameters) | ||
62 | } | ||
63 | if (extraParameters) { | ||
64 | newExtraParameters.putAll(extraParameters) | ||
65 | } | ||
66 | updates.put(entityToken, newExtraParameters) | ||
67 | } | ||
68 | |||
69 | @Override | ||
70 | public void beforeCompletion() { | ||
71 | List<Map<String, Object>> updates = [] | ||
72 | for (Map.Entry<EntityToken, Map<String, Object>> entry: this.updates.entrySet()) { | ||
73 | EntityToken token = entry.getKey() | ||
74 | Map<String, Object> extraParameters = entry.getValue() | ||
75 | updates.add([ | ||
76 | entityName: token.entityName, | ||
77 | keyName: token.keyName, | ||
78 | keyValue: token.keyValue, | ||
79 | extraParameters: extraParameters, | ||
80 | ]) | ||
81 | } | ||
82 | ec.getService().async().name("keycloak.HookServices.process#Updates").parameter('updates', updates).call() | ||
83 | } | ||
84 | |||
85 | @Override | ||
86 | public void afterCompletion(int status) { | ||
87 | |||
88 | } | ||
89 | } | ||
90 | |||
91 | /* | ||
92 | class HookXAResource implements XAResource { | ||
93 | private static class EntityPk { | ||
94 | protected String entityName; | ||
95 | protected Map<String, Object> pk; | ||
96 | |||
97 | protected EntityPk(EntityValue entity) { | ||
98 | this.entityName = entity.getEntityName() | ||
99 | this.pk = entity.getPrimaryKeys() | ||
100 | } | ||
101 | |||
102 | public boolean equals(Object other) { | ||
103 | if (!(object instanceof EntityPk)) { | ||
104 | return false | ||
105 | } | ||
106 | EntityPk that = (EntityPk) other | ||
107 | return entityName.equals(that.entityName) && pk.equals(that.pk) | ||
108 | } | ||
109 | |||
110 | public int hashCode() { | ||
111 | return entityName.hashCode ^ pk.hashCode() | ||
112 | } | ||
113 | } | ||
114 | |||
115 | private Map<Xid, Set<EntityPk>> modifiedValues = new HashMap<>() | ||
116 | private int timeout = 60 | ||
117 | |||
118 | @Override | ||
119 | public void commit(Xid xid, boolean onePhase) { | ||
120 | |||
121 | } | ||
122 | |||
123 | @Override | ||
124 | public void end(Xid xid, int flags) { | ||
125 | if (flags & TMFAIL) { | ||
126 | |||
127 | } else if (flags & TMSUSPEND) { | ||
128 | |||
129 | } else if (flags & TMSUCCESS) { | ||
130 | |||
131 | } | ||
132 | } | ||
133 | |||
134 | @Override | ||
135 | public void forget(Xid xid) { | ||
136 | modifiedValues.remove(xid) | ||
137 | } | ||
138 | |||
139 | @Override | ||
140 | public int getTransactionTimeout() { | ||
141 | return timeout | ||
142 | } | ||
143 | |||
144 | @Override | ||
145 | public boolean isSameRM(XAResource xar) { | ||
146 | return getClass().equals(xar.getClass()) | ||
147 | } | ||
148 | |||
149 | @Override | ||
150 | public int prepare(Xid xid) { | ||
151 | if (!keycloak.connected()) { | ||
152 | throw new XAException(XAException.XAERR_RMFAIL) | ||
153 | } | ||
154 | } | ||
155 | |||
156 | @Override | ||
157 | public Xid[] recover(int flag) { | ||
158 | |||
159 | } | ||
160 | |||
161 | @Override | ||
162 | public void rollback(Xid xid) { | ||
163 | |||
164 | } | ||
165 | |||
166 | @Override | ||
167 | public boolean setTransactionTimeout(int timeout) { | ||
168 | this.timeout = timeout | ||
169 | } | ||
170 | |||
171 | @Override | ||
172 | public void start(Xid xid, int flags) { | ||
173 | |||
174 | } | ||
175 | |||
176 | } | ||
177 | */ | ||
178 | |||
179 | HookSynchronization getHookSync() { | ||
180 | HookSynchronization hookSync = ec.transaction.getActiveSynchronization(getClass().getName()) | ||
181 | if (hookSync == null) { | ||
182 | hookSync = new HookSynchronization(ec) | ||
183 | ec.transaction.putAndEnlistActiveSynchronization(getClass().getName(), hookSync) | ||
184 | } | ||
185 | return hookSync | ||
186 | } | ||
187 | |||
188 | Map<String, Object> handleEntityUpdate() { | ||
189 | logger.info("handleEntityUpdate: ${context.entityName}[${context.value}]") | ||
190 | logger.info("context: ${context}") | ||
191 | ExecutionContext ec = context.ec | ||
192 | Map<String, Object> contextRoot = ec.getContextRoot() | ||
193 | Map<String, Set<Object>> toRegister = contextRoot['KeycloakEntityRegistrations'] | ||
194 | logger.info("toRegister: ${toRegister}") | ||
195 | if (toRegister == null) { | ||
196 | toRegister = contextRoot['KeycloakEntityRegistrations'] = [:] | ||
197 | } | ||
198 | |||
199 | HookSynchronization hookSync = getHookSync() | ||
200 | |||
201 | List<Map<String, Object>> queue = [[entityName: context.entityName, value: context.value]] | ||
202 | |||
203 | while (!queue.isEmpty()) { | ||
204 | Map<String, Object> entry = queue.remove(0) | ||
205 | logger.info("processing entry: ${entry}") | ||
206 | String entityName = entry.entityName | ||
207 | Object value = entry.value | ||
208 | Set<Object> entityRegistrations = toRegister[entityName] | ||
209 | if (entityRegistrations == null) { | ||
210 | entityRegistrations = toRegister[entityName] = new HashSet() | ||
211 | entityRegistrations.add(value) | ||
212 | } else if (entityRegistrations.contains(value)) { | ||
213 | continue | ||
214 | } | ||
215 | String keyName | ||
216 | switch (entityName) { | ||
217 | case 'Party': | ||
218 | EntityList userAccounts = ec.entity.find('UserAccount').condition('partyId', value).list() | ||
219 | for (EntityValue userAccount: userAccounts) { | ||
220 | queue.add([entityName: 'UserAccount', value: userAccount.userId]) | ||
221 | } | ||
222 | continue | ||
223 | case 'ContactMech': | ||
224 | EntityList partyContactMechs = ec.entity.find('PartyContactMech').condition('contactMechId', value).list() | ||
225 | for (EntityValue partyContactMech: partyContactMechs) { | ||
226 | queue.add([entityName: 'Party', value: partyContactMech.partyId]) | ||
227 | } | ||
228 | continue | ||
229 | case 'UserAccount': | ||
230 | keyName = 'userId' | ||
231 | break | ||
232 | case 'UserGroup': | ||
233 | keyName = 'userGroupId' | ||
234 | break | ||
235 | case 'UserGroupMember': | ||
236 | keyName = 'userGroupId' | ||
237 | break | ||
238 | case 'UserPermission': | ||
239 | keyName = 'userPermissionId' | ||
240 | break | ||
241 | case 'PartyClassification': | ||
242 | keyName = 'partyClassificationId' | ||
243 | break | ||
244 | case 'RoleType': | ||
245 | keyName = 'roleTypeId' | ||
246 | break | ||
247 | } | ||
248 | ec.getLogger().info("Registered synchronization for ${entityName}: [${value}]") | ||
249 | hookSync.add(entityName, keyName, value, [:]) | ||
250 | //ec.getService().special().name("keycloak.KeycloakServices.send#${entityName}").parameter(keyName, value).registerOnCommit() | ||
251 | } | ||
252 | } | ||
253 | |||
254 | Map<String, Object> handleUpdatePasswordInternal() { | ||
255 | String userId = ec.context.userId | ||
256 | String newPassword = ec.context.newPassword | ||
257 | Boolean requirePasswordChange = ec.context.requirePasswordChange | ||
258 | ec.getLogger().info("Registered synchronization for update#PasswordInternal: [${userId}]") | ||
259 | HookSynchronization hookSync = getHookSync() | ||
260 | hookSync.add('UserAccount', 'userId', userId, [ | ||
261 | newPassword: newPassword, | ||
262 | requirePasswordChange: requirePasswordChange, | ||
263 | ]) | ||
264 | } | ||
265 | |||
266 | Map<String, Object> processUpdates() { | ||
267 | List<Map<String, Object>> updates = context.updates | ||
268 | |||
269 | for (Map<String, Object> update: updates) { | ||
270 | Map<String, Object> parameters = [:] | ||
271 | parameters[update.keyName] = update.keyValue | ||
272 | parameters.putAll(update.extraParameters) | ||
273 | ec.getService().sync().name("keycloak.KeycloakServices.send#${update.entityName}").parameters(parameters).call() | ||
274 | } | ||
275 | return [:] | ||
276 | } | ||
277 |
service/keycloak/HookServices.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="handle" noun="EntityUpdate" type="script" location="component://moqui-keycloak/service/keycloak/HookServices.groovy" method="handleEntityUpdate" authenticate="anonymous-all"> | ||
18 | <in-parameters> | ||
19 | <parameter name="entityName" type="String" required="true"/> | ||
20 | <parameter name="value" type="Object" required="true"/> | ||
21 | </in-parameters> | ||
22 | </service> | ||
23 | |||
24 | <service verb="handle" noun="UpdatePasswordInternal" type="script" location="component://moqui-keycloak/service/keycloak/HookServices.groovy" method="handleUpdatePasswordInternal" authenticate="anonymous-all"> | ||
25 | <in-parameters> | ||
26 | <parameter name="userId" required="true"/> | ||
27 | <parameter name="newPassword" required="true"/> | ||
28 | <parameter name="requirePasswordChange"/> | ||
29 | </in-parameters> | ||
30 | </service> | ||
31 | |||
32 | <service verb="process" noun="Updates" type="script" location="component://moqui-keycloak/service/keycloak/HookServices.groovy" method="processUpdates" authenticate="anonymous-all"> | ||
33 | <in-parameters> | ||
34 | <parameter name="updates" type="List" required="true"/> | ||
35 | </in-parameters> | ||
36 | </service> | ||
37 | </services> |
... | @@ -2,21 +2,12 @@ import groovy.json.JsonOutput | ... | @@ -2,21 +2,12 @@ import groovy.json.JsonOutput |
2 | import groovy.transform.Field | 2 | import groovy.transform.Field |
3 | 3 | ||
4 | import java.sql.Timestamp | 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 | 5 | ||
13 | import org.slf4j.Logger | 6 | import org.slf4j.Logger |
14 | import org.slf4j.LoggerFactory | 7 | import org.slf4j.LoggerFactory |
15 | 8 | ||
16 | import groovy.json.JsonSlurper | 9 | import groovy.json.JsonSlurper |
17 | 10 | ||
18 | import org.moqui.util.RestClient | ||
19 | import org.moqui.util.ObjectUtilities | ||
20 | import org.moqui.context.ExecutionContext | 11 | import org.moqui.context.ExecutionContext |
21 | import org.moqui.entity.EntityCondition | 12 | import org.moqui.entity.EntityCondition |
22 | import org.moqui.entity.EntityFind | 13 | import org.moqui.entity.EntityFind |
... | @@ -25,19 +16,22 @@ import org.moqui.entity.EntityValue | ... | @@ -25,19 +16,22 @@ import org.moqui.entity.EntityValue |
25 | 16 | ||
26 | import org.moqui.keycloak.KeycloakToolFactory | 17 | import org.moqui.keycloak.KeycloakToolFactory |
27 | 18 | ||
28 | |||
29 | import org.keycloak.OAuth2Constants | 19 | import org.keycloak.OAuth2Constants |
30 | import org.keycloak.admin.client.Keycloak | 20 | import org.keycloak.admin.client.Keycloak |
31 | import org.keycloak.admin.client.KeycloakBuilder | 21 | import org.keycloak.admin.client.KeycloakBuilder |
32 | import org.keycloak.admin.client.resource.ClientResource | 22 | import org.keycloak.admin.client.resource.ClientResource |
23 | import org.keycloak.admin.client.resource.GroupsResource | ||
33 | import org.keycloak.admin.client.resource.RealmResource | 24 | import org.keycloak.admin.client.resource.RealmResource |
34 | import org.keycloak.admin.client.resource.RoleMappingResource | 25 | import org.keycloak.admin.client.resource.RoleMappingResource |
35 | import org.keycloak.admin.client.resource.RoleScopeResource | 26 | import org.keycloak.admin.client.resource.RoleScopeResource |
27 | import org.keycloak.admin.client.resource.RoleResource | ||
36 | import org.keycloak.admin.client.resource.RolesResource | 28 | import org.keycloak.admin.client.resource.RolesResource |
37 | import org.keycloak.admin.client.resource.ServerInfoResource | 29 | import org.keycloak.admin.client.resource.ServerInfoResource |
38 | import org.keycloak.admin.client.resource.UserResource | 30 | import org.keycloak.admin.client.resource.UserResource |
39 | import org.keycloak.admin.client.resource.UsersResource | 31 | import org.keycloak.admin.client.resource.UsersResource |
40 | import org.keycloak.representations.idm.ClientMappingsRepresentation | 32 | import org.keycloak.representations.idm.ClientMappingsRepresentation |
33 | import org.keycloak.representations.idm.CredentialRepresentation | ||
34 | import org.keycloak.representations.idm.GroupRepresentation | ||
41 | import org.keycloak.representations.idm.MappingsRepresentation | 35 | import org.keycloak.representations.idm.MappingsRepresentation |
42 | import org.keycloak.representations.idm.RoleRepresentation | 36 | import org.keycloak.representations.idm.RoleRepresentation |
43 | import org.keycloak.representations.idm.UserRepresentation | 37 | import org.keycloak.representations.idm.UserRepresentation |
... | @@ -53,106 +47,320 @@ String keycloakToJson(Object o) { | ... | @@ -53,106 +47,320 @@ String keycloakToJson(Object o) { |
53 | return JsonSerialization.writeValueAsString(o) | 47 | return JsonSerialization.writeValueAsString(o) |
54 | } | 48 | } |
55 | 49 | ||
56 | Map<String, Object> buildClientConsent() { | 50 | void cleanupKeycloak() { |
57 | return [ | 51 | Map<String, Object> sharedMap = ec.getContextRoot() |
58 | clientId: null, | 52 | Keycloak keycloak = sharedMap['keycloak'] |
59 | createdDate: null, // int64 | 53 | if (keycloak != null) { |
60 | grantedClientScopes: [], // array[string] | 54 | keycloak.close() |
61 | lastUpdatedData: null, // int64 | 55 | } |
62 | ] | 56 | Iterator<String> keyIt = sharedMap.keySet().iterator() |
57 | while (keyIt.hasNext()) { | ||
58 | String key = keyIt.next() | ||
59 | if (key == 'keycloak' || key.startsWith('keycloak:')) { | ||
60 | keyIt.remove() | ||
61 | } | ||
62 | } | ||
63 | } | ||
64 | |||
65 | Keycloak getKeycloak() { | ||
66 | Map<String, Object> sharedMap = ec.getContextRoot() | ||
67 | KeycloakToolFactory keycloakToolFactory = ec.getFactory().getToolFactory('Keycloak') | ||
68 | Keycloak keycloak = sharedMap['keycloak'] | ||
69 | if (keycloak == null) { | ||
70 | keycloak = keycloakToolFactory.getInstance() | ||
71 | sharedMap['keycloak'] = keycloak | ||
72 | } | ||
73 | return keycloak | ||
63 | } | 74 | } |
64 | 75 | ||
65 | Map<String, Object> buildCredential() { | 76 | RealmResource getKeycloakRealm() { |
66 | return [ | 77 | Map<String, Object> sharedMap = ec.getContextRoot() |
67 | createdDate: null, // int64 | 78 | RealmResource keycloakRealm = sharedMap['keycloak:realm'] |
68 | credentialData: null, // string | 79 | if (!realm) { |
69 | id: null, // string | 80 | Keycloak keycloak = getKeycloak() |
70 | priority: null, // int32 | 81 | KeycloakToolFactory keycloakToolFactory = ec.getFactory().getToolFactory('Keycloak') |
71 | secretData: null, // string | 82 | keycloakRealm = keycloak.realm(keycloakToolFactory.getRealmName()) |
72 | temporary: null, // boolean | 83 | sharedMap['keycloak:realm'] = keycloakRealm |
73 | type: null, // string | 84 | } |
74 | userLabel: null, // string | 85 | return keycloakRealm |
75 | value: null, // string | ||
76 | ] | ||
77 | } | 86 | } |
78 | 87 | ||
79 | Map<String, Object> buildFederatedIdentiy() { | 88 | String getKeycloakClientId() { |
80 | return [ | 89 | Map<String, Object> sharedMap = ec.getContextRoot() |
81 | identityProvider: null, // string | 90 | String keycloakClientId = sharedMap['keycloak:clientId'] |
82 | userId: null, // string | 91 | if (!keycloakClientId) { |
83 | userName: null, // string | 92 | RealmResource keycloakRealm = getKeycloakRealm() |
84 | ] | 93 | KeycloakToolFactory keycloakToolFactory = ec.getFactory().getToolFactory('Keycloak') |
94 | keycloakClientId = keycloakRealm.clients().findByClientId(keycloakToolFactory.getClientId()).get(0).getId() | ||
95 | sharedMap['keycloak:clientId'] = keycloakClientId | ||
96 | } | ||
97 | return keycloakClientId | ||
85 | } | 98 | } |
86 | 99 | ||
87 | Map<String, Object> buildKeycloakUser(String userId) { | 100 | ClientResource getKeycloakClient() { |
88 | EntityValue userLogin = ec.entity.find('UserLogin').condition('userId', userId) | 101 | Map<String, Object> sharedMap = ec.getContextRoot() |
89 | 102 | ClientResource keycloakClient = sharedMap['keycloak:client'] | |
90 | Map<String, Object> keycloakUser = [ | 103 | if (!keycloakClient) { |
91 | access: [:], | 104 | String keycloakClientId = getKeycloakClientId() |
92 | attributes: [:], | 105 | RealmResource realm = getKeycloakRealm() |
93 | clientConsents: [], | 106 | keycloakClient = realm.clients().get(keycloakClientId) |
94 | clientRoles: [:], | 107 | sharedMap['keycloak:client'] = keycloakClient |
95 | createdTimestamp: null, // int64 | 108 | } |
96 | credentials: [], | 109 | return keycloakClient |
97 | disableableCredentialTypes: [], // array[string] | ||
98 | email: null, // string | ||
99 | emailVerified: null, // boolean | ||
100 | enabled: null, // boolean | ||
101 | federatedIdentities: [], | ||
102 | federationLink: null, // string | ||
103 | firstName: null, | ||
104 | groups: [], // array[string] | ||
105 | id: null, // string | ||
106 | lastName: null, | ||
107 | notBefore: null, // int32 | ||
108 | origin: null, // string | ||
109 | realmRoles: [], // array[string] | ||
110 | requiredActions: [], // array[string] | ||
111 | self: null, // string | ||
112 | serviceClientId: null, // string | ||
113 | username: null, // string | ||
114 | ] | ||
115 | |||
116 | return keycloakUser | ||
117 | } | 110 | } |
118 | 111 | ||
119 | Map<String, Object> onUpdateEmailAddress() { | 112 | Map<String, Object> syncUserAccount() { |
113 | String userId = context.userId | ||
114 | String newPassword = context.newPassword | ||
120 | 115 | ||
116 | try { | ||
117 | EntityValue userAccount = ec.getEntity().find('UserAccount').condition('userId', userId).one() | ||
118 | if (!userAccount.username) { | ||
119 | ec.message.addMessage("Skipping userAccount with empty username: ${userId})") | ||
120 | return | ||
121 | } | ||
122 | RealmResource realmResource = getKeycloakRealm() | ||
123 | UsersResource usersResource = realmResource.users() | ||
124 | |||
125 | if (userAccount.externalUserId == null) { | ||
126 | logger.info("Searching for existing user: ${userAccount.username}") | ||
127 | List<UserRepresentation> existingByUsername = usersResource.search(userAccount.username, true) | ||
128 | logger.info("Found: ${existingByUsername}") | ||
129 | if (!existingByUsername.isEmpty()) { | ||
130 | // FIXME: disable eeca | ||
131 | boolean reenableEeca = ec.artifactExecutionFacade.disableEntityEca() | ||
132 | try { | ||
133 | userAccount.externalUserId = existingByUsername[0].getId() | ||
134 | userAccount.store() | ||
135 | } finally { | ||
136 | if (reenableEeca) ec.artifactExecutionFacade.enableEntityEca() | ||
137 | } | ||
138 | } | ||
139 | } | ||
140 | if (userAccount.externalUserId == null) { | ||
141 | UserRepresentation newKeycloakUser = new UserRepresentation() | ||
142 | newKeycloakUser.setUsername(userAccount.username) | ||
143 | newKeycloakUser.setEnabled(false) | ||
144 | newKeycloakUser.setEmail(userAccount.emailAddress) | ||
145 | Object response = usersResource.create(newKeycloakUser) | ||
146 | logger.info("create response: ${keycloakToJson(response)}") | ||
147 | logger.info("status=${response.status}") | ||
148 | if (response.status == 201) { | ||
149 | List<UserRepresentation> newlyCreatedUserList = usersResource.search(userAccount.username) | ||
150 | if (newlyCreatedUserList.size() == 1) { | ||
151 | // FIXME: disable eeca | ||
152 | boolean reenableEeca = ec.artifactExecutionFacade.disableEntityEca() | ||
153 | try { | ||
154 | userAccount.externalUserId = newlyCreatedUserList[0].getId() | ||
155 | userAccount.store() | ||
156 | } finally { | ||
157 | if (reenableEeca) ec.artifactExecutionFacade.enableEntityEca() | ||
158 | } | ||
159 | } | ||
160 | } | ||
161 | if (userAccount.externalUserId == null) { | ||
162 | ec.message.addError("Couldn't create keycloak user for: ${userAccount.username})") | ||
163 | return | ||
164 | } | ||
165 | } | ||
166 | String keycloakUserId = userAccount.externalUserId | ||
167 | logger.info("Would update: keycloak(${keycloakUserId})") | ||
168 | updateUser(keycloakUserId, userId, newPassword != null) | ||
121 | 169 | ||
170 | if (newPassword != null) { | ||
171 | UserResource userResource = usersResource.get(keycloakUserId) | ||
172 | List<CredentialRepresentation> credentials = userResource.credentials() | ||
173 | for (CredentialRepresentation credential: credentials) { | ||
174 | logger.info("keycloak(${keycloakUserId}) credential=${keycloakToJson(credential)}") | ||
175 | } | ||
176 | CredentialRepresentation newPasswordRepr = new CredentialRepresentation() | ||
177 | newPasswordRepr.setType(CredentialRepresentation.PASSWORD) | ||
178 | newPasswordRepr.setValue(newPassword) | ||
179 | newPasswordRepr.setTemporary(false) | ||
180 | userResource.resetPassword(newPasswordRepr) | ||
181 | } | ||
182 | |||
183 | } finally { | ||
184 | cleanupKeycloak() | ||
185 | } | ||
122 | } | 186 | } |
123 | 187 | ||
124 | Map<String, Object> joinUserAccountToKeycloak() { | 188 | Map<String, Object> updatePasswordInternal() { |
125 | String userId = context.userId | 189 | String userId = context.userId |
190 | String newPassword = context.newPassword | ||
191 | logger.info("updatePasswordInternal: userId:${userId} newPassword:${newPassword}") | ||
126 | 192 | ||
127 | String externalUserId = context.externalUserId | 193 | try { |
128 | if (externalUserId && externalUserId.startsWith('keycloak:')) { | 194 | EntityValue userAccount = ec.getEntity().find('UserAccount').condition('userId', userId).one() |
129 | // Already joined | 195 | String keycloakUserId = userAccount.externalUserId |
130 | return [:] | 196 | RealmResource realmResource = getKeycloakRealm() |
197 | UsersResource usersResource = realmResource.users() | ||
198 | UserResource userResource = usersResource.get(keycloakUserId) | ||
199 | List<CredentialRepresentation> credentials = userResource.credentials() | ||
200 | for (CredentialRepresentation credential: credentials) { | ||
201 | logger.info("keycloak(${keycloakUserId}) credential=${keycloakToJson(credential)}") | ||
202 | } | ||
203 | CredentialRepresentation newPasswordRepr = new CredentialRepresentation() | ||
204 | newPasswordRepr.setType(CredentialRepresentation.PASSWORD) | ||
205 | newPasswordRepr.setValue(newPassword) | ||
206 | newPasswordRepr.setTemporary(false) | ||
207 | userResource.resetPassword(newPasswordRepr) | ||
208 | |||
209 | } finally { | ||
210 | cleanupKeycloak() | ||
131 | } | 211 | } |
212 | } | ||
213 | |||
214 | // This does not add in the composite child roles | ||
215 | RoleResource syncUserGroup(String userGroupId) { | ||
216 | EntityValue userGroup = ec.getEntity().find('UserGroup').condition('userGroupId', userGroupId).one() | ||
217 | return configureClientRoleResource('UserGroup:' + userGroupId, userGroup.description, null, null) | ||
218 | } | ||
132 | 219 | ||
133 | Keycloak keycloak = KeycloakToolFactory.getInstance() | 220 | |
221 | Map<String, Object> syncUserGroup() { | ||
222 | String userGroupId = context.userGroupId | ||
134 | 223 | ||
135 | try { | 224 | try { |
136 | RealmResource realm = keycloak.realm('master') | 225 | EntityValue userGroup = ec.getEntity().find('UserGroup').condition('userGroupId', userGroupId).one() |
137 | UsersResource users = realm.users() | 226 | String keycloakRoleName = 'UserGroup:' + userGroupId |
138 | for (UserRepresentation user: users.list()) { | 227 | Set<RoleRepresentation> wantedRoles = new HashSet<RoleRepresentation>() |
139 | logger.info('keycloak user: ' + keycloakToJson(user)) | 228 | |
140 | } | 229 | List<EntityValue> userGroupPermissionList = ec.entity.find('UserGroupPermission') |
230 | .condition('userGroupId', userGroupId) | ||
231 | .useCache(false) | ||
232 | .disableAuthz() | ||
233 | .list() | ||
234 | .filterByDate('fromDate', 'thruDate', now) | ||
235 | |||
236 | wantedRoles.addAll(userGroupPermissionList*.userPermissionId.collect { userPermissionId -> syncUserPermission(userPermissionId).toRepresentation() }) | ||
237 | |||
238 | configureClientRoleResource(keycloakRoleName, userGroup.description, wantedRoles, null) | ||
141 | } finally { | 239 | } finally { |
142 | keycloak.close() | 240 | cleanupKeycloak() |
143 | } | 241 | } |
144 | 242 | ||
243 | return [:] | ||
145 | } | 244 | } |
146 | 245 | ||
147 | Map<String, Object> onUpdateUserAccount() { | 246 | RoleRepresentation getClientRoleRepresentation(String roleName) { |
148 | String userId = context.userId | 247 | Map<String, Object> sharedMap = ec.getContextRoot() |
149 | String externalUserId = context.externalUserId | 248 | if (sharedMap['keycloak:clientRole[' + roleName + ']']) { |
150 | if (!(externalUserId && externalUserId.startsWith('keycloak:'))) { | 249 | return sharedMap['keycloak:clientRole[' + roleName + ']'].toRepresentation() |
250 | } | ||
251 | ClientResource clientResource = getKeycloakClient() | ||
252 | RolesResource rolesResource = clientResource.roles() | ||
253 | RoleResource roleResource = rolesResource.get(roleName) | ||
254 | if (roleResource == null) { | ||
255 | rolesResource.create(new RoleRepresentation(roleName, null, false)) | ||
256 | roleResource = rolesResource.get(roleName) | ||
257 | } | ||
258 | return roleResource.toRepresentation() | ||
259 | } | ||
260 | |||
261 | RoleResource configureClientRoleResource(String roleName, String description, Set<RoleRepresentation> wantedRoles, Closure updater) { | ||
262 | String keycloakClientId = getKeycloakClientId() | ||
263 | Map<String, Object> sharedMap = ec.getContextRoot() | ||
264 | if (sharedMap['keycloak:clientRole[' + roleName + ']']) { | ||
265 | return sharedMap['keycloak:clientRole[' + roleName + ']'] | ||
266 | } | ||
267 | logger.info("configureClientRoleResource: ${roleName}: ${description}") | ||
268 | ClientResource clientResource = getKeycloakClient() | ||
269 | RolesResource rolesResource = clientResource.roles() | ||
270 | RoleResource roleResource = rolesResource.get(roleName) | ||
271 | try { | ||
272 | roleResource.toRepresentation() | ||
273 | } catch (javax.ws.rs.NotFoundException e) { | ||
274 | roleResource = null | ||
275 | } | ||
276 | //RoleRepresentation roleRepresentation = rolesResource.list(roleName, false)?[0] | ||
277 | |||
278 | boolean updated = false | ||
279 | //if (roleRepresentation == null) { | ||
280 | if (roleResource == null) { | ||
281 | rolesResource.create(new RoleRepresentation(roleName, description, false)) | ||
282 | roleResource = rolesResource.get(roleName) | ||
283 | //roleRepresentation = rolesResource.list(roleName, false).get(0) | ||
284 | } | ||
285 | RoleRepresentation roleRepresentation = roleResource.toRepresentation() | ||
286 | if (roleRepresentation.getDescription() != description) { | ||
287 | roleRepresentation.setDescription(description) | ||
288 | updated = true | ||
289 | } | ||
290 | if (updater) { | ||
291 | updated |= updater(roleRepresentation) | ||
292 | } | ||
293 | if (updated) { | ||
294 | logger.info("Sending update: ${roleRepresentation}") | ||
295 | roleResource.update(roleRepresentation) | ||
296 | } | ||
297 | |||
298 | if (wantedRoles != null) { | ||
299 | Set<RoleRepresentation> existingRoles = roleResource.getClientRoleComposites(keycloakClientId) | ||
300 | Set<RoleRepresentation> toAdd = new HashSet<RoleRepresentation>(wantedRoles) | ||
301 | toAdd.removeAll(existingRoles) | ||
302 | Set<RoleRepresentation> toRemove = new HashSet<RoleRepresentation>(existingRoles) | ||
303 | toRemove.removeAll(wantedRoles) | ||
304 | if (!toRemove.isEmpty()) { | ||
305 | roleResource.deleteComposites(toRemove as List) | ||
306 | } | ||
307 | if (!wantedRoles.isEmpty()) { | ||
308 | roleResource.addComposites(wantedRoles as List) | ||
309 | } | ||
310 | } | ||
311 | sharedMap['keycloak:clientRole[' + roleName + ']'] = roleResource | ||
312 | return roleResource | ||
313 | } | ||
314 | |||
315 | RoleResource syncUserPermission(String userPermissionId) { | ||
316 | EntityValue userPermission = ec.getEntity().find('UserPermission').condition('userPermissionId', userPermissionId).one() | ||
317 | return configureClientRoleResource('UserPermission:' + userPermissionId, userPermission.description, null, null) | ||
318 | } | ||
319 | |||
320 | Map<String, Object> syncUserPermission() { | ||
321 | String userPermissionId = context.userPermissionId | ||
322 | |||
323 | try { | ||
324 | syncUserPermission(userPermissionId) | ||
325 | } finally { | ||
326 | cleanupKeycloak() | ||
327 | } | ||
328 | |||
151 | return [:] | 329 | return [:] |
330 | } | ||
331 | |||
332 | RoleResource syncRoleType(String roleTypeId) { | ||
333 | EntityValue roleType = ec.getEntity().find('RoleType').condition('roleTypeId', roleTypeId).one() | ||
334 | return configureClientRoleResource('RoleType:' + roleTypeId, roleType.description, null, null) | ||
335 | } | ||
336 | |||
337 | Map<String, Object> syncRoleType() { | ||
338 | String roleTypeId = context.roleTypeId | ||
339 | |||
340 | try { | ||
341 | syncRoleType(roleTypeId) | ||
342 | } finally { | ||
343 | cleanupKeycloak() | ||
152 | } | 344 | } |
153 | String keycloakUserId = externalUserId.substring('keycloak:'.length()) | ||
154 | 345 | ||
346 | return [:] | ||
347 | } | ||
348 | |||
349 | RoleResource syncPartyClassification(String partyClassificationId) { | ||
350 | EntityValue partyClassification = ec.getEntity().find('PartyClassification').condition('partyClassificationId', partyClassificationId).one() | ||
351 | return configureClientRoleResource('PartyClassification:' + partyClassificationId, partyClassification.description, null, null) | ||
352 | } | ||
353 | |||
354 | Map<String, Object> syncPartyClassification() { | ||
355 | String partyClassificationId = context.partyClassificationId | ||
155 | 356 | ||
357 | try { | ||
358 | syncPartyClassification(partyClassificationId) | ||
359 | } finally { | ||
360 | cleanupKeycloak() | ||
361 | } | ||
362 | |||
363 | return [:] | ||
156 | } | 364 | } |
157 | 365 | ||
158 | Map<String, RoleRepresentation> getClientRoles(RealmResource realm, ClientResource clientResource, Collection<String> roleList) { | 366 | Map<String, RoleRepresentation> getClientRoles(RealmResource realm, ClientResource clientResource, Collection<String> roleList) { |
... | @@ -179,42 +387,46 @@ Map<String, RoleRepresentation> getClientRoles(RealmResource realm, ClientResour | ... | @@ -179,42 +387,46 @@ Map<String, RoleRepresentation> getClientRoles(RealmResource realm, ClientResour |
179 | String name = resultEntry.getKey() | 387 | String name = resultEntry.getKey() |
180 | if (resultEntry.getValue() == null) { | 388 | if (resultEntry.getValue() == null) { |
181 | rolesResource.create(new RoleRepresentation(name, "", false)) | 389 | rolesResource.create(new RoleRepresentation(name, "", false)) |
182 | resultEntry.setValue(rolesResource.get(name)) | 390 | resultEntry.setValue(rolesResource.get(name).toRepresentation()) |
183 | } | 391 | } |
184 | } | 392 | } |
185 | 393 | ||
186 | return result | 394 | return result |
187 | } | 395 | } |
188 | 396 | ||
397 | // return getKeycloakGroup(realm, "moqui:userGroup:${userGroup.userGroupId}", sync) | ||
398 | //} | ||
399 | |||
400 | void updateUser(String keycloakUserId, String userId, boolean enabled) { | ||
189 | 401 | ||
190 | void updateUser(RealmResource realm, String keycloakClientId, String keycloakUserId, String userId) { | 402 | RealmResource realmResource = getKeycloakRealm() |
191 | String clientId = realm.clients().findByClientId(keycloakClientId).get(0).getId() | 403 | String keycloakClientId = getKeycloakClientId() |
192 | logger.info("keycloakClientId=${keycloakClientId} clientId=${clientId}") | 404 | ClientResource clientResource = getKeycloakClient() |
193 | ClientResource clientResource = realm.clients().get(clientId) | ||
194 | 405 | ||
195 | UsersResource usersResource = realm.users() | 406 | UsersResource usersResource = realmResource.users() |
196 | UserResource userResource = usersResource.get(keycloakUserId) | 407 | UserResource userResource = usersResource.get(keycloakUserId) |
197 | 408 | ||
198 | Timestamp now = ec.user.nowTimestamp | 409 | Timestamp now = ec.user.nowTimestamp |
199 | 410 | ||
200 | List<EntityValue> userPermissionList = ec.entity.find('UserPermissionCheck') | 411 | EntityValue userAccount = ec.entity.find('UserAccount').condition('userId', userId).useCache(false).disableAuthz().one() |
201 | .condition('userId', userId) | 412 | String partyId = userAccount.partyId |
202 | .useCache(false) | 413 | EntityValue person = ec.entity.find('Person').condition('partyId', partyId).useCache(false).disableAuthz().one() |
203 | .disableAuthz() | 414 | |
204 | .list() | 415 | |
205 | .filterByDate('groupFromDate', 'groupThruDate', now) | 416 | |
206 | .filterByDate('permissionFromDate', 'permissionThruDate', now) | 417 | |
207 | List<String> moquiPermissions = userPermisionList*.userPermissionId.collect { permission -> 'permission:' + permission } | 418 | Set<RoleRepresentation> wantedClientRoles = new HashSet<RoleRepresentation>() |
208 | List<EntityValue> userGroupList = ec.entity.find('UserGroupMemberUser') | 419 | |
420 | List<EntityValue> userGroupMemberList = ec.entity.find('UserGroupMember') | ||
209 | .condition('userId', userId) | 421 | .condition('userId', userId) |
210 | .useCache(false) | 422 | .useCache(false) |
211 | .disableAuthz() | 423 | .disableAuthz() |
212 | .list() | 424 | .list() |
213 | .filterByDate('fromDate', 'thruDate', now) | 425 | .filterByDate('fromDate', 'thruDate', now) |
214 | Set<String> moquiGroups = userGroupList*.userGroupId.collect { group -> 'group:' + group } | 426 | Set<String> userGroupIds = new HashSet<String>(userGroupMemberList*.userGroupId) |
215 | moquiGroups.add('group:ALL_USERS') | 427 | logger.info("userGroupIds: ${userGroupIds}") |
216 | 428 | userGroupIds.add('ALL_USERS') | |
217 | Map<String, RoleRepresentation> wantedClientRoles = getClientRoles(realm, clientResource, moquiPermissions + moquiGroups) | 429 | wantedClientRoles.addAll(userGroupIds.collect { userGroupId -> syncUserGroup(userGroupId).toRepresentation() }) |
218 | 430 | ||
219 | UserRepresentation userRep = userResource.toRepresentation() | 431 | UserRepresentation userRep = userResource.toRepresentation() |
220 | Map<String, List<String>> attributes = new HashMap(userRep.getAttributes() ?: [:]) | 432 | Map<String, List<String>> attributes = new HashMap(userRep.getAttributes() ?: [:]) |
... | @@ -224,6 +436,10 @@ void updateUser(RealmResource realm, String keycloakClientId, String keycloakUse | ... | @@ -224,6 +436,10 @@ void updateUser(RealmResource realm, String keycloakClientId, String keycloakUse |
224 | 436 | ||
225 | logger.info("user[$userId]} attributes: " + attributes) | 437 | logger.info("user[$userId]} attributes: " + attributes) |
226 | userRep.setAttributes(attributes) | 438 | userRep.setAttributes(attributes) |
439 | if (person) { | ||
440 | userRep.setFirstName(person.firstName) | ||
441 | userRep.setLastName(person.lastName) | ||
442 | } | ||
227 | 443 | ||
228 | List<RoleRepresentation> toRemove = [] | 444 | List<RoleRepresentation> toRemove = [] |
229 | RoleMappingResource roleMappingResource = userResource.roles() | 445 | RoleMappingResource roleMappingResource = userResource.roles() |
... | @@ -233,64 +449,26 @@ void updateUser(RealmResource realm, String keycloakClientId, String keycloakUse | ... | @@ -233,64 +449,26 @@ void updateUser(RealmResource realm, String keycloakClientId, String keycloakUse |
233 | toRemove.add(existingRoleRep) | 449 | toRemove.add(existingRoleRep) |
234 | } | 450 | } |
235 | } | 451 | } |
236 | List<RoleRepresentation> toAdd = wantedClientRoles.values() as List | 452 | List<RoleRepresentation> toAdd = wantedClientRoles as List |
237 | logger.info("roles to remove: ${toRemove}") | 453 | logger.info("roles to remove: ${toRemove}") |
238 | logger.info("roles to add: ${toAdd}") | 454 | logger.info("roles to add: ${toAdd}") |
239 | RoleScopeResource clientRoleScopeResource = roleMappingResource.clientLevel(clientId) | 455 | RoleScopeResource clientRoleScopeResource = roleMappingResource.clientLevel(keycloakClientId) |
240 | clientRoleScopeResource.remove(toRemove) | 456 | clientRoleScopeResource.remove(toRemove) |
241 | clientRoleScopeResource.add(toAdd) | 457 | clientRoleScopeResource.add(toAdd) |
458 | if (enabled) { | ||
459 | userRep.setEnabled(true) | ||
460 | } | ||
242 | 461 | ||
243 | userResource.update(userRep) | 462 | userResource.update(userRep) |
244 | Map<String, Object> foo = userResource.impersonate() | 463 | Map<String, Object> foo = userResource.impersonate() |
245 | logger.info("impersonate: foo=${foo}") | 464 | logger.info("impersonate: foo=${foo}") |
246 | } | 465 | } |
247 | 466 | ||
248 | |||
249 | Map<String, Object> pushKeycloakUser() { | ||
250 | String userId = context.userId | ||
251 | EntityValue userLogin = ec.entity.find('UserLogin').condition('userId', userId).one() | ||
252 | |||
253 | |||
254 | List<EntityValue> logins | ||
255 | return [:] | ||
256 | |||
257 | /* | ||
258 | <view-entity entity-name="UserPermissionCheck" package="moqui.security"> | ||
259 | <member-entity entity-alias="UGM" entity-name="moqui.security.UserGroupMember"/> | ||
260 | <member-relationship entity-alias="UGP" join-from-alias="UGM" relationship="permissions"/> | ||
261 | <alias name="userGroupId" entity-alias="UGM"/> | ||
262 | <alias name="userId" entity-alias="UGM"/> | ||
263 | <alias name="userPermissionId" entity-alias="UGP"/> | ||
264 | <alias name="groupFromDate" entity-alias="UGM" field="fromDate"/> | ||
265 | <alias name="groupThruDate" entity-alias="UGM" field="thruDate"/> | ||
266 | <alias name="permissionFromDate" entity-alias="UGP" field="fromDate"/> | ||
267 | <alias name="permissionThruDate" entity-alias="UGP" field="thruDate"/> | ||
268 | </view-entity> | ||
269 | */ | ||
270 | } | ||
271 | |||
272 | Map<String, Object> getKeycloakUsers() { | 467 | Map<String, Object> getKeycloakUsers() { |
273 | String keycloakClientId = 'moqui' | ||
274 | |||
275 | KeycloakToolFactory keycloakToolFactory = ec.getFactory().getToolFactory('Keycloak') | ||
276 | Keycloak keycloak = keycloakToolFactory.getInstance() | ||
277 | |||
278 | try { | 468 | try { |
279 | RealmResource realm = keycloak.realm(keycloakToolFactory.getRealmName()) | 469 | updateUser('c6f99571-a79d-4267-b76e-a02a6847c8c9', '100000') |
280 | /* | ||
281 | ServerInfoResource serverInfo = keycloak.serverInfo() | ||
282 | logger.info('keycloak serverInfo: ' + keycloakToJson(serverInfo.getInfo())) | ||
283 | logger.info('keycloak keys: ' + keycloakToJson(realm.keys().getKeyMetadata())) | ||
284 | UsersResource users = realm.users() | ||
285 | for (UserRepresentation user: users.list()) { | ||
286 | logger.info('keycloak user: ' + keycloakToJson(user)) | ||
287 | } | ||
288 | */ | ||
289 | updateUser(realm, keycloakClientId, 'c6a4cb53-4533-4236-89e5-058967b9b90a', '100003') | ||
290 | logger.info("access token=${keycloakToJson(keycloak.tokenManager().getAccessToken())}") | ||
291 | |||
292 | } finally { | 470 | } finally { |
293 | keycloak.close() | 471 | cleanupKeycloak() |
294 | } | 472 | } |
295 | } | 473 | } |
296 | 474 | ... | ... |
... | @@ -14,6 +14,55 @@ along with this software (see the LICENSE.md file). If not, see | ... | @@ -14,6 +14,55 @@ along with this software (see the LICENSE.md file). If not, see |
14 | --> | 14 | --> |
15 | <services xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://moqui.org/xsd/service-definition-3.xsd"> | 15 | <services xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://moqui.org/xsd/service-definition-3.xsd"> |
16 | 16 | ||
17 | <service verb="send" noun="UserAccount" type="script" location="component://moqui-keycloak/service/keycloak/KeycloakServices.groovy" method="syncUserAccount" authenticate="anonymous-all"> | ||
18 | <in-parameters> | ||
19 | <parameter name="userId" type="String" required="true"/> | ||
20 | <parameter name="newPassword" required="false"/> | ||
21 | <parameter name="requirePasswordChange"/> | ||
22 | </in-parameters> | ||
23 | </service> | ||
24 | |||
25 | <service verb="update" noun="PasswordInternal" authenticate="false" allow-remote="false" type="script" location="component://moqui-keycloak/service/keycloak/KeycloakServices.groovy" method="updatePasswordInternal"> | ||
26 | <in-parameters> | ||
27 | <parameter name="userId" required="true"/> | ||
28 | <parameter name="newPassword" required="true"/> | ||
29 | <parameter name="requirePasswordChange"/> | ||
30 | </in-parameters> | ||
31 | </service> | ||
32 | |||
33 | <service verb="send" noun="UserGroup" type="script" location="component://moqui-keycloak/service/keycloak/KeycloakServices.groovy" method="syncUserGroup" authenticate="anonymous-all"> | ||
34 | <in-parameters> | ||
35 | <parameter name="userGroupId" type="String" required="true"/> | ||
36 | </in-parameters> | ||
37 | </service> | ||
38 | |||
39 | <service verb="send" noun="UserPermission" type="script" location="component://moqui-keycloak/service/keycloak/KeycloakServices.groovy" method="syncUserPermission" authenticate="anonymous-all"> | ||
40 | <in-parameters> | ||
41 | <parameter name="userPermissionId" type="String" required="true"/> | ||
42 | </in-parameters> | ||
43 | </service> | ||
44 | |||
45 | <service verb="send" noun="RoleType" type="script" location="component://moqui-keycloak/service/keycloak/KeycloakServices.groovy" method="syncRoleType" authenticate="anonymous-all"> | ||
46 | <in-parameters> | ||
47 | <parameter name="roleTypeId" type="String" required="true"/> | ||
48 | </in-parameters> | ||
49 | </service> | ||
50 | |||
51 | <service verb="send" noun="PartyClassification" type="script" location="component://moqui-keycloak/service/keycloak/KeycloakServices.groovy" method="syncPartyClassification" authenticate="anonymous-all"> | ||
52 | <in-parameters> | ||
53 | <parameter name="partyClassificationId" type="String" required="true"/> | ||
54 | </in-parameters> | ||
55 | </service> | ||
56 | |||
17 | <service verb="get" noun="KeycloakUsers" type="script" location="component://moqui-keycloak/service/keycloak/KeycloakServices.groovy" method="getKeycloakUsers"> | 57 | <service verb="get" noun="KeycloakUsers" type="script" location="component://moqui-keycloak/service/keycloak/KeycloakServices.groovy" method="getKeycloakUsers"> |
18 | </service> | 58 | </service> |
59 | |||
60 | <!-- | ||
61 | <service verb="update" noun="PasswordInternal" type="script" location="component://moqui-keycloak/service/keycloak/KeycloakServices.groovy" method="updatePasswordInternal" authenticate="anonymous-all"> | ||
62 | <in-parameters> | ||
63 | <parameter name="userId" type="String" required="true"/> | ||
64 | <parameter name="newPassword" type="String" required="true"/> | ||
65 | </in-parameters> | ||
66 | </service> | ||
67 | --> | ||
19 | </services> | 68 | </services> | ... | ... |
-
Please register or sign in to post a comment