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>
......@@ -2,21 +2,12 @@ import groovy.json.JsonOutput
import groovy.transform.Field
import java.sql.Timestamp
import java.util.Base64
import java.util.UUID
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.PBEParameterSpec;
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import groovy.json.JsonSlurper
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
......@@ -25,19 +16,22 @@ import org.moqui.entity.EntityValue
import org.moqui.keycloak.KeycloakToolFactory
import org.keycloak.OAuth2Constants
import org.keycloak.admin.client.Keycloak
import org.keycloak.admin.client.KeycloakBuilder
import org.keycloak.admin.client.resource.ClientResource
import org.keycloak.admin.client.resource.GroupsResource
import org.keycloak.admin.client.resource.RealmResource
import org.keycloak.admin.client.resource.RoleMappingResource
import org.keycloak.admin.client.resource.RoleScopeResource
import org.keycloak.admin.client.resource.RoleResource
import org.keycloak.admin.client.resource.RolesResource
import org.keycloak.admin.client.resource.ServerInfoResource
import org.keycloak.admin.client.resource.UserResource
import org.keycloak.admin.client.resource.UsersResource
import org.keycloak.representations.idm.ClientMappingsRepresentation
import org.keycloak.representations.idm.CredentialRepresentation
import org.keycloak.representations.idm.GroupRepresentation
import org.keycloak.representations.idm.MappingsRepresentation
import org.keycloak.representations.idm.RoleRepresentation
import org.keycloak.representations.idm.UserRepresentation
......@@ -53,106 +47,320 @@ String keycloakToJson(Object o) {
return JsonSerialization.writeValueAsString(o)
}
Map<String, Object> buildClientConsent() {
return [
clientId: null,
createdDate: null, // int64
grantedClientScopes: [], // array[string]
lastUpdatedData: null, // int64
]
void cleanupKeycloak() {
Map<String, Object> sharedMap = ec.getContextRoot()
Keycloak keycloak = sharedMap['keycloak']
if (keycloak != null) {
keycloak.close()
}
Iterator<String> keyIt = sharedMap.keySet().iterator()
while (keyIt.hasNext()) {
String key = keyIt.next()
if (key == 'keycloak' || key.startsWith('keycloak:')) {
keyIt.remove()
}
}
}
Map<String, Object> buildCredential() {
return [
createdDate: null, // int64
credentialData: null, // string
id: null, // string
priority: null, // int32
secretData: null, // string
temporary: null, // boolean
type: null, // string
userLabel: null, // string
value: null, // string
]
Keycloak getKeycloak() {
Map<String, Object> sharedMap = ec.getContextRoot()
KeycloakToolFactory keycloakToolFactory = ec.getFactory().getToolFactory('Keycloak')
Keycloak keycloak = sharedMap['keycloak']
if (keycloak == null) {
keycloak = keycloakToolFactory.getInstance()
sharedMap['keycloak'] = keycloak
}
return keycloak
}
Map<String, Object> buildFederatedIdentiy() {
return [
identityProvider: null, // string
userId: null, // string
userName: null, // string
]
RealmResource getKeycloakRealm() {
Map<String, Object> sharedMap = ec.getContextRoot()
RealmResource keycloakRealm = sharedMap['keycloak:realm']
if (!realm) {
Keycloak keycloak = getKeycloak()
KeycloakToolFactory keycloakToolFactory = ec.getFactory().getToolFactory('Keycloak')
keycloakRealm = keycloak.realm(keycloakToolFactory.getRealmName())
sharedMap['keycloak:realm'] = keycloakRealm
}
return keycloakRealm
}
String getKeycloakClientId() {
Map<String, Object> sharedMap = ec.getContextRoot()
String keycloakClientId = sharedMap['keycloak:clientId']
if (!keycloakClientId) {
RealmResource keycloakRealm = getKeycloakRealm()
KeycloakToolFactory keycloakToolFactory = ec.getFactory().getToolFactory('Keycloak')
keycloakClientId = keycloakRealm.clients().findByClientId(keycloakToolFactory.getClientId()).get(0).getId()
sharedMap['keycloak:clientId'] = keycloakClientId
}
return keycloakClientId
}
Map<String, Object> buildKeycloakUser(String userId) {
EntityValue userLogin = ec.entity.find('UserLogin').condition('userId', userId)
Map<String, Object> keycloakUser = [
access: [:],
attributes: [:],
clientConsents: [],
clientRoles: [:],
createdTimestamp: null, // int64
credentials: [],
disableableCredentialTypes: [], // array[string]
email: null, // string
emailVerified: null, // boolean
enabled: null, // boolean
federatedIdentities: [],
federationLink: null, // string
firstName: null,
groups: [], // array[string]
id: null, // string
lastName: null,
notBefore: null, // int32
origin: null, // string
realmRoles: [], // array[string]
requiredActions: [], // array[string]
self: null, // string
serviceClientId: null, // string
username: null, // string
]
return keycloakUser
ClientResource getKeycloakClient() {
Map<String, Object> sharedMap = ec.getContextRoot()
ClientResource keycloakClient = sharedMap['keycloak:client']
if (!keycloakClient) {
String keycloakClientId = getKeycloakClientId()
RealmResource realm = getKeycloakRealm()
keycloakClient = realm.clients().get(keycloakClientId)
sharedMap['keycloak:client'] = keycloakClient
}
return keycloakClient
}
Map<String, Object> onUpdateEmailAddress() {
Map<String, Object> syncUserAccount() {
String userId = context.userId
String newPassword = context.newPassword
try {
EntityValue userAccount = ec.getEntity().find('UserAccount').condition('userId', userId).one()
if (!userAccount.username) {
ec.message.addMessage("Skipping userAccount with empty username: ${userId})")
return
}
RealmResource realmResource = getKeycloakRealm()
UsersResource usersResource = realmResource.users()
if (userAccount.externalUserId == null) {
logger.info("Searching for existing user: ${userAccount.username}")
List<UserRepresentation> existingByUsername = usersResource.search(userAccount.username, true)
logger.info("Found: ${existingByUsername}")
if (!existingByUsername.isEmpty()) {
// FIXME: disable eeca
boolean reenableEeca = ec.artifactExecutionFacade.disableEntityEca()
try {
userAccount.externalUserId = existingByUsername[0].getId()
userAccount.store()
} finally {
if (reenableEeca) ec.artifactExecutionFacade.enableEntityEca()
}
}
}
if (userAccount.externalUserId == null) {
UserRepresentation newKeycloakUser = new UserRepresentation()
newKeycloakUser.setUsername(userAccount.username)
newKeycloakUser.setEnabled(false)
newKeycloakUser.setEmail(userAccount.emailAddress)
Object response = usersResource.create(newKeycloakUser)
logger.info("create response: ${keycloakToJson(response)}")
logger.info("status=${response.status}")
if (response.status == 201) {
List<UserRepresentation> newlyCreatedUserList = usersResource.search(userAccount.username)
if (newlyCreatedUserList.size() == 1) {
// FIXME: disable eeca
boolean reenableEeca = ec.artifactExecutionFacade.disableEntityEca()
try {
userAccount.externalUserId = newlyCreatedUserList[0].getId()
userAccount.store()
} finally {
if (reenableEeca) ec.artifactExecutionFacade.enableEntityEca()
}
}
}
if (userAccount.externalUserId == null) {
ec.message.addError("Couldn't create keycloak user for: ${userAccount.username})")
return
}
}
String keycloakUserId = userAccount.externalUserId
logger.info("Would update: keycloak(${keycloakUserId})")
updateUser(keycloakUserId, userId, newPassword != null)
if (newPassword != null) {
UserResource userResource = usersResource.get(keycloakUserId)
List<CredentialRepresentation> credentials = userResource.credentials()
for (CredentialRepresentation credential: credentials) {
logger.info("keycloak(${keycloakUserId}) credential=${keycloakToJson(credential)}")
}
CredentialRepresentation newPasswordRepr = new CredentialRepresentation()
newPasswordRepr.setType(CredentialRepresentation.PASSWORD)
newPasswordRepr.setValue(newPassword)
newPasswordRepr.setTemporary(false)
userResource.resetPassword(newPasswordRepr)
}
} finally {
cleanupKeycloak()
}
}
Map<String, Object> joinUserAccountToKeycloak() {
Map<String, Object> updatePasswordInternal() {
String userId = context.userId
String newPassword = context.newPassword
logger.info("updatePasswordInternal: userId:${userId} newPassword:${newPassword}")
try {
EntityValue userAccount = ec.getEntity().find('UserAccount').condition('userId', userId).one()
String keycloakUserId = userAccount.externalUserId
RealmResource realmResource = getKeycloakRealm()
UsersResource usersResource = realmResource.users()
UserResource userResource = usersResource.get(keycloakUserId)
List<CredentialRepresentation> credentials = userResource.credentials()
for (CredentialRepresentation credential: credentials) {
logger.info("keycloak(${keycloakUserId}) credential=${keycloakToJson(credential)}")
}
CredentialRepresentation newPasswordRepr = new CredentialRepresentation()
newPasswordRepr.setType(CredentialRepresentation.PASSWORD)
newPasswordRepr.setValue(newPassword)
newPasswordRepr.setTemporary(false)
userResource.resetPassword(newPasswordRepr)
} finally {
cleanupKeycloak()
}
}
// This does not add in the composite child roles
RoleResource syncUserGroup(String userGroupId) {
EntityValue userGroup = ec.getEntity().find('UserGroup').condition('userGroupId', userGroupId).one()
return configureClientRoleResource('UserGroup:' + userGroupId, userGroup.description, null, null)
}
Map<String, Object> syncUserGroup() {
String userGroupId = context.userGroupId
try {
EntityValue userGroup = ec.getEntity().find('UserGroup').condition('userGroupId', userGroupId).one()
String keycloakRoleName = 'UserGroup:' + userGroupId
Set<RoleRepresentation> wantedRoles = new HashSet<RoleRepresentation>()
String externalUserId = context.externalUserId
if (externalUserId && externalUserId.startsWith('keycloak:')) {
// Already joined
return [:]
List<EntityValue> userGroupPermissionList = ec.entity.find('UserGroupPermission')
.condition('userGroupId', userGroupId)
.useCache(false)
.disableAuthz()
.list()
.filterByDate('fromDate', 'thruDate', now)
wantedRoles.addAll(userGroupPermissionList*.userPermissionId.collect { userPermissionId -> syncUserPermission(userPermissionId).toRepresentation() })
configureClientRoleResource(keycloakRoleName, userGroup.description, wantedRoles, null)
} finally {
cleanupKeycloak()
}
Keycloak keycloak = KeycloakToolFactory.getInstance()
return [:]
}
RoleRepresentation getClientRoleRepresentation(String roleName) {
Map<String, Object> sharedMap = ec.getContextRoot()
if (sharedMap['keycloak:clientRole[' + roleName + ']']) {
return sharedMap['keycloak:clientRole[' + roleName + ']'].toRepresentation()
}
ClientResource clientResource = getKeycloakClient()
RolesResource rolesResource = clientResource.roles()
RoleResource roleResource = rolesResource.get(roleName)
if (roleResource == null) {
rolesResource.create(new RoleRepresentation(roleName, null, false))
roleResource = rolesResource.get(roleName)
}
return roleResource.toRepresentation()
}
RoleResource configureClientRoleResource(String roleName, String description, Set<RoleRepresentation> wantedRoles, Closure updater) {
String keycloakClientId = getKeycloakClientId()
Map<String, Object> sharedMap = ec.getContextRoot()
if (sharedMap['keycloak:clientRole[' + roleName + ']']) {
return sharedMap['keycloak:clientRole[' + roleName + ']']
}
logger.info("configureClientRoleResource: ${roleName}: ${description}")
ClientResource clientResource = getKeycloakClient()
RolesResource rolesResource = clientResource.roles()
RoleResource roleResource = rolesResource.get(roleName)
try {
RealmResource realm = keycloak.realm('master')
UsersResource users = realm.users()
for (UserRepresentation user: users.list()) {
logger.info('keycloak user: ' + keycloakToJson(user))
roleResource.toRepresentation()
} catch (javax.ws.rs.NotFoundException e) {
roleResource = null
}
//RoleRepresentation roleRepresentation = rolesResource.list(roleName, false)?[0]
boolean updated = false
//if (roleRepresentation == null) {
if (roleResource == null) {
rolesResource.create(new RoleRepresentation(roleName, description, false))
roleResource = rolesResource.get(roleName)
//roleRepresentation = rolesResource.list(roleName, false).get(0)
}
RoleRepresentation roleRepresentation = roleResource.toRepresentation()
if (roleRepresentation.getDescription() != description) {
roleRepresentation.setDescription(description)
updated = true
}
if (updater) {
updated |= updater(roleRepresentation)
}
if (updated) {
logger.info("Sending update: ${roleRepresentation}")
roleResource.update(roleRepresentation)
}
if (wantedRoles != null) {
Set<RoleRepresentation> existingRoles = roleResource.getClientRoleComposites(keycloakClientId)
Set<RoleRepresentation> toAdd = new HashSet<RoleRepresentation>(wantedRoles)
toAdd.removeAll(existingRoles)
Set<RoleRepresentation> toRemove = new HashSet<RoleRepresentation>(existingRoles)
toRemove.removeAll(wantedRoles)
if (!toRemove.isEmpty()) {
roleResource.deleteComposites(toRemove as List)
}
if (!wantedRoles.isEmpty()) {
roleResource.addComposites(wantedRoles as List)
}
}
sharedMap['keycloak:clientRole[' + roleName + ']'] = roleResource
return roleResource
}
RoleResource syncUserPermission(String userPermissionId) {
EntityValue userPermission = ec.getEntity().find('UserPermission').condition('userPermissionId', userPermissionId).one()
return configureClientRoleResource('UserPermission:' + userPermissionId, userPermission.description, null, null)
}
Map<String, Object> syncUserPermission() {
String userPermissionId = context.userPermissionId
try {
syncUserPermission(userPermissionId)
} finally {
keycloak.close()
cleanupKeycloak()
}
return [:]
}
Map<String, Object> onUpdateUserAccount() {
String userId = context.userId
String externalUserId = context.externalUserId
if (!(externalUserId && externalUserId.startsWith('keycloak:'))) {
return [:]
RoleResource syncRoleType(String roleTypeId) {
EntityValue roleType = ec.getEntity().find('RoleType').condition('roleTypeId', roleTypeId).one()
return configureClientRoleResource('RoleType:' + roleTypeId, roleType.description, null, null)
}
Map<String, Object> syncRoleType() {
String roleTypeId = context.roleTypeId
try {
syncRoleType(roleTypeId)
} finally {
cleanupKeycloak()
}
String keycloakUserId = externalUserId.substring('keycloak:'.length())
return [:]
}
RoleResource syncPartyClassification(String partyClassificationId) {
EntityValue partyClassification = ec.getEntity().find('PartyClassification').condition('partyClassificationId', partyClassificationId).one()
return configureClientRoleResource('PartyClassification:' + partyClassificationId, partyClassification.description, null, null)
}
Map<String, Object> syncPartyClassification() {
String partyClassificationId = context.partyClassificationId
try {
syncPartyClassification(partyClassificationId)
} finally {
cleanupKeycloak()
}
return [:]
}
Map<String, RoleRepresentation> getClientRoles(RealmResource realm, ClientResource clientResource, Collection<String> roleList) {
......@@ -179,42 +387,46 @@ Map<String, RoleRepresentation> getClientRoles(RealmResource realm, ClientResour
String name = resultEntry.getKey()
if (resultEntry.getValue() == null) {
rolesResource.create(new RoleRepresentation(name, "", false))
resultEntry.setValue(rolesResource.get(name))
resultEntry.setValue(rolesResource.get(name).toRepresentation())
}
}
return result
}
// return getKeycloakGroup(realm, "moqui:userGroup:${userGroup.userGroupId}", sync)
//}
void updateUser(String keycloakUserId, String userId, boolean enabled) {
void updateUser(RealmResource realm, String keycloakClientId, String keycloakUserId, String userId) {
String clientId = realm.clients().findByClientId(keycloakClientId).get(0).getId()
logger.info("keycloakClientId=${keycloakClientId} clientId=${clientId}")
ClientResource clientResource = realm.clients().get(clientId)
RealmResource realmResource = getKeycloakRealm()
String keycloakClientId = getKeycloakClientId()
ClientResource clientResource = getKeycloakClient()
UsersResource usersResource = realm.users()
UsersResource usersResource = realmResource.users()
UserResource userResource = usersResource.get(keycloakUserId)
Timestamp now = ec.user.nowTimestamp
List<EntityValue> userPermissionList = ec.entity.find('UserPermissionCheck')
.condition('userId', userId)
.useCache(false)
.disableAuthz()
.list()
.filterByDate('groupFromDate', 'groupThruDate', now)
.filterByDate('permissionFromDate', 'permissionThruDate', now)
List<String> moquiPermissions = userPermisionList*.userPermissionId.collect { permission -> 'permission:' + permission }
List<EntityValue> userGroupList = ec.entity.find('UserGroupMemberUser')
EntityValue userAccount = ec.entity.find('UserAccount').condition('userId', userId).useCache(false).disableAuthz().one()
String partyId = userAccount.partyId
EntityValue person = ec.entity.find('Person').condition('partyId', partyId).useCache(false).disableAuthz().one()
Set<RoleRepresentation> wantedClientRoles = new HashSet<RoleRepresentation>()
List<EntityValue> userGroupMemberList = ec.entity.find('UserGroupMember')
.condition('userId', userId)
.useCache(false)
.disableAuthz()
.list()
.filterByDate('fromDate', 'thruDate', now)
Set<String> moquiGroups = userGroupList*.userGroupId.collect { group -> 'group:' + group }
moquiGroups.add('group:ALL_USERS')
Map<String, RoleRepresentation> wantedClientRoles = getClientRoles(realm, clientResource, moquiPermissions + moquiGroups)
Set<String> userGroupIds = new HashSet<String>(userGroupMemberList*.userGroupId)
logger.info("userGroupIds: ${userGroupIds}")
userGroupIds.add('ALL_USERS')
wantedClientRoles.addAll(userGroupIds.collect { userGroupId -> syncUserGroup(userGroupId).toRepresentation() })
UserRepresentation userRep = userResource.toRepresentation()
Map<String, List<String>> attributes = new HashMap(userRep.getAttributes() ?: [:])
......@@ -224,6 +436,10 @@ void updateUser(RealmResource realm, String keycloakClientId, String keycloakUse
logger.info("user[$userId]} attributes: " + attributes)
userRep.setAttributes(attributes)
if (person) {
userRep.setFirstName(person.firstName)
userRep.setLastName(person.lastName)
}
List<RoleRepresentation> toRemove = []
RoleMappingResource roleMappingResource = userResource.roles()
......@@ -233,64 +449,26 @@ void updateUser(RealmResource realm, String keycloakClientId, String keycloakUse
toRemove.add(existingRoleRep)
}
}
List<RoleRepresentation> toAdd = wantedClientRoles.values() as List
List<RoleRepresentation> toAdd = wantedClientRoles as List
logger.info("roles to remove: ${toRemove}")
logger.info("roles to add: ${toAdd}")
RoleScopeResource clientRoleScopeResource = roleMappingResource.clientLevel(clientId)
RoleScopeResource clientRoleScopeResource = roleMappingResource.clientLevel(keycloakClientId)
clientRoleScopeResource.remove(toRemove)
clientRoleScopeResource.add(toAdd)
if (enabled) {
userRep.setEnabled(true)
}
userResource.update(userRep)
Map<String, Object> foo = userResource.impersonate()
logger.info("impersonate: foo=${foo}")
}
Map<String, Object> pushKeycloakUser() {
String userId = context.userId
EntityValue userLogin = ec.entity.find('UserLogin').condition('userId', userId).one()
List<EntityValue> logins
return [:]
/*
<view-entity entity-name="UserPermissionCheck" package="moqui.security">
<member-entity entity-alias="UGM" entity-name="moqui.security.UserGroupMember"/>
<member-relationship entity-alias="UGP" join-from-alias="UGM" relationship="permissions"/>
<alias name="userGroupId" entity-alias="UGM"/>
<alias name="userId" entity-alias="UGM"/>
<alias name="userPermissionId" entity-alias="UGP"/>
<alias name="groupFromDate" entity-alias="UGM" field="fromDate"/>
<alias name="groupThruDate" entity-alias="UGM" field="thruDate"/>
<alias name="permissionFromDate" entity-alias="UGP" field="fromDate"/>
<alias name="permissionThruDate" entity-alias="UGP" field="thruDate"/>
</view-entity>
*/
}
Map<String, Object> getKeycloakUsers() {
String keycloakClientId = 'moqui'
KeycloakToolFactory keycloakToolFactory = ec.getFactory().getToolFactory('Keycloak')
Keycloak keycloak = keycloakToolFactory.getInstance()
try {
RealmResource realm = keycloak.realm(keycloakToolFactory.getRealmName())
/*
ServerInfoResource serverInfo = keycloak.serverInfo()
logger.info('keycloak serverInfo: ' + keycloakToJson(serverInfo.getInfo()))
logger.info('keycloak keys: ' + keycloakToJson(realm.keys().getKeyMetadata()))
UsersResource users = realm.users()
for (UserRepresentation user: users.list()) {
logger.info('keycloak user: ' + keycloakToJson(user))
}
*/
updateUser(realm, keycloakClientId, 'c6a4cb53-4533-4236-89e5-058967b9b90a', '100003')
logger.info("access token=${keycloakToJson(keycloak.tokenManager().getAccessToken())}")
updateUser('c6f99571-a79d-4267-b76e-a02a6847c8c9', '100000')
} finally {
keycloak.close()
cleanupKeycloak()
}
}
......
......@@ -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>
......