/* * Copyright (c) 2015 EMC Corporation * All Rights Reserved */ package com.emc.storageos.api.service.impl.resource; import com.emc.storageos.api.mapper.DbObjectMapper; import com.emc.storageos.api.mapper.functions.MapUserGroup; import com.emc.storageos.api.service.impl.response.BulkList; import com.emc.storageos.db.client.URIUtil; import com.emc.storageos.db.client.constraint.AlternateIdConstraint; import com.emc.storageos.db.client.constraint.NamedElementQueryResultList; import com.emc.storageos.db.client.constraint.URIQueryResultList; import com.emc.storageos.db.client.model.AuthnProvider; import com.emc.storageos.db.client.model.DataObject; import com.emc.storageos.db.client.model.StringSet; import com.emc.storageos.db.client.model.UserGroup; import com.emc.storageos.db.exceptions.DatabaseException; import com.emc.storageos.model.BulkIdParam; import com.emc.storageos.model.ResourceTypeEnum; import com.emc.storageos.model.usergroup.*; import com.emc.storageos.security.authentication.StorageOSUser; import com.emc.storageos.security.authorization.ACL; import com.emc.storageos.security.authorization.CheckPermission; import com.emc.storageos.security.authorization.DefaultPermissions; import com.emc.storageos.security.authorization.Role; import com.emc.storageos.services.OperationTypeEnum; import com.emc.storageos.svcs.errorhandling.resources.APIException; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.CollectionUtils; import javax.ws.rs.*; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import java.net.URI; import java.util.*; import static com.emc.storageos.api.mapper.UserGroupMapper.map; /** * API for creating and manipulating user groups. * So, that these groups can be used as groups in role-assignments, * acl-assignments and also in tenant userMappings. */ @Path("/vdc/admin/user-groups") @DefaultPermissions(readRoles = { Role.SECURITY_ADMIN }, writeRoles = { Role.SECURITY_ADMIN }) public class UserGroupService extends TaggedResource { private static String EXPECTED_GEO_VERSION = "2.3"; private static String FEATURE_NAME = "Attributes Based Role and ACL Assignments"; private static final Logger _log = LoggerFactory.getLogger(UserGroupService.class); private static final String EVENT_SERVICE_TYPE = "usergroupconfig"; @Override public String getServiceType() { return EVENT_SERVICE_TYPE; } @Override protected DataObject queryResource(URI id) { return getUserGroupById(id, false); } @Override protected URI getTenantOwner(URI id) { return null; } @SuppressWarnings("unchecked") @Override public Class<UserGroup> getResourceClass() { return UserGroup.class; } @Override protected ResourceTypeEnum getResourceType() { return ResourceTypeEnum.USER_GROUP; } /** * @param id the URN of a ViPR UserGroup to get details from * @param checkInactive If true, make sure that provider is not inactive * @return UserGroup object for the given id */ private UserGroup getUserGroupById(URI id, boolean checkInactive) { if (id == null) { _log.debug("User Group ID is NULL"); return null; } _log.debug("User Group ID is {}", id.toString()); UserGroup userGroup = _permissionsHelper.getObjectById(id, UserGroup.class); ArgValidator.checkEntity(userGroup, id, isIdEmbeddedInURL(id), checkInactive); return userGroup; } /** * Creates user group. * The submitted user group element values will be validated. * <p> * The minimal set of parameters include: name, domain, attributes (key and values pair). * <p> * * @param param Representation of UserGroup with all necessary elements * * @brief Creates an User Group * @return Newly created User Group details as UserGroupRestRep * @see UserGroupCreateParam * @see UserGroupRestRep */ @POST @Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON }) @CheckPermission(roles = { Role.SECURITY_ADMIN }) public UserGroupRestRep createUserGroup(UserGroupCreateParam param) { checkCompatibleVersion(); validateUserGroupCreateParam(param); // Check for active UserGroup with same name checkDuplicateLabel(UserGroup.class, param.getLabel()); UserGroup userGroup = map(param); URI id = URIUtil.createId(UserGroup.class); userGroup.setId(id); _log.debug("Saving the UserGroup: {}: {}", userGroup.getId(), userGroup.toString()); // Check if there is any existing user group with same set of properties. checkForOverlappingUserGroup(userGroup); _dbClient.createObject(userGroup); auditOp(OperationTypeEnum.CREATE_USERGROUP, true, null, userGroup.toString(), userGroup.getId().toString()); return map(getUserGroupById(id, false)); } /** * Updates user group. * The submitted user group element values will be validated. * <p> * The minimal set of parameters include: name, domain, attributes (key and values pair). * <p> * * @param param Representation of UserGroup with all necessary elements * * @brief Updates an User Group * @return The updated User Group details as UserGroupRestRep * @see UserGroupUpdateParam * @see UserGroupRestRep */ @PUT @Path("/{id}") @Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON }) @CheckPermission(roles = { Role.SECURITY_ADMIN }) public UserGroupRestRep updateUserGroup(@PathParam("id") URI id, UserGroupUpdateParam param) { checkCompatibleVersion(); UserGroup userGroup = getUserGroupById(id, false); ArgValidator.checkEntityNotNull(userGroup, id, isIdEmbeddedInURL(id)); validateUserGroupUpdateParam(param); // Update the db object with new information. overlayUserGroup(userGroup, param); // Check if there is any existing user group with same set of properties. checkForOverlappingUserGroup(userGroup); _dbClient.persistObject(userGroup); auditOp(OperationTypeEnum.UPDATE_USERGROUP, true, null, userGroup.toString(), userGroup.getId().toString()); return map(getUserGroupById(id, false)); } /** * Gets the user group list (of URNs). * * @brief All the active user group's URN will be returned. * @return All the user groups details as UserGroupList * @see UserGroupList */ @GET @Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON }) public UserGroupList getUserGroupIds() { checkCompatibleVersion(); checkIfUserHasPermissions(); NamedElementQueryResultList userGroups = new NamedElementQueryResultList(); List<URI> uris = _dbClient.queryByType(UserGroup.class, true); List<UserGroup> configs = _dbClient.queryObject(UserGroup.class, uris); List<NamedElementQueryResultList.NamedElement> elements = new ArrayList<NamedElementQueryResultList.NamedElement>(configs.size()); for (UserGroup p : configs) { elements.add(NamedElementQueryResultList.NamedElement.createElement(p.getId(), p.getLabel())); } userGroups.setResult(elements.iterator()); UserGroupList list = new UserGroupList(); list.getUserGroups().addAll(DbObjectMapper.map(ResourceTypeEnum.USER_GROUP, userGroups)); return list; } /** * Gets the details of one user group. * * @param id of the user group to be returned. * @brief The details of the active user group is returned. * @return The user groups details as UserGroupRestRep * @see UserGroupRestRep */ @GET @Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON }) @Path("/{id}") public UserGroupRestRep getUserGroup(@PathParam("id") URI id) { checkCompatibleVersion(); checkIfUserHasPermissions(); UserGroup userGroup = getUserGroupById(id, false); ArgValidator.checkEntityNotNull(userGroup, id, isIdEmbeddedInURL(id)); return map(userGroup); } /** * Deletes the active user group. * * @param id of the user group to be deleted. * @brief The user group that matches the id will be deactivated. * @return Ok if deletion is successful otherwise valid exception. */ @DELETE @CheckPermission(roles = { Role.SECURITY_ADMIN }) @Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON }) @Path("/{id}") public Response deleteUserGroup(@PathParam("id") URI id) { checkCompatibleVersion(); UserGroup userGroup = getUserGroupById(id, false); ArgValidator.checkEntityNotNull(userGroup, id, isIdEmbeddedInURL(id)); // check that there are no active resources that uses this user group. checkForActiveUsageOfUserGroup(userGroup.getDomain(), userGroup.getLabel()); _dbClient.removeObject(userGroup); auditOp(OperationTypeEnum.DELETE_USERGROUP, true, null, userGroup.getId().toString()); return Response.ok().build(); } /** * Retrieve resource representations based on input ids. * * @param param POST data containing the id list. * @prereq none * @brief List data of user group resources * @return list of representations. * @throws DatabaseException When an error occurs querying the database. */ @POST @Path("/bulk") @Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON }) @Override public UserGroupBulkRestRep getBulkResources(BulkIdParam param) { return (UserGroupBulkRestRep) super.getBulkResources(param); } /** * Validates if all the user attributes param contains the valid set of * keys and values. * * @param attributeParams */ private void validateUserAttributeParam(Set<UserAttributeParam> attributeParams) { for (UserAttributeParam userAttributeParam : attributeParams) { if (userAttributeParam != null) { ArgValidator.checkFieldNotNull(userAttributeParam.getKey(), "key"); ArgValidator.checkFieldNotEmpty(userAttributeParam.getValues(), "values"); } } } /** * Validates if UserGroupCreateParam of the create api * contains all the valid payload that is expected. * * @param param of user group create api payload to be validated. */ private void validateUserGroupCreateParam(UserGroupCreateParam param) { validateUserGroupBaseParam(param); ArgValidator.checkFieldNotEmpty(param.getAttributes(), "attributes"); // Make sure all the UserAttributeParam contains valid attribute key and values. validateUserAttributeParam(param.getAttributes()); } /** * Validates if UserGroupUpdateParam of the update api * contains all the valid payload that is expected. * * @param param of user group update api to be validated. */ private void validateUserGroupUpdateParam(UserGroupUpdateParam param) { validateUserGroupBaseParam(param); // Make sure all the UserAttributeParam contains valid attribute key and values. validateUserAttributeParam(param.getAddAttributes()); } /** * Validates if UserGroupBaseParam of the create/update api * contains all the valid payload that is expected. * * @param param of user group create/update api to be validated. */ private void validateUserGroupBaseParam(UserGroupBaseParam param) { if (param == null) { throw APIException.badRequests.resourceEmptyConfiguration("user group"); } _log.debug("Validate user group param: {}", param); ArgValidator.checkFieldNotNull(param.getDomain(), "domain"); ArgValidator.checkFieldNotNull(param.getLabel(), "label"); if (param.getLabel().contains("@")) { throw APIException.badRequests.invalidParameter("label", param.getLabel()); } if (!authNProviderExistsForDomain(param.getDomain())) { throw APIException.badRequests.invalidParameter("domain", param.getDomain()); } } /** * Overlay the UserGroup (a db object) with the information * for the update api payload. * * @param to user group db object to be updated. * @param from user group update api payload to be * updated in the user group db object. */ private void overlayUserGroup(UserGroup to, UserGroupUpdateParam from) { if (from == null || to == null) { throw APIException.badRequests.resourceEmptyConfiguration("user group"); } if (!to.getLabel().equalsIgnoreCase(from.getLabel())) { throw APIException.badRequests.cannotRenameUserGroup(to.getLabel()); } if (!to.getDomain().equalsIgnoreCase(from.getDomain())) { checkForActiveUsageOfUserGroup(to.getDomain(), to.getLabel()); } to.setDomain(from.getDomain()); to.setLabel(from.getLabel()); Map<String, UserAttributeParam> userAttributeParamMap = getUserAttributesToMap(to.getAttributes()); Map<String, UserAttributeParam> FromUserAttributeParamMap = getUserAttributesToMap(from.getAddAttributes()); if (!CollectionUtils.isEmpty(FromUserAttributeParamMap)) { for (Map.Entry<String, UserAttributeParam> addAttribute : FromUserAttributeParamMap.entrySet()) { addToMapIfNotExist(userAttributeParamMap, addAttribute); } } if (!CollectionUtils.isEmpty(from.getRemoveAttributes())) { for (String removeAttribute : from.getRemoveAttributes()) { userAttributeParamMap.remove(removeAttribute); } } if (CollectionUtils.isEmpty(userAttributeParamMap)) { ArgValidator.checkFieldNotEmpty(userAttributeParamMap, "Attempt to remove the last attribute is not allowed. At least one attribute must be in the user group."); } StringSet attributesToAdd = new StringSet(); for (UserAttributeParam userAttributeParam : userAttributeParamMap.values()) { attributesToAdd.add(userAttributeParam.toString()); } to.getAttributes().replace(attributesToAdd); } /*** * Add the user attribute param to map if it is not already exist in the map. * * @param userAttributeParamMap * @param addAttribute */ private void addToMapIfNotExist(Map<String, UserAttributeParam> userAttributeParamMap, Map.Entry<String, UserAttributeParam> addAttribute) { if (CollectionUtils.isEmpty(userAttributeParamMap)) { _log.info("Invalid map to add the entries"); return; } UserAttributeParam userAttributeParam = userAttributeParamMap.get(addAttribute.getKey()); if (userAttributeParam == null) { userAttributeParamMap.put(addAttribute.getKey(), addAttribute.getValue()); } else { userAttributeParam.setValues(addAttribute.getValue().getValues()); } } /** * Creates a map with key as attribute key and user attribute * param as value. So, that each key (attribute) will have only one set * of values. * * @param attributes to be converted to map. * @return returns the map<String, UserAttributeParam> with key * as attribute key. */ private Map<String, UserAttributeParam> getUserAttributesToMap(StringSet attributes) { Map<String, UserAttributeParam> userAttributeParamMap = new TreeMap<String, UserAttributeParam>(String.CASE_INSENSITIVE_ORDER); if (CollectionUtils.isEmpty(attributes)) { _log.warn("Invalid attributes set"); return userAttributeParamMap; } for (String userAttributeParamString : attributes) { if (StringUtils.isBlank(userAttributeParamString)) { _log.debug("Invalid user attributes param string {} in user group", userAttributeParamString); continue; } UserAttributeParam userAttributeParam = UserAttributeParam.fromString(userAttributeParamString); if (userAttributeParam == null) { _log.info("Failed to convert user attributes param string {} to object.", userAttributeParamString); continue; } UserAttributeParam userAttributeParamFromMap = userAttributeParamMap.get(userAttributeParam.getKey()); if (userAttributeParamFromMap == null) { userAttributeParamMap.put(userAttributeParam.getKey(), userAttributeParam); } else { userAttributeParamFromMap.getValues().addAll(userAttributeParam.getValues()); } } return userAttributeParamMap; } /** * Creates a map with key as attribute key and user attribute * param as value. So, that each key (attribute) will have only one set * of values. * * @param userAttributeParams to be converted to map. * @return returns the map<String, UserAttributeParam> with key * as attribute key. */ private Map<String, UserAttributeParam> getUserAttributesToMap(Set<UserAttributeParam> userAttributeParams) { StringSet userAttributesStringSet = new StringSet(); if (!CollectionUtils.isEmpty(userAttributeParams)) { for (UserAttributeParam param : userAttributeParams) { if (param != null) { userAttributesStringSet.add(param.toString()); } } } Map<String, UserAttributeParam> userAttributeParamMap = getUserAttributesToMap(userAttributesStringSet); return userAttributeParamMap; } /** * Check if the user group object presented with the label * and domain is actively used by any other resources (Tenants userMapping, * VDC role-assignments, tenants role-assignments, projects acls, catalog acls). * * @param domain for which this user group is configured. * @param label name that represents the user group in the db. * This name is what used in all above said resources to represent it. */ void checkForActiveUsageOfUserGroup(String domain, String label) { // Check if VDC rol-assignment references the user group. Set<URI> resourcesUsingUserGroup = new HashSet<URI>(); Set<URI> vdcURI = _permissionsHelper.checkForActiveVDCRoleAssignmentsUsingUserGroup(label); if (!CollectionUtils.isEmpty(vdcURI)) { resourcesUsingUserGroup.addAll(vdcURI); } // Check if tenants role-assignments references the user group. Set<URI> tenantsURI = _permissionsHelper.checkForActiveTenantRoleAssignmentsUsingUserGroup(label); if (!CollectionUtils.isEmpty(tenantsURI)) { resourcesUsingUserGroup.addAll(tenantsURI); } // Check if tenants user mapping for the domain references the user group. Set<URI> userMappingsURI = _permissionsHelper.checkForActiveUserMappingUsingGroup(domain, label); if (!CollectionUtils.isEmpty(userMappingsURI)) { resourcesUsingUserGroup.addAll(userMappingsURI); } // Check if project acls references the user group. Set<URI> projectsURI = _permissionsHelper.checkForActiveProjectAclsUsingUserGroup(label); if (!CollectionUtils.isEmpty(projectsURI)) { resourcesUsingUserGroup.addAll(projectsURI); } // Check if catalog categories acls references the user group. Set<URI> catalogCategoriesURI = _permissionsHelper.checkForActiveCatalogCategoryAclsUsingUserGroup(label); if (!CollectionUtils.isEmpty(catalogCategoriesURI)) { resourcesUsingUserGroup.addAll(catalogCategoriesURI); } // Check if catalog services acls references the user group. Set<URI> catalogServicesURI = _permissionsHelper.checkForActiveCatalogServiceAclsUsingUserGroup(label); if (!CollectionUtils.isEmpty(catalogServicesURI)) { resourcesUsingUserGroup.addAll(catalogServicesURI); } if (!CollectionUtils.isEmpty(resourcesUsingUserGroup)) { throw APIException.badRequests.cannotDeleteOrEditUserGroup(resourcesUsingUserGroup.size(), resourcesUsingUserGroup); } } /** * Check if a provider exists for the given domain * * @param domain * @return */ private boolean authNProviderExistsForDomain(String domain) { URIQueryResultList providers = new URIQueryResultList(); try { _dbClient.queryByConstraint(AlternateIdConstraint.Factory .getAuthnProviderDomainConstraint(domain), providers); } catch (DatabaseException ex) { _log.error( "Could not query for authn providers to check for existing domain {}", domain, ex.getStackTrace()); throw ex; } // check if there is an AuthnProvider contains the given domain and not in disabled state boolean bExist = false; Iterator<URI> it = providers.iterator(); while (it.hasNext()) { URI providerURI = it.next(); AuthnProvider provider = _dbClient.queryObject(AuthnProvider.class, providerURI); if (provider != null && provider.getDisable() == false) { bExist = true; break; } } return bExist; } /** * Check if the user contains any the these roles. * Roles required : Tenant.Admin, Security.Admin, Project.OWN. * */ private void checkIfUserHasPermissions() { StorageOSUser user = getUserFromContext(); if ((!_permissionsHelper.userHasGivenRoleInAnyTenant(user, Role.SECURITY_ADMIN, Role.TENANT_ADMIN)) && (!_permissionsHelper.userHasGivenProjectACL(user, ACL.OWN))) { throw APIException.forbidden.insufficientPermissionsForUser(user.getName()); } } /** * Check if all the VDCs in the federation are in the same expected * or minimum supported version for this api. * */ private void checkCompatibleVersion() { if (!_dbClient.checkGeoCompatible(EXPECTED_GEO_VERSION)) { throw APIException.badRequests.incompatibleGeoVersions(EXPECTED_GEO_VERSION, FEATURE_NAME); } } /** * Returns the minimum expected version for this API to the * consumers of the apisvc (portal). * * @return minimum expected geo version for this api. */ public static String getExpectedGeoVDCVersion() { return EXPECTED_GEO_VERSION; } private void checkForOverlappingUserGroup(UserGroup userGroup) { if (userGroup == null) { _log.error("Invalid user group to compare"); return; } List<UserGroup> userGroupList = _permissionsHelper.getAllUserGroupForDomain(userGroup.getDomain()); if (CollectionUtils.isEmpty(userGroupList)) { _log.debug("No user group found for the domain {}", userGroup.getDomain()); return; } Set<String> overlappingGroups = new HashSet<String>(); for (UserGroup existingUserGroup : userGroupList) { if (existingUserGroup == null) { _log.info("Invalid user group found in db"); continue; } if ((!userGroup.getLabel().equalsIgnoreCase(existingUserGroup.getLabel())) && userGroup.overlap(existingUserGroup)) { overlappingGroups.add(existingUserGroup.getLabel()); } if ((!userGroup.getLabel().equalsIgnoreCase(existingUserGroup.getLabel())) && existingUserGroup.overlap(userGroup)) { overlappingGroups.add(existingUserGroup.getLabel()); } } if (!CollectionUtils.isEmpty(overlappingGroups)) { throw APIException.badRequests.overlappingAttributesNotAllowed(userGroup.getLabel(), overlappingGroups); } } @Override public UserGroupBulkRestRep queryBulkResourceReps(List<URI> ids) { Iterator<UserGroup> _dbIterator = _dbClient.queryIterativeObjects(getResourceClass(), ids); return new UserGroupBulkRestRep(BulkList.wrapping(_dbIterator, MapUserGroup.getInstance())); } @Override protected UserGroupBulkRestRep queryFilteredBulkResourceReps(List<URI> ids) { Iterator<UserGroup> _dbIterator = _dbClient.queryIterativeObjects(getResourceClass(), ids); BulkList.ResourceFilter filter = new BulkList.UserGroupFilter(getUserFromContext(), _permissionsHelper); return new UserGroupBulkRestRep(BulkList.wrapping(_dbIterator, MapUserGroup.getInstance(), filter)); } }