61fe5e33 by Adam Heath

Hook based pushing of account data to keycloak.

1 parent b355489b
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>
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>
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
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>
...@@ -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>
......