61fe5e33 by Adam Heath

Hook based pushing of account data to keycloak.

1 parent b355489b
<?xml version="1.0" encoding="UTF-8"?>
<!--
This software is in the public domain under CC0 1.0 Universal plus a
Grant of Patent License.
To the extent possible under law, the author(s) have dedicated all
copyright and related and neighboring rights to this software to the
public domain worldwide. This software is distributed without any
warranty.
You should have received a copy of the CC0 Public Domain Dedication
along with this software (see the LICENSE.md file). If not, see
<http://creativecommons.org/publicdomain/zero/1.0/>.
-->
<eecas xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://moqui.org/xsd/entity-eca-3.xsd">
<!--
moqui.security.UserGroup
moqui.security.UserGroupMember
moqui.security.UserGroupPermission
moqui.security.UserPermission
mantle.party.PartyClassification
moqui.security.UserPermission -> userPermission:$id
mantle.party.PartyClassification -> partyClassification:$id
mantle.party.RoleType -> roleType:$id
moqui.security.UserAccount
moqui.security.UserAccount
mantle.party.Party
mantle.party.PartyClassificationAppl
mantle.party.PartyRole
mantle.party.Person
mantle.party.RoleType
mantle.party.contact.ContactMech
mantle.party.contact.TelecomNumber
mantle.party.contact.PostalAddress
mantle.party.contact.PartyContactMech
Future use:
mantle.party.PartyRelationship
-->
<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">
<actions><service-call name="keycloak.HookServices.handle#EntityUpdate" in-map="[entityName: 'ContactMech', value: contactMechId]"/></actions>
</eeca>
<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">
<actions><service-call name="keycloak.HookServices.handle#EntityUpdate" in-map="[entityName: 'Party', value: partyId]"/></actions>
</eeca>
<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">
<actions><service-call name="keycloak.HookServices.handle#EntityUpdate" in-map="[entityName: 'PartyClassification', value: partyClassificationId]"/></actions>
</eeca>
<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">
<actions><service-call name="keycloak.HookServices.handle#EntityUpdate" in-map="[entityName: 'Party', value: partyId]"/></actions>
</eeca>
<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">
<actions><service-call name="keycloak.HookServices.handle#EntityUpdate" in-map="[entityName: 'Party', value: partyId]"/></actions>
</eeca>
<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">
<actions><service-call name="keycloak.HookServices.handle#EntityUpdate" in-map="[entityName: 'Party', value: partyId]"/></actions>
</eeca>
<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">
<actions><service-call name="keycloak.HookServices.handle#EntityUpdate" in-map="[entityName: 'Party', value: partyId]"/></actions>
</eeca>
<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">
<actions><service-call name="keycloak.HookServices.handle#EntityUpdate" in-map="[entityName: 'ContactMech', value: contactMechId]"/></actions>
</eeca>
<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">
<actions><service-call name="keycloak.HookServices.handle#EntityUpdate" in-map="[entityName: 'RoleType', value: roleTypeId]"/></actions>
</eeca>
<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">
<actions><service-call name="keycloak.HookServices.handle#EntityUpdate" in-map="[entityName: 'ContactMech', value: contactMechId]"/></actions>
</eeca>
<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">
<actions><service-call name="keycloak.HookServices.handle#EntityUpdate" in-map="[entityName: 'UserAccount', value: userId]"/></actions>
</eeca>
<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">
<actions><service-call name="keycloak.HookServices.handle#EntityUpdate" in-map="[entityName: 'UserGroup', value: userGroupId]"/></actions>
</eeca>
<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">
<actions><service-call name="keycloak.HookServices.handle#EntityUpdate" in-map="[entityName: 'UserAccount', value: userId]"/></actions>
</eeca>
<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">
<actions><service-call name="keycloak.HookServices.handle#EntityUpdate" in-map="[entityName: 'UserGroup', value: userGroupId]"/></actions>
</eeca>
<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">
<actions><service-call name="keycloak.HookServices.handle#EntityUpdate" in-map="[entityName: 'UserPermission', value: userPermissionId]"/></actions>
</eeca>
</eecas>
<screen-extend xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://moqui.org/xsd/xml-screen-3.xsd">
<form-single name="UpdateUserAccount">
<field name="externalUserId"><default-field><text-line size="60"/></default-field></field>
</form-single>
</screen-extend>
<screen-extend xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://moqui.org/xsd/xml-screen-3.xsd">
<form-single name="UpdateUserAccount">
<field name="externalUserId"><default-field><text-line size="60"/></default-field></field>
<field-layout>
<fields-not-referenced/>
<field-row><field-ref name="externalUserId"/></field-row>
<field-row><field-ref name="requirePasswordChange"/><field-ref name="disabled"/></field-row>
<field-row><field-ref name="successiveFailedLogins"/><field-ref name="disabledDateTime"/></field-row>
<field-ref name="submitButton"/>
</field-layout>
</form-single>
<!--
-->
</screen-extend>
<?xml version="1.0" encoding="UTF-8"?>
<!--
This software is in the public domain under CC0 1.0 Universal plus a
Grant of Patent License.
To the extent possible under law, the author(s) have dedicated all
copyright and related and neighboring rights to this software to the
public domain worldwide. This software is distributed without any
warranty.
You should have received a copy of the CC0 Public Domain Dedication
along with this software (see the LICENSE.md file). If not, see
<http://creativecommons.org/publicdomain/zero/1.0/>.
-->
<secas xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://moqui.org/xsd/service-eca-3.xsd">
<!-- Invoice GL Posting -->
<seca id="updatePasswordInternal" service="org.moqui.impl.UserServices.update#PasswordInternal" when="post-service" run-on-error="false">
<condition><expression>updateSuccessful</expression></condition>
<actions><service-call name="keycloak.HookServices.handle#UpdatePasswordInternal" in-map="context"/></actions>
</seca>
<!--
<service verb="update" noun="PasswordInternal" authenticate="false" allow-remote="false">
<in-parameters>
<parameter name="userId" required="true"/>
<parameter name="newPassword" required="true"/>
<parameter name="newPasswordVerify" required="true"/>
<parameter name="requirePasswordChange" default-value="N"/>
</in-parameters>
<out-parameters>
<parameter name="passwordIssues" type="Boolean"/>
<parameter name="updateSuccessful" type="Boolean"/>
</out-parameters>
-->
</secas>
import javax.transaction.Synchronization
import javax.transaction.xa.XAResource
import javax.transaction.xa.Xid
import groovy.transform.Field
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.moqui.util.RestClient
import org.moqui.util.ObjectUtilities
import org.moqui.context.ExecutionContext
import org.moqui.entity.EntityCondition
import org.moqui.entity.EntityFind
import org.moqui.entity.EntityList
import org.moqui.entity.EntityValue
@Field Logger logger = LoggerFactory.getLogger(getClass().getName())
//'HookServices')
class HookSynchronization implements Synchronization {
private final ExecutionContext ec;
private static class EntityToken {
protected final String entityName
protected final String keyName
protected final Object keyValue
protected final Map<String, Object> extraParameters = new HashMap<String, Object>()
protected EntityToken(String entityName, String keyName, Object keyValue) {
this.entityName = entityName
this.keyName = keyName
this.keyValue = keyValue
}
public boolean equals(Object other) {
if (!(other instanceof EntityToken)) {
return false
}
EntityToken that = (EntityToken) other
return entityName.equals(that.entityName) && keyName.equals(that.keyName) && keyValue.equals(that.keyValue)
}
public int hashCode() {
return entityName.hashCode() ^ keyName.hashCode() ^ keyValue.hashCode()
}
}
private Map<EntityToken, Map<String, Object>> updates = new LinkedHashMap<>()
protected HookSynchronization(ExecutionContext ec) {
this.ec = ec
}
protected void add(String entityName, String keyName, Object keyValue, Map<String, Object> extraParameters) {
EntityToken entityToken = new EntityToken(entityName, keyName, keyValue)
// This always move the value to the end of the list
Map<String, Object> existingExtraParameters = updates.remove(entityToken)
Map<String, Object> newExtraParameters = [:]
if (existingExtraParameters) {
newExtraParameters.putAll(existingExtraParameters)
}
if (extraParameters) {
newExtraParameters.putAll(extraParameters)
}
updates.put(entityToken, newExtraParameters)
}
@Override
public void beforeCompletion() {
List<Map<String, Object>> updates = []
for (Map.Entry<EntityToken, Map<String, Object>> entry: this.updates.entrySet()) {
EntityToken token = entry.getKey()
Map<String, Object> extraParameters = entry.getValue()
updates.add([
entityName: token.entityName,
keyName: token.keyName,
keyValue: token.keyValue,
extraParameters: extraParameters,
])
}
ec.getService().async().name("keycloak.HookServices.process#Updates").parameter('updates', updates).call()
}
@Override
public void afterCompletion(int status) {
}
}
/*
class HookXAResource implements XAResource {
private static class EntityPk {
protected String entityName;
protected Map<String, Object> pk;
protected EntityPk(EntityValue entity) {
this.entityName = entity.getEntityName()
this.pk = entity.getPrimaryKeys()
}
public boolean equals(Object other) {
if (!(object instanceof EntityPk)) {
return false
}
EntityPk that = (EntityPk) other
return entityName.equals(that.entityName) && pk.equals(that.pk)
}
public int hashCode() {
return entityName.hashCode ^ pk.hashCode()
}
}
private Map<Xid, Set<EntityPk>> modifiedValues = new HashMap<>()
private int timeout = 60
@Override
public void commit(Xid xid, boolean onePhase) {
}
@Override
public void end(Xid xid, int flags) {
if (flags & TMFAIL) {
} else if (flags & TMSUSPEND) {
} else if (flags & TMSUCCESS) {
}
}
@Override
public void forget(Xid xid) {
modifiedValues.remove(xid)
}
@Override
public int getTransactionTimeout() {
return timeout
}
@Override
public boolean isSameRM(XAResource xar) {
return getClass().equals(xar.getClass())
}
@Override
public int prepare(Xid xid) {
if (!keycloak.connected()) {
throw new XAException(XAException.XAERR_RMFAIL)
}
}
@Override
public Xid[] recover(int flag) {
}
@Override
public void rollback(Xid xid) {
}
@Override
public boolean setTransactionTimeout(int timeout) {
this.timeout = timeout
}
@Override
public void start(Xid xid, int flags) {
}
}
*/
HookSynchronization getHookSync() {
HookSynchronization hookSync = ec.transaction.getActiveSynchronization(getClass().getName())
if (hookSync == null) {
hookSync = new HookSynchronization(ec)
ec.transaction.putAndEnlistActiveSynchronization(getClass().getName(), hookSync)
}
return hookSync
}
Map<String, Object> handleEntityUpdate() {
logger.info("handleEntityUpdate: ${context.entityName}[${context.value}]")
logger.info("context: ${context}")
ExecutionContext ec = context.ec
Map<String, Object> contextRoot = ec.getContextRoot()
Map<String, Set<Object>> toRegister = contextRoot['KeycloakEntityRegistrations']
logger.info("toRegister: ${toRegister}")
if (toRegister == null) {
toRegister = contextRoot['KeycloakEntityRegistrations'] = [:]
}
HookSynchronization hookSync = getHookSync()
List<Map<String, Object>> queue = [[entityName: context.entityName, value: context.value]]
while (!queue.isEmpty()) {
Map<String, Object> entry = queue.remove(0)
logger.info("processing entry: ${entry}")
String entityName = entry.entityName
Object value = entry.value
Set<Object> entityRegistrations = toRegister[entityName]
if (entityRegistrations == null) {
entityRegistrations = toRegister[entityName] = new HashSet()
entityRegistrations.add(value)
} else if (entityRegistrations.contains(value)) {
continue
}
String keyName
switch (entityName) {
case 'Party':
EntityList userAccounts = ec.entity.find('UserAccount').condition('partyId', value).list()
for (EntityValue userAccount: userAccounts) {
queue.add([entityName: 'UserAccount', value: userAccount.userId])
}
continue
case 'ContactMech':
EntityList partyContactMechs = ec.entity.find('PartyContactMech').condition('contactMechId', value).list()
for (EntityValue partyContactMech: partyContactMechs) {
queue.add([entityName: 'Party', value: partyContactMech.partyId])
}
continue
case 'UserAccount':
keyName = 'userId'
break
case 'UserGroup':
keyName = 'userGroupId'
break
case 'UserGroupMember':
keyName = 'userGroupId'
break
case 'UserPermission':
keyName = 'userPermissionId'
break
case 'PartyClassification':
keyName = 'partyClassificationId'
break
case 'RoleType':
keyName = 'roleTypeId'
break
}
ec.getLogger().info("Registered synchronization for ${entityName}: [${value}]")
hookSync.add(entityName, keyName, value, [:])
//ec.getService().special().name("keycloak.KeycloakServices.send#${entityName}").parameter(keyName, value).registerOnCommit()
}
}
Map<String, Object> handleUpdatePasswordInternal() {
String userId = ec.context.userId
String newPassword = ec.context.newPassword
Boolean requirePasswordChange = ec.context.requirePasswordChange
ec.getLogger().info("Registered synchronization for update#PasswordInternal: [${userId}]")
HookSynchronization hookSync = getHookSync()
hookSync.add('UserAccount', 'userId', userId, [
newPassword: newPassword,
requirePasswordChange: requirePasswordChange,
])
}
Map<String, Object> processUpdates() {
List<Map<String, Object>> updates = context.updates
for (Map<String, Object> update: updates) {
Map<String, Object> parameters = [:]
parameters[update.keyName] = update.keyValue
parameters.putAll(update.extraParameters)
ec.getService().sync().name("keycloak.KeycloakServices.send#${update.entityName}").parameters(parameters).call()
}
return [:]
}
<?xml version="1.0" encoding="UTF-8"?>
<!--
This software is in the public domain under CC0 1.0 Universal plus a
Grant of Patent License.
To the extent possible under law, the author(s) have dedicated all
copyright and related and neighboring rights to this software to the
public domain worldwide. This software is distributed without any
warranty.
You should have received a copy of the CC0 Public Domain Dedication
along with this software (see the LICENSE.md file). If not, see
<http://creativecommons.org/publicdomain/zero/1.0/>.
-->
<services xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://moqui.org/xsd/service-definition-3.xsd">
<service verb="handle" noun="EntityUpdate" type="script" location="component://moqui-keycloak/service/keycloak/HookServices.groovy" method="handleEntityUpdate" authenticate="anonymous-all">
<in-parameters>
<parameter name="entityName" type="String" required="true"/>
<parameter name="value" type="Object" required="true"/>
</in-parameters>
</service>
<service verb="handle" noun="UpdatePasswordInternal" type="script" location="component://moqui-keycloak/service/keycloak/HookServices.groovy" method="handleUpdatePasswordInternal" authenticate="anonymous-all">
<in-parameters>
<parameter name="userId" required="true"/>
<parameter name="newPassword" required="true"/>
<parameter name="requirePasswordChange"/>
</in-parameters>
</service>
<service verb="process" noun="Updates" type="script" location="component://moqui-keycloak/service/keycloak/HookServices.groovy" method="processUpdates" authenticate="anonymous-all">
<in-parameters>
<parameter name="updates" type="List" required="true"/>
</in-parameters>
</service>
</services>
......@@ -14,6 +14,55 @@ along with this software (see the LICENSE.md file). If not, see
-->
<services xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://moqui.org/xsd/service-definition-3.xsd">
<service verb="send" noun="UserAccount" type="script" location="component://moqui-keycloak/service/keycloak/KeycloakServices.groovy" method="syncUserAccount" authenticate="anonymous-all">
<in-parameters>
<parameter name="userId" type="String" required="true"/>
<parameter name="newPassword" required="false"/>
<parameter name="requirePasswordChange"/>
</in-parameters>
</service>
<service verb="update" noun="PasswordInternal" authenticate="false" allow-remote="false" type="script" location="component://moqui-keycloak/service/keycloak/KeycloakServices.groovy" method="updatePasswordInternal">
<in-parameters>
<parameter name="userId" required="true"/>
<parameter name="newPassword" required="true"/>
<parameter name="requirePasswordChange"/>
</in-parameters>
</service>
<service verb="send" noun="UserGroup" type="script" location="component://moqui-keycloak/service/keycloak/KeycloakServices.groovy" method="syncUserGroup" authenticate="anonymous-all">
<in-parameters>
<parameter name="userGroupId" type="String" required="true"/>
</in-parameters>
</service>
<service verb="send" noun="UserPermission" type="script" location="component://moqui-keycloak/service/keycloak/KeycloakServices.groovy" method="syncUserPermission" authenticate="anonymous-all">
<in-parameters>
<parameter name="userPermissionId" type="String" required="true"/>
</in-parameters>
</service>
<service verb="send" noun="RoleType" type="script" location="component://moqui-keycloak/service/keycloak/KeycloakServices.groovy" method="syncRoleType" authenticate="anonymous-all">
<in-parameters>
<parameter name="roleTypeId" type="String" required="true"/>
</in-parameters>
</service>
<service verb="send" noun="PartyClassification" type="script" location="component://moqui-keycloak/service/keycloak/KeycloakServices.groovy" method="syncPartyClassification" authenticate="anonymous-all">
<in-parameters>
<parameter name="partyClassificationId" type="String" required="true"/>
</in-parameters>
</service>
<service verb="get" noun="KeycloakUsers" type="script" location="component://moqui-keycloak/service/keycloak/KeycloakServices.groovy" method="getKeycloakUsers">
</service>
<!--
<service verb="update" noun="PasswordInternal" type="script" location="component://moqui-keycloak/service/keycloak/KeycloakServices.groovy" method="updatePasswordInternal" authenticate="anonymous-all">
<in-parameters>
<parameter name="userId" type="String" required="true"/>
<parameter name="newPassword" type="String" required="true"/>
</in-parameters>
</service>
-->
</services>
......