/** * * Copyright * 2009-2015 Jayway Products AB * 2016-2017 Föreningen Sambruk * * Licensed under AGPL, Version 3.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.gnu.org/licenses/agpl.txt * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package se.streamsource.streamflow.web.infrastructure.plugin.ldap; import java.io.IOException; import java.util.List; import org.apache.commons.collections.IteratorUtils; import org.qi4j.api.common.Optional; import org.qi4j.api.composite.TransientComposite; import org.qi4j.api.configuration.Configuration; import org.qi4j.api.entity.EntityReference; import org.qi4j.api.injection.scope.Structure; import org.qi4j.api.injection.scope.Uses; import org.qi4j.api.mixin.Mixins; import org.qi4j.api.specification.Specification; import org.qi4j.api.structure.Module; import org.qi4j.api.unitofwork.NoSuchEntityException; import org.qi4j.api.unitofwork.UnitOfWork; import org.qi4j.api.unitofwork.UnitOfWorkCompletionException; import org.qi4j.api.usecase.Usecase; import org.qi4j.api.usecase.UsecaseBuilder; import org.qi4j.api.util.Iterables; import org.qi4j.api.value.ValueBuilder; import org.quartz.Job; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.restlet.data.Status; import org.restlet.representation.Representation; import org.restlet.resource.ClientResource; import org.restlet.resource.ResourceException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import se.streamsource.streamflow.api.workspace.cases.contact.ContactDTO; import se.streamsource.streamflow.api.workspace.cases.contact.ContactEmailDTO; import se.streamsource.streamflow.api.workspace.cases.contact.ContactPhoneDTO; import se.streamsource.streamflow.server.plugin.authentication.UserDetailsValue; import se.streamsource.streamflow.server.plugin.ldapimport.GroupDetailsValue; import se.streamsource.streamflow.server.plugin.ldapimport.GroupListValue; import se.streamsource.streamflow.server.plugin.ldapimport.GroupMemberDetailValue; import se.streamsource.streamflow.server.plugin.ldapimport.UserListValue; import se.streamsource.streamflow.web.domain.entity.ExternalReference; import se.streamsource.streamflow.web.domain.entity.organization.OrganizationsEntity; import se.streamsource.streamflow.web.domain.entity.user.UserEntity; import se.streamsource.streamflow.web.domain.entity.user.UsersEntity; import se.streamsource.streamflow.web.domain.interaction.security.Authentication; import se.streamsource.streamflow.web.domain.structure.group.Group; import se.streamsource.streamflow.web.domain.structure.group.Groups; import se.streamsource.streamflow.web.domain.structure.group.Participant; import se.streamsource.streamflow.web.domain.structure.group.Participants; import se.streamsource.streamflow.web.domain.structure.organization.Organization; import se.streamsource.streamflow.web.domain.structure.organization.OrganizationParticipations; import se.streamsource.streamflow.web.domain.structure.organization.Organizations; import se.streamsource.streamflow.web.domain.structure.user.Contactable; import se.streamsource.streamflow.web.domain.structure.user.User; import se.streamsource.streamflow.web.infrastructure.plugin.LdapImporterServiceConfiguration; /** * A quartz job responsible for import of users an groups from ldap. */ @Mixins(LdapImportJob.Mixin.class) public interface LdapImportJob extends Job, TransientComposite { void importUsers() throws UnitOfWorkCompletionException; void importGroups(); abstract class Mixin implements LdapImportJob { Logger logger = LoggerFactory.getLogger( LdapImportJob.class ); @Structure Module module; @Optional @Uses Configuration<LdapImporterServiceConfiguration> config; private static Usecase addUserUsecase = UsecaseBuilder.newUsecase( "Import new user" ); private static Usecase updateUserUsecase = UsecaseBuilder.newUsecase("Update user from import"); private static Usecase addGroupUsecase = UsecaseBuilder.newUsecase( "Import new group" ); private static Usecase updateGroupUsecase = UsecaseBuilder.newUsecase("Update group from import"); /** * Import users from LDAP. Check local users against users fetched from LDAP. * If a local user is not present in LDAP disable him. If a user fetched from LDAP does not exist locally create it with a dummy password. * If a ldap user exists locally but has changed properties update the local user with changed properties. * @throws UnitOfWorkCompletionException */ public void importUsers() { String json = callPlugin( "users" ); final UserListValue externalUserListValue = module.valueBuilderFactory().newValueFromJSON(UserListValue.class, json); handleUsersToLeaveOrganization( externalUserListValue.users().get() ); handleCreateAndUpdateOfLocalUsers( externalUserListValue.users().get() ); } private void handleCreateAndUpdateOfLocalUsers( final List<UserDetailsValue> externalUsers ) { UnitOfWork uow = module.unitOfWorkFactory().newUnitOfWork( UsecaseBuilder.newUsecase("Handle create and update local users") ); // discover if we need to insert new user or have to do an update. for( UserDetailsValue userDetail : externalUsers ) { Authentication localUser = null; try { localUser = uow.get( Authentication.class, userDetail.username().get() ); } catch (NoSuchEntityException ne ) { // ok - do nothing } if (localUser == null) { createNewUser(userDetail, userDetail.username().get() ); } else { updateUser(userDetail, (Contactable.Data) localUser ); } } // changes where handled downstream so we can discard this uow to avoid // ConcurrentEntityModificationException uow.discard(); } private void handleUsersToLeaveOrganization( final List<UserDetailsValue> externalUsers ) { UnitOfWork uow = module.unitOfWorkFactory().newUnitOfWork( UsecaseBuilder.newUsecase("Handle users to leave organization") ); UsersEntity usersEntity = uow.get( UsersEntity.class, UsersEntity.USERS_ID ); Organization org = ((Organizations.Data)uow.get( Organizations.class, OrganizationsEntity.ORGANIZATIONS_ID )).organization().get(); // compare local users with ldap users and deactivate if they are not present in ldap. Iterable<UserEntity> usersNotInLdap = Iterables.filter( new Specification<UserEntity>() { public boolean satisfiedBy( final UserEntity user ) { return !Iterables.matchesAny( new Specification<UserDetailsValue>() { public boolean satisfiedBy( UserDetailsValue userDetail ) { // not interested in administrator or already unjoined or users existing in ldap return user.isAdministrator() || user.organizations().count() == 0 || user.userName().get().equals( userDetail.username().get() ); } }, externalUsers ); } }, usersEntity.users() ); for( UserEntity user : usersNotInLdap ) { user.leave( org ); } try { uow.complete(); } catch (UnitOfWorkCompletionException e) { logger.error( "Could not commit handle users to leave organization.", e ); uow.discard(); } } public void importGroups() { String json = callPlugin( "groups" ); final GroupListValue externalGroupListValue = module.valueBuilderFactory().newValueFromJSON( GroupListValue.class, json ); handleRemoveGroups( externalGroupListValue.groups().get() ); handleCreateOrUpdateLocalGroups( externalGroupListValue.groups().get() ); // cycle once more for update of participants // a new uow will be created downstream for these changes for( GroupDetailsValue externalGroup : externalGroupListValue.groups().get() ) { synchronizeParticipants( externalGroup ); } } private void handleCreateOrUpdateLocalGroups( final List<GroupDetailsValue> externalGroups ) { UnitOfWork uow = module.unitOfWorkFactory().newUnitOfWork( UsecaseBuilder.newUsecase("Handle create and update local groups") ); Groups localGroups = (Groups)uow.get( Organizations.Data.class, OrganizationsEntity.ORGANIZATIONS_ID ).organization().get(); // create or update groups without touching participants just yet for( GroupDetailsValue externalGroup : externalGroups ) { Group localGroup = null; localGroup = ((Groups)localGroups).findByExternalReference( externalGroup.id().get() ); if( localGroup == null) { createNewGroupOnOrganization( externalGroup ); } else { updateGroupOnOrganization( localGroup, externalGroup.name().get() ); } } uow.discard(); } private void updateGroupOnOrganization( Group localGroup, String name ) { UnitOfWork uow = module.unitOfWorkFactory().newUnitOfWork( UsecaseBuilder.newUsecase("Handle update local group") ); Group group = uow.get( localGroup ); group.changeDescription( name ); try { uow.complete(); } catch (UnitOfWorkCompletionException e) { logger.error( "Could not update local group", e ); uow.discard(); } } private void handleRemoveGroups( final List<GroupDetailsValue> externalGroups ) { UnitOfWork uow = module.unitOfWorkFactory().newUnitOfWork( UsecaseBuilder.newUsecase("Handle remove groups") ); Groups localGroups = (Groups)uow.get( Organizations.Data.class, OrganizationsEntity.ORGANIZATIONS_ID ).organization().get(); // first compare local groups and ldap groups and delete local groups that are no longer available in LDAP. Iterable<Group> groupsNotInLdap = Iterables.filter( new Specification<Group>() { public boolean satisfiedBy( final Group group ) { return !Iterables.matchesAny( new Specification<GroupDetailsValue>() { public boolean satisfiedBy( GroupDetailsValue groupDetail ) { return groupDetail.id().get().equals( ((ExternalReference.Data) group).reference().get() ); } }, externalGroups ); } }, ((Groups.Data) localGroups).groups() ); // fetch list from iterables iterator to avoid ConcurrentModificationException during remove operation. List<Group> toRemove = (List<Group>)IteratorUtils.toList( groupsNotInLdap.iterator() ); for( Group group : toRemove ) { localGroups.removeGroup( group ); } try { uow.complete(); } catch (UnitOfWorkCompletionException e) { logger.error( "Could not remove groups.", e ); uow.discard(); } } private void synchronizeParticipants(final GroupDetailsValue externalGroup ) { UnitOfWork uow = module.unitOfWorkFactory().newUnitOfWork( updateGroupUsecase ); final Groups groups = (Groups)uow.get( Organizations.Data.class, OrganizationsEntity.ORGANIZATIONS_ID ).organization().get(); final Group group = groups.findByExternalReference( externalGroup.id().get() ); if( group != null ) { // check first if there are any participants in local group that are not present in LDAP and let them leave the group Iterable<Participant> participantsNotPresentInLdap = Iterables.filter( new Specification<Participant>() { public boolean satisfiedBy( final Participant participant ) { return !Iterables.matchesAny( new Specification<GroupMemberDetailValue>() { public boolean satisfiedBy( GroupMemberDetailValue externalParticipant ) { return EntityReference.getEntityReference( participant ).identity().equals( extractInternalIdForParticipant( externalParticipant, groups ) ); } }, externalGroup.members().get() ); } }, ((Participants.Data)group).participants() ); for ( Participant participant : participantsNotPresentInLdap ) { group.removeParticipant( participant ); } // check the other way round and join group Iterable<GroupMemberDetailValue> ldapParticipantNotPresentLocally = Iterables.filter( new Specification<GroupMemberDetailValue>() { public boolean satisfiedBy( final GroupMemberDetailValue externalParticipant ) { return !Iterables.matchesAny( new Specification<Participant>() { public boolean satisfiedBy( Participant localParticipant ) { return EntityReference.getEntityReference( localParticipant ).identity().equals( extractInternalIdForParticipant( externalParticipant, groups ) ); } }, ((Participants.Data) group).participants() ); } }, externalGroup.members().get() ); for( GroupMemberDetailValue externalMember : ldapParticipantNotPresentLocally ) { Participant participant = uow.get( Participant.class, extractInternalIdForParticipant( externalMember, groups ) ); group.addParticipant( participant ); } try { uow.complete(); } catch (UnitOfWorkCompletionException uowe ) { logger.error( "Could not commit member synchronization.", uowe ); uow.discard(); } } else throw new IllegalArgumentException( "The group we try to synchronize participants for does not exist!" ); } private String extractInternalIdForParticipant( GroupMemberDetailValue externalParticipant, Groups orgGroups ) { String externalId = ""; switch ( externalParticipant.memberType().get() ) { case user: externalId = externalParticipant.id().get(); break; case group: try { externalId = EntityReference.getEntityReference( orgGroups.findByExternalReference( externalParticipant.id().get() ) ).identity(); } catch (NullPointerException npe ) { logger.error( "Not able to find any imported group with DN: " + externalParticipant.id().get() ); throw new IllegalArgumentException( "Not able to find any imported group with DN: " + externalParticipant.id().get(), npe ); } break; } return externalId; } private void createNewGroupOnOrganization( GroupDetailsValue externalGroup ) { UnitOfWork uow = module.unitOfWorkFactory().newUnitOfWork( addGroupUsecase ); Organization org = uow.get( Organizations.Data.class, OrganizationsEntity.ORGANIZATIONS_ID ).organization().get(); Group group = org.createGroup( externalGroup.name().get() ); group.changeReference( externalGroup.id().get() ); try { uow.complete(); } catch (UnitOfWorkCompletionException e) { logger.error( "Import of new group failed.", e ); uow.discard(); } } private void updateUser(UserDetailsValue externalUser, Contactable.Data user ) { boolean modified = false; if( ((OrganizationParticipations.Data)user).organizations().count() == 0 ) { modified = true; } if (!externalUser.name().get().equals(user.contact().get().name().get())) { modified = true; } List contactEmailList = user.contact().get().emailAddresses().get(); if (!externalUser.emailAddress().get() .equals( contactEmailList.size() == 0 ? "" : ((ContactEmailDTO) contactEmailList.get( 0 )).emailAddress().get() )) { modified = true; } List contactPhoneList = user.contact().get().phoneNumbers().get(); if (!externalUser.phoneNumber().get() .equals( contactPhoneList.size() == 0 ? "" : ((ContactPhoneDTO)contactPhoneList.get( 0 )).phoneNumber().get() )) { modified = true; } if (modified) { UnitOfWork unitOfWork = module.unitOfWorkFactory().newUnitOfWork( updateUserUsecase ); UserEntity userEntity = unitOfWork.get( (UserEntity) user ); Organization org = unitOfWork.get( Organizations.Data.class, OrganizationsEntity.ORGANIZATIONS_ID ).organization().get(); userEntity.changeDescription( externalUser.name().get() ); userEntity.join( org ); ValueBuilder<ContactDTO> contactBuilder = module.valueBuilderFactory().newValueBuilder(ContactDTO.class); contactBuilder.prototype().name().set(externalUser.name().get()); ValueBuilder<ContactEmailDTO> emailBuilder = module.valueBuilderFactory().newValueBuilder(ContactEmailDTO.class); emailBuilder.prototype().emailAddress().set(externalUser.emailAddress().get()); contactBuilder.prototype().emailAddresses().get().add(emailBuilder.newInstance()); ValueBuilder<ContactPhoneDTO> phoneBuilder = module.valueBuilderFactory().newValueBuilder(ContactPhoneDTO.class); phoneBuilder.prototype().phoneNumber().set(externalUser.phoneNumber().get()); contactBuilder.prototype().phoneNumbers().get().add(phoneBuilder.newInstance()); ((Contactable) userEntity).updateContact(contactBuilder.newInstance()); try { unitOfWork.complete(); } catch (UnitOfWorkCompletionException e) { logger.error( "Update of imported user failed.", e ); unitOfWork.discard(); } } } private void createNewUser(UserDetailsValue externalUser, String username ) { UnitOfWork unitOfWork = module.unitOfWorkFactory().newUnitOfWork( addUserUsecase ); UsersEntity usersEntity = unitOfWork.get( UsersEntity.class, UsersEntity.USERS_ID ); User user = usersEntity.createUser( username, "ett2tre!magicString" ); ValueBuilder<ContactDTO> contactBuilder = module.valueBuilderFactory().newValueBuilder(ContactDTO.class); contactBuilder.prototype().name().set(externalUser.name().get()); ValueBuilder<ContactEmailDTO> emailBuilder = module.valueBuilderFactory().newValueBuilder(ContactEmailDTO.class); emailBuilder.prototype().emailAddress().set(externalUser.emailAddress().get()); contactBuilder.prototype().emailAddresses().get().add(emailBuilder.newInstance()); ValueBuilder<ContactPhoneDTO> phoneBuilder = module.valueBuilderFactory().newValueBuilder(ContactPhoneDTO.class); phoneBuilder.prototype().phoneNumber().set(externalUser.phoneNumber().get()); contactBuilder.prototype().phoneNumbers().get().add(phoneBuilder.newInstance()); ((Contactable) user).updateContact(contactBuilder.newInstance()); Organization org = unitOfWork.get( Organizations.Data.class, OrganizationsEntity.ORGANIZATIONS_ID ).organization().get(); user.join( org ); try { unitOfWork.complete(); } catch (UnitOfWorkCompletionException e) { logger.error( "Create of new imported user failed.", e ); unitOfWork.discard(); } } private String callPlugin( String command ) { String json = ""; ClientResource clientResource = new ClientResource(config.configuration().url().get() + "/import/" + command ); try { // Call plugin Representation result = clientResource.get(); try { json = result.getText(); } catch (IOException e) { throw new ResourceException(Status.CLIENT_ERROR_UNAUTHORIZED, "Could not get userdetails for externally validated user"); } } catch (ResourceException e) { //TODO do what? } return json; } public void execute( JobExecutionContext context ) throws JobExecutionException { try { config = (Configuration<LdapImporterServiceConfiguration>)context.getJobDetail().getJobDataMap().get( "config" ); logger.info( "Start LDAP import" ); importUsers(); importGroups(); logger.info("Finished LDAP import"); } catch (Throwable e) { logger.error("Could not complete import from LDAP", e); } } } }