/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
*
* 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 org.apache.ambari.server.controller.internal;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import javax.naming.OperationNotSupportedException;
import org.apache.ambari.server.AmbariException;
import org.apache.ambari.server.controller.AmbariManagementController;
import org.apache.ambari.server.controller.LdapSyncRequest;
import org.apache.ambari.server.controller.spi.NoSuchParentResourceException;
import org.apache.ambari.server.controller.spi.NoSuchResourceException;
import org.apache.ambari.server.controller.spi.Predicate;
import org.apache.ambari.server.controller.spi.Request;
import org.apache.ambari.server.controller.spi.RequestStatus;
import org.apache.ambari.server.controller.spi.Resource;
import org.apache.ambari.server.controller.spi.ResourceAlreadyExistsException;
import org.apache.ambari.server.controller.spi.SystemException;
import org.apache.ambari.server.controller.spi.UnsupportedPropertyException;
import org.apache.ambari.server.controller.utilities.PropertyHelper;
import org.apache.ambari.server.orm.entities.LdapSyncEventEntity;
import org.apache.ambari.server.orm.entities.LdapSyncSpecEntity;
import org.apache.ambari.server.security.authorization.AuthorizationException;
import org.apache.ambari.server.security.authorization.AuthorizationHelper;
import org.apache.ambari.server.security.authorization.ResourceType;
import org.apache.ambari.server.security.authorization.RoleAuthorization;
import org.apache.ambari.server.security.ldap.LdapBatchDto;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Resource provider for ldap sync events.
*/
public class LdapSyncEventResourceProvider extends AbstractControllerResourceProvider {
/**
* Thread pool
*/
private static ExecutorService executorService;
// TODO : Do we need to expose the thread pool params in configuration?
private final static int THREAD_POOL_CORE_SIZE = 2;
private final static int THREAD_POOL_MAX_SIZE = 5;
private final static long THREAD_POOL_TIMEOUT = 1000L;
/**
* Event property id constants.
*/
public static final String EVENT_ID_PROPERTY_ID = "Event/id";
public static final String EVENT_STATUS_PROPERTY_ID = "Event/status";
public static final String EVENT_STATUS_DETAIL_PROPERTY_ID = "Event/status_detail";
public static final String EVENT_START_TIME_PROPERTY_ID = "Event/sync_time/start";
public static final String EVENT_END_TIME_PROPERTY_ID = "Event/sync_time/end";
public static final String USERS_CREATED_PROPERTY_ID = "Event/summary/users/created";
public static final String USERS_UPDATED_PROPERTY_ID = "Event/summary/users/updated";
public static final String USERS_REMOVED_PROPERTY_ID = "Event/summary/users/removed";
public static final String USERS_SKIPPED_PROPERTY_ID = "Event/summary/users/skipped";
public static final String GROUPS_CREATED_PROPERTY_ID = "Event/summary/groups/created";
public static final String GROUPS_UPDATED_PROPERTY_ID = "Event/summary/groups/updated";
public static final String GROUPS_REMOVED_PROPERTY_ID = "Event/summary/groups/removed";
public static final String MEMBERSHIPS_CREATED_PROPERTY_ID = "Event/summary/memberships/created";
public static final String MEMBERSHIPS_REMOVED_PROPERTY_ID = "Event/summary/memberships/removed";
public static final String EVENT_SPECS_PROPERTY_ID = "Event/specs";
/**
* The key property ids for a event resource.
*/
private static Map<Resource.Type, String> keyPropertyIds = new HashMap<>();
static {
keyPropertyIds.put(Resource.Type.LdapSyncEvent, EVENT_ID_PROPERTY_ID);
}
/**
* The property ids for a event resource.
*/
private static Set<String> propertyIds = new HashSet<>();
static {
propertyIds.add(EVENT_ID_PROPERTY_ID);
propertyIds.add(EVENT_STATUS_PROPERTY_ID);
propertyIds.add(EVENT_STATUS_DETAIL_PROPERTY_ID);
propertyIds.add(EVENT_START_TIME_PROPERTY_ID);
propertyIds.add(EVENT_END_TIME_PROPERTY_ID);
propertyIds.add(USERS_CREATED_PROPERTY_ID);
propertyIds.add(USERS_UPDATED_PROPERTY_ID);
propertyIds.add(USERS_REMOVED_PROPERTY_ID);
propertyIds.add(USERS_SKIPPED_PROPERTY_ID);
propertyIds.add(GROUPS_CREATED_PROPERTY_ID);
propertyIds.add(GROUPS_UPDATED_PROPERTY_ID);
propertyIds.add(GROUPS_REMOVED_PROPERTY_ID);
propertyIds.add(MEMBERSHIPS_CREATED_PROPERTY_ID);
propertyIds.add(MEMBERSHIPS_REMOVED_PROPERTY_ID);
propertyIds.add(EVENT_SPECS_PROPERTY_ID);
}
/**
* Spec property keys.
*/
private static final String PRINCIPAL_TYPE_SPEC_KEY = "principal_type";
private static final String SYNC_TYPE_SPEC_KEY = "sync_type";
private static final String NAMES_SPEC_KEY = "names";
/**
* Map of all sync events.
*/
private final Map<Long, LdapSyncEventEntity> events = new ConcurrentSkipListMap<>();
/**
* The queue of events to be processed.
*/
private final Queue<LdapSyncEventEntity> eventQueue = new LinkedList<>();
/**
* Indicates whether or not the events are currently being processed.
*/
private volatile boolean processingEvents = false;
/**
* The next event id.
*/
private AtomicLong nextEventId = new AtomicLong(1L);
/**
* The logger.
*/
protected final static Logger LOG = LoggerFactory.getLogger(LdapSyncEventResourceProvider.class);
// ----- Constructors ------------------------------------------------------
/**
* Construct a event resource provider.
*/
public LdapSyncEventResourceProvider(AmbariManagementController managementController) {
super(propertyIds, keyPropertyIds, managementController);
EnumSet<RoleAuthorization> roleAuthorizations =
EnumSet.of(RoleAuthorization.AMBARI_MANAGE_GROUPS, RoleAuthorization.AMBARI_MANAGE_USERS);
setRequiredCreateAuthorizations(roleAuthorizations);
setRequiredDeleteAuthorizations(roleAuthorizations);
}
// ----- ResourceProvider --------------------------------------------------
@Override
public RequestStatus createResourcesAuthorized(Request event)
throws SystemException, UnsupportedPropertyException,
ResourceAlreadyExistsException, NoSuchParentResourceException {
Set<LdapSyncEventEntity> newEvents = new HashSet<>();
for (Map<String, Object> properties : event.getProperties()) {
newEvents.add(createResources(getCreateCommand(properties)));
}
notifyCreate(Resource.Type.ViewInstance, event);
Set<Resource> associatedResources = new HashSet<>();
for (LdapSyncEventEntity eventEntity : newEvents) {
Resource resource = new ResourceImpl(Resource.Type.LdapSyncEvent);
resource.setProperty(EVENT_ID_PROPERTY_ID, eventEntity.getId());
associatedResources.add(resource);
synchronized (eventQueue) {
eventQueue.offer(eventEntity);
}
}
ensureEventProcessor();
return getRequestStatus(null, associatedResources);
}
@Override
public Set<Resource> getResources(Request event, Predicate predicate)
throws SystemException, UnsupportedPropertyException, NoSuchResourceException, NoSuchParentResourceException {
Set<Resource> resources = new HashSet<>();
Set<String> requestedIds = getRequestPropertyIds(event, predicate);
for (LdapSyncEventEntity eventEntity : events.values()) {
resources.add(toResource(eventEntity, requestedIds));
}
return resources;
}
@Override
public RequestStatus updateResources(Request event, Predicate predicate)
throws SystemException, UnsupportedPropertyException, NoSuchResourceException, NoSuchParentResourceException {
throw new UnsupportedOperationException("Not supported.");
}
@Override
public RequestStatus deleteResourcesAuthorized(Request request, Predicate predicate)
throws SystemException, UnsupportedPropertyException, NoSuchResourceException, NoSuchParentResourceException {
modifyResources(getDeleteCommand(predicate));
notifyDelete(Resource.Type.ViewInstance, predicate);
return getRequestStatus(null);
}
@Override
public Map<Resource.Type, String> getKeyPropertyIds() {
return keyPropertyIds;
}
// ----- AbstractResourceProvider ------------------------------------------
@Override
protected Set<String> getPKPropertyIds() {
return new HashSet<>(keyPropertyIds.values());
}
// ----- helper methods ----------------------------------------------------
/**
* Ensure that sync events are being processed.
*/
protected void ensureEventProcessor() {
if (!processingEvents) {
synchronized (eventQueue) {
if (!processingEvents) {
processingEvents = true;
getExecutorService().submit(new Runnable() {
@Override
public void run() {
processSyncEvents();
}
});
}
}
}
}
// create a resource from the given event entity
private Resource toResource(LdapSyncEventEntity eventEntity, Set<String> requestedIds) {
Resource resource = new ResourceImpl(Resource.Type.LdapSyncEvent);
setResourceProperty(resource, EVENT_ID_PROPERTY_ID, eventEntity.getId(), requestedIds);
setResourceProperty(resource, EVENT_STATUS_PROPERTY_ID, eventEntity.getStatus().toString().toUpperCase(), requestedIds);
setResourceProperty(resource, EVENT_STATUS_DETAIL_PROPERTY_ID, eventEntity.getStatusDetail(), requestedIds);
setResourceProperty(resource, USERS_CREATED_PROPERTY_ID, eventEntity.getUsersCreated(), requestedIds);
setResourceProperty(resource, USERS_UPDATED_PROPERTY_ID, eventEntity.getUsersUpdated(), requestedIds);
setResourceProperty(resource, USERS_REMOVED_PROPERTY_ID, eventEntity.getUsersRemoved(), requestedIds);
setResourceProperty(resource, USERS_SKIPPED_PROPERTY_ID, eventEntity.getUsersSkipped(), requestedIds);
setResourceProperty(resource, GROUPS_CREATED_PROPERTY_ID, eventEntity.getGroupsCreated(), requestedIds);
setResourceProperty(resource, GROUPS_UPDATED_PROPERTY_ID, eventEntity.getGroupsUpdated(), requestedIds);
setResourceProperty(resource, GROUPS_REMOVED_PROPERTY_ID, eventEntity.getGroupsRemoved(), requestedIds);
setResourceProperty(resource, MEMBERSHIPS_CREATED_PROPERTY_ID, eventEntity.getMembershipsCreated(), requestedIds);
setResourceProperty(resource, MEMBERSHIPS_REMOVED_PROPERTY_ID, eventEntity.getMembershipsRemoved(), requestedIds);
Set<Map<String, String>> specs = new HashSet<>();
List<LdapSyncSpecEntity> specList = eventEntity.getSpecs();
for (LdapSyncSpecEntity spec : specList) {
Map<String, String> specMap = new HashMap<>();
specMap.put(PRINCIPAL_TYPE_SPEC_KEY, spec.getPrincipalType().toString().toLowerCase());
specMap.put(SYNC_TYPE_SPEC_KEY, spec.getSyncType().toString().toLowerCase());
List<String> names = spec.getPrincipalNames();
if (!names.isEmpty()) {
specMap.put(NAMES_SPEC_KEY, names.toString().replace("[", "").replace("]", "").replace(", ", ","));
}
specs.add(specMap);
}
setResourceProperty(resource, EVENT_SPECS_PROPERTY_ID, specs, requestedIds);
setResourceProperty(resource, EVENT_START_TIME_PROPERTY_ID, eventEntity.getStartTime(), requestedIds);
setResourceProperty(resource, EVENT_END_TIME_PROPERTY_ID, eventEntity.getEndTime(), requestedIds);
return resource;
}
// create a event entity from the given set of properties
private LdapSyncEventEntity toEntity(Map<String, Object> properties) {
LdapSyncEventEntity entity = new LdapSyncEventEntity(getNextEventId());
List<LdapSyncSpecEntity> specList = new LinkedList<>();
Set<Map<String, String>> specs = (Set<Map<String, String>>) properties.get(EVENT_SPECS_PROPERTY_ID);
for (Map<String, String> specMap : specs) {
LdapSyncSpecEntity.SyncType syncType = null;
LdapSyncSpecEntity.PrincipalType principalType = null;
List<String> principalNames = Collections.emptyList();
for (Map.Entry<String, String> entry : specMap.entrySet()) {
String key = entry.getKey();
if (key.equalsIgnoreCase(PRINCIPAL_TYPE_SPEC_KEY)) {
principalType = LdapSyncSpecEntity.PrincipalType.valueOfIgnoreCase(entry.getValue());
} else if (key.equalsIgnoreCase(SYNC_TYPE_SPEC_KEY)) {
syncType = LdapSyncSpecEntity.SyncType.valueOfIgnoreCase(entry.getValue());
} else if (key.equalsIgnoreCase(NAMES_SPEC_KEY)) {
String names = entry.getValue();
principalNames = Arrays.asList(names.split("\\s*,\\s*"));
} else {
throw new IllegalArgumentException("Unknown spec key " + key + ".");
}
}
if (syncType == null || principalType == null) {
throw new IllegalArgumentException("LDAP event spec must include both sync-type and principal-type.");
}
LdapSyncSpecEntity spec = new LdapSyncSpecEntity(principalType, syncType, principalNames);
specList.add(spec);
}
entity.setSpecs(specList);
return entity;
}
// get the next event id
private long getNextEventId() {
return nextEventId.getAndIncrement();
}
// Create a create command with all properties set
private Command<LdapSyncEventEntity> getCreateCommand(final Map<String, Object> properties) {
return new Command<LdapSyncEventEntity>() {
@Override
public LdapSyncEventEntity invoke() throws AmbariException, AuthorizationException {
LdapSyncEventEntity eventEntity = toEntity(properties);
for (LdapSyncSpecEntity ldapSyncSpecEntity : eventEntity.getSpecs()) {
if (ldapSyncSpecEntity.getPrincipalType() == LdapSyncSpecEntity.PrincipalType.USERS) {
if (!AuthorizationHelper.isAuthorized(ResourceType.AMBARI, null, RoleAuthorization.AMBARI_MANAGE_USERS)) {
throw new AuthorizationException("The uthenticated user is not authorized to syng LDAP users");
}
} else {
if (!AuthorizationHelper.isAuthorized(ResourceType.AMBARI, null, RoleAuthorization.AMBARI_MANAGE_GROUPS)) {
throw new AuthorizationException("The uthenticated user is not authorized to syng LDAP groups");
}
}
}
events.put(eventEntity.getId(), eventEntity);
return eventEntity;
}
};
}
// Create a delete command with the given predicate
private Command<Void> getDeleteCommand(final Predicate predicate) {
return new Command<Void>() {
@Override
public Void invoke() throws AmbariException {
Set<String> requestedIds = getRequestPropertyIds(PropertyHelper.getReadRequest(), predicate);
Set<LdapSyncEventEntity> entities = new HashSet<>();
for (LdapSyncEventEntity entity : events.values()){
Resource resource = toResource(entity, requestedIds);
if (predicate == null || predicate.evaluate(resource)) {
entities.add(entity);
}
}
for (LdapSyncEventEntity entity : entities) {
events.remove(entity.getId());
}
return null;
}
};
}
// Get the ldap sync thread pool
private static synchronized ExecutorService getExecutorService() {
if (executorService == null) {
LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
THREAD_POOL_CORE_SIZE,
THREAD_POOL_MAX_SIZE,
THREAD_POOL_TIMEOUT,
TimeUnit.MILLISECONDS,
queue);
threadPoolExecutor.allowCoreThreadTimeOut(true);
executorService = threadPoolExecutor;
}
return executorService;
}
// Process any queued up sync events
private void processSyncEvents() {
while (processingEvents) {
LdapSyncEventEntity event;
synchronized (eventQueue) {
if (processingEvents) {
event = eventQueue.poll();
if (event == null) {
processingEvents = false;
return;
}
} else {
break;
}
}
event.setStatus(LdapSyncEventEntity.Status.RUNNING);
event.setStatusDetail("Running LDAP sync.");
event.setStartTime(System.currentTimeMillis());
try {
populateLdapSyncEvent(event, syncLdap(event));
event.setStatus(LdapSyncEventEntity.Status.COMPLETE);
event.setStatusDetail("Completed LDAP sync.");
} catch (Exception e) {
event.setStatus(LdapSyncEventEntity.Status.ERROR);
String msg = "Caught exception running LDAP sync. ";
if (e.getCause() instanceof OperationNotSupportedException) {
msg += "LDAP server may not support search results pagination. " +
"Try to turn the pagination off.";
}
event.setStatusDetail(msg + e.getMessage());
LOG.error(msg, e);
} finally {
event.setEndTime(System.currentTimeMillis());
}
}
}
/**
* Sync the users and groups specified in the given sync event with ldap.
*
* @param event the sync event
*
* @return the results of the sync
*
* @throws AmbariException if the sync could not be completed
*/
private LdapBatchDto syncLdap(LdapSyncEventEntity event) throws AmbariException {
LdapSyncRequest userRequest = null;
LdapSyncRequest groupRequest = null;
for (LdapSyncSpecEntity spec : event.getSpecs()) {
switch (spec.getPrincipalType()) {
case USERS:
userRequest = getLdapRequest(userRequest, spec);
break;
case GROUPS:
groupRequest = getLdapRequest(groupRequest, spec);
break;
}
}
return getManagementController().synchronizeLdapUsersAndGroups(userRequest, groupRequest);
}
/**
* Update the given request with the given ldap event spec.
*
* @param request the sync request; may be null
* @param spec the specification of what to sync
*
* @return the updated sync request or a new sync request if the given request is null
*/
private LdapSyncRequest getLdapRequest(LdapSyncRequest request, LdapSyncSpecEntity spec) {
switch (spec.getSyncType()) {
case ALL:
return new LdapSyncRequest(LdapSyncSpecEntity.SyncType.ALL);
case EXISTING:
return new LdapSyncRequest(LdapSyncSpecEntity.SyncType.EXISTING);
case SPECIFIC:
Set<String> principalNames = new HashSet<>(spec.getPrincipalNames());
if (request == null ) {
request = new LdapSyncRequest(LdapSyncSpecEntity.SyncType.SPECIFIC, principalNames);
} else {
request.addPrincipalNames(principalNames);
}
}
return request;
}
/**
* Populate the given ldap sync event with the results of an ldap sync.
*
* @param event the sync event
* @param syncInfo the sync results
*/
private void populateLdapSyncEvent(LdapSyncEventEntity event, LdapBatchDto syncInfo) {
event.setUsersCreated(syncInfo.getUsersToBeCreated().size());
event.setUsersUpdated(syncInfo.getUsersToBecomeLdap().size());
event.setUsersRemoved(syncInfo.getUsersToBeRemoved().size());
event.setUsersSkipped(syncInfo.getUsersSkipped().size());
event.setGroupsCreated(syncInfo.getGroupsToBeCreated().size());
event.setGroupsUpdated(syncInfo.getGroupsToBecomeLdap().size());
event.setGroupsRemoved(syncInfo.getGroupsToBeRemoved().size());
event.setMembershipsCreated(syncInfo.getMembershipToAdd().size());
event.setMembershipsRemoved(syncInfo.getMembershipToRemove().size());
}
}