/*
* Copyright (c) 2013 EMC Corporation
* All Rights Reserved
*/
package com.emc.storageos.api.service.impl.resource;
import java.net.URI;
import java.text.MessageFormat;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import com.emc.storageos.db.client.model.TenantOrg;
import com.emc.storageos.util.InvokeTestFailure;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.emc.storageos.api.service.authorization.PermissionsHelper;
import com.emc.storageos.api.service.impl.response.FilterIterator;
import com.emc.storageos.api.service.impl.response.ResRepFilter;
import com.emc.storageos.api.service.impl.response.SearchedResRepList;
import com.emc.storageos.db.client.constraint.PrefixConstraint;
import com.emc.storageos.db.client.model.DataObject;
import com.emc.storageos.db.client.model.ScopedLabel;
import com.emc.storageos.db.client.model.ScopedLabelSet;
import com.emc.storageos.model.BulkIdParam;
import com.emc.storageos.model.BulkRestRep;
import com.emc.storageos.model.RelatedResourceRep;
import com.emc.storageos.model.ResourceTypeEnum;
import com.emc.storageos.model.TagAssignment;
import com.emc.storageos.model.search.SearchResultResourceRep;
import com.emc.storageos.model.search.SearchResults;
import com.emc.storageos.model.search.Tags;
import com.emc.storageos.security.authentication.RequestProcessingUtils;
import com.emc.storageos.security.authentication.StorageOSUser;
import com.emc.storageos.security.authorization.ExcludeLicenseCheck;
import com.emc.storageos.security.authorization.InheritCheckPermission;
import com.emc.storageos.security.authorization.Role;
import com.emc.storageos.svcs.errorhandling.resources.APIException;
/**
* Base class for all resources with
* 1. support for /<base path>/{id}/...
* 2. support for tagging
*/
public abstract class TaggedResource extends ResourceService {
private static Logger _log = LoggerFactory.getLogger(TaggedResource.class);
private static final int DEFAULT_MAX_BULK_SIZE = 4000;
private int _maxBulkSize = DEFAULT_MAX_BULK_SIZE;
/**
* Derived class can set the max bulk size based on its resource rep type.
*/
public void setMaxBulkSize(int maxBulkSize) {
_maxBulkSize = maxBulkSize;
}
public int getMaxBulkSize() {
return _maxBulkSize;
}
/**
* @brief Assign tags to resource
* Assign tags
*
* @prereq none
*
* @param id the URN of a ViPR resource
* @param assignment tag assignments
* @return No data returned in response body
*/
@PUT
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@Path("/{id}/tags")
@InheritCheckPermission(writeAccess = true)
public Tags assignTags(@PathParam("id") URI id, TagAssignment assignment) {
DataObject object = queryResource(id);
ArgValidator.checkEntityNotNull(object, id, isIdEmbeddedInURL(id));
InvokeTestFailure.internalOnlyInvokeTestFailure(InvokeTestFailure.ARTIFICIAL_FAILURE_082);
ScopedLabelSet tagSet = object.getTag();
if (tagSet == null) {
tagSet = new ScopedLabelSet();
object.setTag(tagSet);
}
if (assignment.getAdd() != null && !assignment.getAdd().isEmpty()) {
Iterator<String> it = assignment.getAdd().iterator();
while (it.hasNext()) {
String tagName = it.next();
if (tagName == null || tagName.isEmpty() || tagName.length() < 2) {
throw APIException.badRequests.parameterTooShortOrEmpty("Tag", 2);
}
ScopedLabel tagLabel = new ScopedLabel(getTenantOwnerIdString(id), tagName);
tagSet.add(tagLabel);
}
}
if (assignment.getRemove() != null && !assignment.getRemove().isEmpty()) {
Iterator<String> it = assignment.getRemove().iterator();
while (it.hasNext()) {
String tagName = it.next();
if (tagName == null || tagName.isEmpty()) {
continue;
}
ScopedLabel tagLabel = new ScopedLabel(getTenantOwnerIdString(id), tagName);
if (tagSet.contains(tagLabel)) {
tagSet.remove(tagLabel);
}
}
}
_dbClient.updateAndReindexObject(object);
return getTagsResponse(object);
}
/**
* @brief List tags assigned to resource
* Returns assigned tags
*
* @prereq none
*
* @param id the URN of a ViPR Resource
* @return Tags information
*/
@GET
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@Path("/{id}/tags")
@InheritCheckPermission
public Tags getTags(@PathParam("id") URI id) {
DataObject object = queryResource(id);
ArgValidator.checkEntityNotNull(object, id, isIdEmbeddedInURL(id));
return getTagsResponse(object);
}
private Tags getTagsResponse(DataObject object) {
Tags tags = new Tags();
if (object.getTag() != null) {
for (ScopedLabel label : object.getTag()) {
tags.getTag().add(label.getLabel());
}
}
return tags;
}
private String getTenantOwnerIdString(URI id) {
URI tenantOwner = getTenantOwner(id);
if (tenantOwner == null) {
return null;
} else {
return tenantOwner.toString();
}
}
/**
* Get tenant object from id
*
* it will also check if user have access to the tenant, return the tenant if:
* 1. it is user's home tenant
* 2. it is a subtenant which user has tenant role.
*
* or else throw insufficient permission exception.
*
* @param tenantId the URN of a ViPR tenant
* @return
*/
protected TenantOrg getTenantIfHaveAccess(String tenantId) {
if (!StringUtils.isEmpty(tenantId)) {
URI tenantUri = URI.create(tenantId);
TenantOrg org = _permissionsHelper.getObjectById(tenantUri, TenantOrg.class);
ArgValidator.checkEntity(org, tenantUri, isIdEmbeddedInURL(tenantUri), true);
// check user has access to the input tenant
StorageOSUser user = getUserFromContext();
if (org.getId().toString().equals(user.getTenantId())) {
return org;
} else {
for (String subTenantId : _permissionsHelper.getSubtenantsForUser(user)){
if (org.getId().toString().equals(subTenantId)) {
return org;
}
}
throw APIException.forbidden.insufficientPermissionsForUser(user.getName());
}
}
return null;
}
/**
* Actual Services provide resource query implementation
*/
protected abstract DataObject queryResource(URI id);
/**
*
* return the actual class object of this resource
*
* The base class throws unsupported exception.
* Derived resource class which supports bulk retrieving should override this method.
*/
protected <T extends DataObject> Class<T> getResourceClass() {
throw APIException.methodNotAllowed.notSupported();
}
/**
* Tenant this object belongs to. For non tenant specific objects (like storage system), it returns null
*
* @return
*/
protected abstract URI getTenantOwner(URI id);
// The following 8 methods are used by the Search API.
// Every derived class needs to override them appropriately
protected abstract ResourceTypeEnum getResourceType();
/**
* To detect zone level resources -- default is true
*/
protected boolean isZoneLevelResource() {
return true;
}
/**
* Non-zone level resource, but visible to system admin -- default false
*
* @return
*/
protected boolean isSysAdminReadableResource() {
return false;
}
/**
* Get search results by name in zone (default) or in a specific project.
*
* @return SearchedResRepList
*/
protected SearchedResRepList getNamedSearchResults(String name, URI projectId) {
SearchedResRepList resRepList = null;
if (projectId == null) {
resRepList = new SearchedResRepList(getResourceType());
_dbClient.queryByConstraint(
PrefixConstraint.Factory.getLabelPrefixConstraint(getResourceClass(), name),
resRepList);
} else {
throw APIException.badRequests.parameterNotSupportedFor(
"project-level search",
MessageFormat.format("{0} search", getResourceClass().getName()));
}
return resRepList;
}
/**
* Get search results by tag in zone (default) or in a specific tenant level
*
* @return SearchedResRepList
*/
protected SearchedResRepList getTagSearchResults(String tag, URI tenant) {
SearchedResRepList resRepList = new SearchedResRepList(getResourceType());
_dbClient.queryByConstraint(
PrefixConstraint.Factory.getTagsPrefixConstraint(getResourceClass(), tag, tenant),
resRepList);
return resRepList;
}
/**
* Get search results by project alone.
* By default this fails
*
* @return SearchedResRepList
*/
protected SearchedResRepList getProjectSearchResults(URI projectId) {
throw APIException.badRequests.parameterNotSupportedFor("project",
MessageFormat.format("{0} search", getResourceClass().getName()));
}
/**
* Get object specific search results by parameters other than name and tag.
* Default is not implemented error
*
* @return SearchResults
*/
protected SearchResults getOtherSearchResults(Map<String, List<String>> parameters,
boolean authorized) {
throw APIException.badRequests.unknownParameter("search", parameters.toString());
}
/**
* Get object specific permissions filter, if applicable
* Default is null
*
* @return ResRepFilter<? extends RelatedResourceRep>
*/
protected ResRepFilter<? extends RelatedResourceRep> getPermissionFilter(
StorageOSUser user, PermissionsHelper permissionsHelper)
{
if (!isZoneLevelResource()) {
throw new UnsupportedOperationException("non-zone level resource needs to implement its specific permission filter");
}
throw APIException.forbidden
.insufficientPermissionWhileSearchingZoneLevelResource(getUserFromContext()
.toString());
}
/**
* @brief search API
* Search resources by name, tag, project or additional parameters (for example, wwn or initiator_port etc.)
*
* @prereq none
* @return search results
*/
/*
* Parameters:
* Common Parameters:
* name: Name has to be a minimum of 2 characters. Could only be combined with project parameter
* tag: Tag has to be a minimum of 2 characters. Could only be combined with tenant parameter
* project: The full project URI needs to be provided.
*
* NOTE: Name and tag are not case sensitive.
* For Zone level resources, search by project is not allowed.
* Special Parameters:
* wwn: only used by block service
* initiator_port: only used by virtual array service.
*/
@GET
@Path("/search")
@Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
public SearchResults search() {
// 1. Figure out user privilege
boolean bAuthorized = false;
StorageOSUser user = getUserFromContext();
if (_permissionsHelper.userHasGivenRole(user, null, Role.SYSTEM_MONITOR)) {
bAuthorized = true;
} else if ((isZoneLevelResource() || isSysAdminReadableResource()) &&
(_permissionsHelper.userHasGivenRole(user, null, Role.SYSTEM_ADMIN))) {
bAuthorized = true;
}
// 2. Parameter check and early detection for unsupported search
String name = null, tag = null;
URI projectId = null, tenant = null;
Map<String, List<String>> parameters = uriInfo.getQueryParameters();
// remove non-search related common parameters
parameters.remove(RequestProcessingUtils.REQUESTING_COOKIES);
if (parameters.containsKey("name")) {
name = parameters.get("name").get(0);
checkSearchParameterLength("name", name, getResourceClass());
checkParameterCombination(parameters, getResourceClass(), "name", "project");
}
if (parameters.containsKey("tag")) {
tag = parameters.get("tag").get(0);
checkSearchParameterLength("tag", tag, getResourceClass());
checkParameterCombination(parameters, getResourceClass(), "tag", "tenant");
// set tenant scope for non-zone resources
if (!isZoneLevelResource()) {
if (parameters.containsKey("tenant")) {
tenant = URI.create(parameters.get("tenant").get(0));
}
}
}
if (parameters.containsKey("project")) {
checkParameterCombination(parameters, getResourceClass(), "project", "name");
projectId = URI.create(parameters.get("project").get(0));
if (isZoneLevelResource()) {
throw APIException.badRequests.invalidParameterSearchProjectNotSupported(getResourceClass().getName());
}
// check if user is authorized for the project
if (!bAuthorized) {
if (!isAuthorized(projectId)) {
throw APIException.forbidden
.insufficientPermissionsForUser(getUserFromContext()
.toString());
}
bAuthorized = true;
}
}
// 3. Search from db and response permission-eligible results
SearchResults result = new SearchResults();
if (name != null || tag != null) {
SearchedResRepList resRepList = null;
if (name != null) {
// search named resources
resRepList = getNamedSearchResults(name, projectId);
} else {
// search tagged resources
resRepList = getTagSearchResults(tag, tenant);
}
if (!bAuthorized) {
SearchedResRepList filteredResRepList = new SearchedResRepList();
Iterator<SearchResultResourceRep> _queryResultIterator = resRepList.iterator();
ResRepFilter<SearchResultResourceRep> resrepFilter = null;
resrepFilter = (ResRepFilter<SearchResultResourceRep>) getPermissionFilter(getUserFromContext(), _permissionsHelper);
filteredResRepList.setResult(
new FilterIterator<SearchResultResourceRep>(_queryResultIterator, resrepFilter));
result.setResource(filteredResRepList);
} else {
result.setResource(resRepList);
}
return result;
}
if (projectId != null) {
// start resource search within project
// note: for project resources search, the permission check
// has been addressed in parameter checke period.
result.setResource(getProjectSearchResults(projectId));
return result;
} // end resource search within project
// some other resource specific search
return getOtherSearchResults(parameters, bAuthorized);
}
// End of the search API section
private static void checkParameterCombination(
Map<String, List<String>> parameters, Class<DataObject> resourceClass,
final String first, final String second) {
for (Map.Entry<String, List<String>> entry : parameters.entrySet()) {
if (!entry.getKey().equals(first) && !entry.getKey().equals(second)) {
throw APIException.badRequests.parameterForSearchCouldOnlyBeCombinedWithOtherParameter(resourceClass.getName(), first,
second);
}
}
}
private static void checkSearchParameterLength(final String field, String value, Class<DataObject> resourceClass) {
final int minimum = 2;
if (value.length() < minimum) {
throw APIException.badRequests.invalidParameterSearchStringTooShort(field, value, resourceClass.getName(), minimum);
}
}
/**
* @brief List all instances of resource type
* Retrieve all ids of this type of resources.
*
* @prereq none
*
* @return list of ids.
*/
@GET
@Path("/bulk")
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
public BulkIdParam getBulkIds() {
StorageOSUser user = getUserFromContext();
if (_permissionsHelper.userHasGivenRole(user, null, Role.SYSTEM_MONITOR) ||
((isZoneLevelResource() || isSysAdminReadableResource()) &&
_permissionsHelper.userHasGivenRole(user, null, Role.SYSTEM_ADMIN))) {
return queryBulkIds();
}
throw APIException.forbidden.insufficientPermissionsForUser(user.getName());
}
/**
* Retrieve resource ids.
*
* @return list of IDs of the resources of this type.
*/
protected BulkIdParam queryBulkIds() {
BulkIdParam ret = new BulkIdParam();
ret.setIds(_dbClient.queryByType(getResourceClass(), true));
return ret;
}
/**
* @brief List data of specified resources
* Retrieve resource representations based on input ids.
*
* @prereq none
*
* @param param POST data containing the id list.
* @return list of representations.
*/
@POST
@Path("/bulk")
@Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@ExcludeLicenseCheck
public BulkRestRep getBulkResources(BulkIdParam param) {
return getBulkResources(param.getIds());
}
protected BulkRestRep getBulkResources(List<URI> ids) {
StorageOSUser user = getUserFromContext();
BulkRestRep ret = null;
if (ids.size() > _maxBulkSize) {
throw APIException.badRequests.exceedingLimit("bulk size", _maxBulkSize);
}
// full list for:
// -system monitor
// -sysadmin (if zone level resource or resource is system admin readable)
if (_permissionsHelper.userHasGivenRole(user, null, Role.SYSTEM_MONITOR) ||
((isZoneLevelResource() || isSysAdminReadableResource())
&& _permissionsHelper.userHasGivenRole(user, null, Role.SYSTEM_ADMIN))) {
_log.info("Bulk of {} for sysmonitor/sysadmin",
getResourceClass().getSimpleName());
ret = queryBulkResourceReps(ids);
} else {
_log.info("Bulk of {} for user",
getResourceClass().getSimpleName());
ret = queryFilteredBulkResourceReps(ids);
}
return ret;
}
/**
* Query resource objects from db based on given ids which are accessible
* by teh user in the securiy context and wrap them into resourceRestRep
* objects.
*
* The base class throws unsupported exception.
* Derived resource class which supports bulk retrieving in user context
* should override this method.
*
* @param ids the URN of a ViPR representations to be filtered upon
* @return list of filtered representations.
*/
protected BulkRestRep queryFilteredBulkResourceReps(
List<URI> ids) {
throw APIException.methodNotAllowed.notSupported();
}
/**
* Query resource objects from db based on given ids and wrap them
* into resourceRestRep objects.
*
* The base class throws unsupported exception.
* Derived resource class which supports bulk retrieving should override this method.
*
* @param ids the URN list of a ViPR bulk resource
* @return list of representations.
*/
public BulkRestRep queryBulkResourceReps(List<URI> ids) {
throw APIException.methodNotAllowed.notSupported();
}
protected void verifySystemAdmin() {
if (!isSystemOrRestrictedSystemAdmin()) {
throw APIException.forbidden
.insufficientPermissionsForUser(getUserFromContext().getName());
}
}
protected boolean isSystemAdmin() {
return _permissionsHelper.userHasGivenRole(
getUserFromContext(), null, Role.SYSTEM_ADMIN);
}
protected boolean isRestrictedSystemAdmin() {
return _permissionsHelper.userHasGivenRole(
getUserFromContext(), null, Role.RESTRICTED_SYSTEM_ADMIN);
}
protected boolean isSystemOrRestrictedSystemAdmin() {
return isSystemAdmin() || isRestrictedSystemAdmin();
}
protected boolean isSecurityAdmin() {
return _permissionsHelper.userHasGivenRole(getUserFromContext(), null, Role.SECURITY_ADMIN);
}
}