package com.thinkbiganalytics.feedmgr.rest.controller;
/*-
* #%L
* thinkbig-feed-manager-controller
* %%
* Copyright (C) 2017 ThinkBig Analytics
* %%
* Licensed 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.
* #L%
*/
import com.google.common.collect.Lists;
import com.mifmif.common.regex.Generex;
import com.thinkbiganalytics.annotations.AnnotatedFieldProperty;
import com.thinkbiganalytics.annotations.AnnotationFieldNameResolver;
import com.thinkbiganalytics.discovery.schema.QueryResult;
import com.thinkbiganalytics.feedmgr.rest.model.EditFeedEntity;
import com.thinkbiganalytics.feedmgr.rest.model.EntityAccessRoleMembership;
import com.thinkbiganalytics.feedmgr.rest.model.FeedMetadata;
import com.thinkbiganalytics.feedmgr.rest.model.FeedSummary;
import com.thinkbiganalytics.feedmgr.rest.model.NifiFeed;
import com.thinkbiganalytics.feedmgr.rest.model.UIFeed;
import com.thinkbiganalytics.feedmgr.service.AccessControlledEntityTransform;
import com.thinkbiganalytics.feedmgr.service.FeedCleanupFailedException;
import com.thinkbiganalytics.feedmgr.service.FeedCleanupTimeoutException;
import com.thinkbiganalytics.feedmgr.service.MetadataService;
import com.thinkbiganalytics.feedmgr.service.datasource.DatasourceService;
import com.thinkbiganalytics.feedmgr.service.feed.DuplicateFeedNameException;
import com.thinkbiganalytics.feedmgr.service.feed.FeedManagerPreconditionService;
import com.thinkbiganalytics.feedmgr.service.security.SecurityService;
import com.thinkbiganalytics.feedmgr.service.template.RegisteredTemplateService;
import com.thinkbiganalytics.feedmgr.sla.ServiceLevelAgreementService;
import com.thinkbiganalytics.hive.service.HiveService;
import com.thinkbiganalytics.hive.util.HiveUtils;
import com.thinkbiganalytics.metadata.FeedPropertySection;
import com.thinkbiganalytics.metadata.FeedPropertyType;
import com.thinkbiganalytics.metadata.rest.model.data.DatasourceDefinition;
import com.thinkbiganalytics.metadata.rest.model.data.DatasourceDefinitions;
import com.thinkbiganalytics.metadata.rest.model.feed.FeedLineageStyle;
import com.thinkbiganalytics.metadata.rest.model.sla.FeedServiceLevelAgreement;
import com.thinkbiganalytics.nifi.rest.client.LegacyNifiRestClient;
import com.thinkbiganalytics.nifi.rest.client.NifiClientRuntimeException;
import com.thinkbiganalytics.nifi.rest.model.NifiProperty;
import com.thinkbiganalytics.nifi.rest.support.NifiPropertyUtil;
import com.thinkbiganalytics.policy.rest.model.PreconditionRule;
import com.thinkbiganalytics.rest.model.RestResponseStatus;
import com.thinkbiganalytics.security.rest.controller.SecurityModelTransform;
import com.thinkbiganalytics.security.rest.model.ActionGroup;
import com.thinkbiganalytics.security.rest.model.PermissionsChange;
import com.thinkbiganalytics.security.rest.model.PermissionsChange.ChangeType;
import com.thinkbiganalytics.security.rest.model.RoleMembership;
import com.thinkbiganalytics.security.rest.model.RoleMembershipChange;
import com.thinkbiganalytics.support.FeedNameUtil;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
import org.glassfish.jersey.media.multipart.FormDataParam;
import org.hibernate.JDBCException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.dao.DataAccessException;
import org.springframework.stereotype.Component;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.AccessDeniedException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.security.AccessControlException;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.annotation.Nonnull;
import javax.inject.Inject;
import javax.ws.rs.ClientErrorException;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.InternalServerErrorException;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import io.swagger.annotations.SwaggerDefinition;
import io.swagger.annotations.Tag;
@Api(tags = "Feed Manager - Feeds", produces = "application/json")
@Path(FeedRestController.BASE)
@Component
@SwaggerDefinition(tags = @Tag(name = "Feed Manager - Feeds", description = "manages feeds"))
public class FeedRestController {
private static final Logger log = LoggerFactory.getLogger(FeedRestController.class);
public static final String BASE = "/v1/feedmgr/feeds";
/**
* Messages for the default locale
*/
private static final ResourceBundle STRINGS = ResourceBundle.getBundle("com.thinkbiganalytics.feedmgr.rest.controller.FeedMessages");
private static final int MAX_LIMIT = 1000;
private static final String NAMES = "/names";
@Autowired
@Qualifier("nifiRestClient")
LegacyNifiRestClient nifiRestClient;
@Autowired
MetadataService metadataService;
//Profile needs hive service
@Autowired
HiveService hiveService;
@Autowired
FeedManagerPreconditionService feedManagerPreconditionService;
@Inject
DatasourceService datasourceService;
@Inject
private SecurityService securityService;
@Inject
private SecurityModelTransform actionsTransform;
@Inject
ServiceLevelAgreementService serviceLevelAgreementService;
@Inject
RegisteredTemplateService registeredTemplateService;
@Inject
AccessControlledEntityTransform accessControlledEntityTransform;
private MetadataService getMetadataService() {
return metadataService;
}
/**
* Creates a new Feed using the specified metadata.
*
* @param editFeedEntity the feed metadata
* @return the feed
*/
@POST
@Path("/edit/{feedId}")
@Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_FORM_URLENCODED})
@Produces(MediaType.APPLICATION_JSON)
@ApiOperation("Creates or updates a feed.")
@ApiResponses(
@ApiResponse(code = 200, message = "Returns the feed including any error messages.", response = NifiFeed.class)
)
@Nonnull
public Response editFeed(@Nonnull final EditFeedEntity editFeedEntity) {
return createFeed(editFeedEntity.getFeedMetadata());
}
private void populateFeed(EditFeedEntity editFeedEntity) {
//fetch the feed
FeedMetadata feed = getMetadataService().getFeedById(editFeedEntity.getFeedMetadata().getFeedId());
FeedMetadata editFeed = editFeedEntity.getFeedMetadata();
switch (editFeedEntity.getAction()) {
case SUMMARY:
updateFeedMetadata(feed, editFeed, FeedPropertySection.SUMMARY);
break;
case NIFI_PROPERTIES:
updateFeedMetadata(feed, editFeed, FeedPropertySection.NIFI_PROPERTIES);
break;
case PROPERTIES:
updateFeedMetadata(feed, editFeed, FeedPropertySection.PROPERTIES);
break;
case TABLE_DATA:
updateFeedMetadata(feed, editFeed, FeedPropertySection.TABLE_DATA);
break;
case SCHEDULE:
updateFeedMetadata(feed, editFeed, FeedPropertySection.SCHEDULE);
break;
default:
break;
}
createFeed(feed);
}
private void updateFeedMetadata(FeedMetadata targetFeedMetadata, FeedMetadata modifiedFeedMetadata, FeedPropertySection feedPropertySection) {
AnnotationFieldNameResolver annotationFieldNameResolver = new AnnotationFieldNameResolver(FeedPropertyType.class);
List<AnnotatedFieldProperty> list = annotationFieldNameResolver.getProperties(FeedMetadata.class);
List<AnnotatedFieldProperty>
sectionList =
list.stream().filter(annotatedFieldProperty -> feedPropertySection.equals(((FeedPropertyType) annotatedFieldProperty.getAnnotation()).section())).collect(Collectors.toList());
sectionList.forEach(annotatedFieldProperty -> {
try {
Object value = FieldUtils.readField(annotatedFieldProperty.getField(), modifiedFeedMetadata);
FieldUtils.writeField(annotatedFieldProperty.getField(), targetFeedMetadata, value);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
});
}
/**
* Creates a new Feed using the specified metadata.
*
* @param feedMetadata the feed metadata
* @return the feed
*/
@POST
@Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_FORM_URLENCODED})
@Produces(MediaType.APPLICATION_JSON)
@ApiOperation("Creates or updates a feed.")
@ApiResponses(
@ApiResponse(code = 200, message = "Returns the feed including any error messages.", response = NifiFeed.class)
)
@Nonnull
public Response createFeed(@Nonnull final FeedMetadata feedMetadata) {
NifiFeed feed;
try {
feed = getMetadataService().createFeed(feedMetadata);
} catch (DuplicateFeedNameException e) {
log.info("Failed to create a new feed due to another feed having the same category/feed name: " + feedMetadata.getCategoryAndFeedDisplayName());
// Create an error message
String msg = "A feed already exists in the category \"" + e.getCategoryName() + "\" with name name \"" + e.getFeedName() + "\"";
// Add error message to feed
feed = new NifiFeed(feedMetadata, null);
feed.addErrorMessage(msg);
feed.setSuccess(false);
} catch (Exception e) {
log.error("Failed to create a new feed.", e);
// Create an error message
String msg = (e.getMessage() != null) ? "Error saving Feed " + e.getMessage() : "An unknown error occurred while saving the feed.";
if (e.getCause() instanceof JDBCException) {
msg += ". " + ((JDBCException) e).getSQLException();
}
// Add error message to feed
feed = new NifiFeed(feedMetadata, null);
feed.addErrorMessage(msg);
feed.setSuccess(false);
}
return Response.ok(feed).build();
}
@POST
@Path("/enable/{feedId}")
@Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_FORM_URLENCODED})
@Produces(MediaType.APPLICATION_JSON)
@ApiOperation("Enables a feed.")
@ApiResponses({
@ApiResponse(code = 200, message = "The feed was enabled.", response = FeedSummary.class),
@ApiResponse(code = 500, message = "The feed could not be enabled.", response = RestResponseStatus.class)
})
public Response enableFeed(@PathParam("feedId") String feedId) {
FeedSummary feed = getMetadataService().enableFeed(feedId);
return Response.ok(feed).build();
}
@POST
@Path("/disable/{feedId}")
@Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_FORM_URLENCODED})
@Produces(MediaType.APPLICATION_JSON)
@ApiOperation("Disables a feed.")
@ApiResponses({
@ApiResponse(code = 200, message = "The feed was disabled.", response = FeedSummary.class),
@ApiResponse(code = 500, message = "The feed could not be disabled.", response = RestResponseStatus.class)
})
public Response disableFeed(@PathParam("feedId") String feedId) {
FeedSummary feed = getMetadataService().disableFeed(feedId);
return Response.ok(feed).build();
}
@GET
@Path(NAMES)
@Produces(MediaType.APPLICATION_JSON)
@ApiOperation("Gets the list of feed summaries.")
@ApiResponses(
@ApiResponse(code = 200, message = "Returns a list of feeds.", response = FeedSummary.class, responseContainer = "List")
)
public Response getFeedNames() {
Collection<FeedSummary> feeds = getMetadataService().getFeedSummaryData();
return Response.ok(feeds).build();
}
@GET
@Produces(MediaType.APPLICATION_JSON)
@ApiOperation("Gets the list of feeds.")
@ApiResponses(
@ApiResponse(code = 200, message = "Returns a list of feeds.", response = FeedMetadata.class, responseContainer = "List")
)
public Response getFeeds(@QueryParam("verbose") @DefaultValue("false") boolean verbose) {
Collection<? extends UIFeed> feeds = getMetadataService().getFeeds(verbose);
return Response.ok(feeds).build();
}
@GET
@Path("/{feedId}")
@Produces(MediaType.APPLICATION_JSON)
@ApiOperation("Gets the specified feed.")
@ApiResponses({
@ApiResponse(code = 200, message = "Returns the feed.", response = FeedMetadata.class),
@ApiResponse(code = 500, message = "The feed is unavailable.", response = RestResponseStatus.class)
})
public Response getFeed(@PathParam("feedId") String feedId) {
FeedMetadata feed = getMetadataService().getFeedById(feedId, true);
return Response.ok(feed).build();
}
@GET
@Path("/by-name/{feedName}")
@Produces(MediaType.APPLICATION_JSON)
@ApiOperation("Gets the specified feed.")
@ApiResponses({
@ApiResponse(code = 200, message = "Returns the feed.", response = FeedMetadata.class),
@ApiResponse(code = 500, message = "The feed is unavailable.", response = RestResponseStatus.class)
})
public Response getFeedByName(@PathParam("feedName") String feedName) {
String categorySystemName = FeedNameUtil.category(feedName);
String feedSystemName = FeedNameUtil.feed(feedName);
if (StringUtils.isNotBlank(categorySystemName) && StringUtils.isNotBlank(feedSystemName)) {
FeedMetadata feed = getMetadataService().getFeedByName(categorySystemName, feedSystemName);
return Response.ok(feed).build();
} else {
throw new NotFoundException("Unable to find the feed for name: " + feedName);
}
}
@GET
@Path("/by-name/{feedName}/field-policies")
@Produces(MediaType.APPLICATION_JSON)
@ApiOperation("Gets the specified feed.")
@ApiResponses({
@ApiResponse(code = 200, message = "Returns the feed field policies (List<FieldPolicy>) as json.", response = List.class),
@ApiResponse(code = 500, message = "The feed is unavailable.", response = RestResponseStatus.class)
})
public Response getFeedFieldPoliciesByName(@PathParam("feedName") String feedName) {
String categorySystemName = FeedNameUtil.category(feedName);
String feedSystemName = FeedNameUtil.feed(feedName);
if (StringUtils.isNotBlank(categorySystemName) && StringUtils.isNotBlank(feedSystemName)) {
FeedMetadata feed = getMetadataService().getFeedByName(categorySystemName, feedSystemName);
if (feed != null && feed.getTable() != null) {
return Response.ok(feed.getTable().getFieldPoliciesJson()).build();
} else {
throw new NotFoundException("Unable to find the feed field policies for name: " + feedName);
}
} else {
throw new NotFoundException("Unable to find the feed field policies for name: " + feedName);
}
}
/**
* Deletes the specified feed.
*
* @param feedId the feed id
* @return the response
*/
@DELETE
@Path("/{feedId}")
@Produces(MediaType.APPLICATION_JSON)
@ApiOperation("Deletes the specified feed.")
@ApiResponses({
@ApiResponse(code = 204, message = "The feed was deleted."),
@ApiResponse(code = 404, message = "The feed does not exist.", response = RestResponseStatus.class),
@ApiResponse(code = 409, message = "There are dependent feeds.", response = RestResponseStatus.class),
@ApiResponse(code = 500, message = "The feed could not be deleted.", response = RestResponseStatus.class)
})
@Nonnull
public Response deleteFeed(@Nonnull @PathParam("feedId") final String feedId) {
try {
getMetadataService().deleteFeed(feedId);
return Response.noContent().build();
} catch (AccessControlException e) {
log.debug("Access controll failure attempting to delete a feed", e);
throw e;
} catch (FeedCleanupFailedException e) {
log.error("Error deleting feed: Cleanup error", e);
throw new InternalServerErrorException(STRINGS.getString("deleteFeed.cleanupError"), e);
} catch (FeedCleanupTimeoutException e) {
log.error("Error deleting feed: Cleanup timeout", e);
throw new InternalServerErrorException(STRINGS.getString("deleteFeed.cleanupTimeout"), e);
} catch (IllegalArgumentException e) {
log.error("Error deleting feed: Illegal Argument", e);
throw new NotFoundException(STRINGS.getString("deleteFeed.notFound"), e);
} catch (final IllegalStateException e) {
log.error("Error deleting feed: Illegal state", e);
throw new ClientErrorException(STRINGS.getString("deleteFeed.hasDependents"), Response.Status.CONFLICT, e);
} catch (NifiClientRuntimeException e) {
log.error("Error deleting feed: NiFi error", e);
throw new InternalServerErrorException(STRINGS.getString("deleteFeed.nifiError"), e);
} catch (Exception e) {
log.error("Error deleting feed: Unknown error", e);
throw new InternalServerErrorException(STRINGS.getString("deleteFeed.unknownError"), e);
}
}
@POST
@Path("/{feedId}/merge-template")
@Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_FORM_URLENCODED})
@Produces(MediaType.APPLICATION_JSON)
@ApiOperation("Updates a feed with the latest template metadata.")
@ApiResponses({
@ApiResponse(code = 200, message = "The feed was updated.", response = FeedMetadata.class),
@ApiResponse(code = 500, message = "The feed could not be updated.", response = RestResponseStatus.class)
})
public Response mergeTemplate(@PathParam("feedId") String feedId, FeedMetadata feed) {
registeredTemplateService.mergeTemplatePropertiesWithFeed(feed);
return Response.ok(feed).build();
}
@GET
@Path("/{feedId}/profile-summary")
@Produces(MediaType.APPLICATION_JSON)
@ApiOperation("Gets a summary of the feed profiles.")
@ApiResponses({
@ApiResponse(code = 200, message = "Returns the profile summaries.", response = Map.class, responseContainer = "List"),
@ApiResponse(code = 500, message = "The profiles are unavailable.", response = RestResponseStatus.class)
})
public Response profileSummary(@PathParam("feedId") String feedId) {
FeedMetadata feedMetadata = getMetadataService().getFeedById(feedId);
final String profileTable = HiveUtils.quoteIdentifier(feedMetadata.getProfileTableName());
String query = "SELECT * from " + profileTable + " where columnname = '(ALL)'";
List<Map<String, Object>> rows = new ArrayList<>();
try {
QueryResult results = hiveService.query(query);
rows.addAll(results.getRows());
//add in the archive date time fields if applicipable
String ARCHIVE_PROCESSOR_TYPE = "com.thinkbiganalytics.nifi.GetTableData";
if (feedMetadata.getInputProcessorType().equalsIgnoreCase(ARCHIVE_PROCESSOR_TYPE)) {
NifiProperty property = NifiPropertyUtil.findPropertyByProcessorType(feedMetadata.getProperties(), ARCHIVE_PROCESSOR_TYPE, "Date Field");
if (property != null && property.getValue() != null) {
String field = property.getValue();
if (field.contains(".")) {
field = StringUtils.substringAfterLast(field, ".");
}
query = "SELECT * from " + profileTable + " where metrictype IN('MIN_TIMESTAMP','MAX_TIMESTAMP') AND columnname = " + HiveUtils.quoteString(field);
QueryResult dateRows = hiveService.query(query);
if (dateRows != null && !dateRows.isEmpty()) {
rows.addAll(dateRows.getRows());
}
}
}
} catch (DataAccessException e) {
if (e.getCause() instanceof org.apache.hive.service.cli.HiveSQLException && e.getCause().getMessage().contains("Table not found")) {
//this exception is ok to swallow since it just means no profile data exists yet
} else if (e.getCause().getMessage().contains("HiveAccessControlException Permission denied")) {
throw new AccessControlException("You do not have permission to execute this hive query");
} else {
throw e;
}
}
return Response.ok(rows).build();
}
@GET
@Path("/{feedId}/profile-stats")
@Produces(MediaType.APPLICATION_JSON)
@ApiOperation("Gets the profile statistics for the specified job.")
@ApiResponses({
@ApiResponse(code = 200, message = "Returns the profile statistics.", response = Map.class, responseContainer = "List"),
@ApiResponse(code = 500, message = "The profile is unavailable.", response = RestResponseStatus.class)
})
public Response profileStats(@PathParam("feedId") String feedId, @QueryParam("processingdttm") String processingdttm) {
FeedMetadata feedMetadata = getMetadataService().getFeedById(feedId);
String profileTable = feedMetadata.getProfileTableName();
String query = "SELECT * from " + HiveUtils.quoteIdentifier(profileTable) + " where processing_dttm = " + HiveUtils.quoteString(processingdttm);
QueryResult rows = hiveService.query(query);
return Response.ok(rows.getRows()).build();
}
@GET
@Path("/{feedId}/profile-invalid-results")
@Produces(MediaType.APPLICATION_JSON)
@ApiOperation("Gets statistics on the invalid rows for the specified job.")
@ApiResponses({
@ApiResponse(code = 200, message = "Returns the invalid row statistics.", response = Map.class, responseContainer = "List"),
@ApiResponse(code = 500, message = "The profile is unavailable.", response = RestResponseStatus.class)
})
public Response queryProfileInvalidResults(
@PathParam("feedId") String feedId,
@QueryParam("processingdttm") String processingdttm,
@QueryParam("limit") int limit,
@QueryParam("filter") String filter) {
FeedMetadata feedMetadata = getMetadataService().getFeedById(feedId);
String condition = "";
if (StringUtils.isNotBlank(filter)) {
condition = " and dlp_reject_reason like " + HiveUtils.quoteString("%" + filter + "%") + " ";
}
return getPage(processingdttm, limit, feedMetadata.getInvalidTableName(), condition);
}
@GET
@Path("/{feedId}/profile-valid-results")
@Produces(MediaType.APPLICATION_JSON)
@ApiOperation("Gets statistics on the valid rows for the specified job.")
@ApiResponses({
@ApiResponse(code = 200, message = "Returns the valid row statistics.", response = Map.class, responseContainer = "List"),
@ApiResponse(code = 500, message = "The profile is unavailable.", response = RestResponseStatus.class)
})
public Response queryProfileValidResults(
@PathParam("feedId") String feedId,
@QueryParam("processingdttm") String processingdttm,
@QueryParam("limit") int limit) {
FeedMetadata feedMetadata = getMetadataService().getFeedById(feedId);
return getPage(processingdttm, limit, feedMetadata.getValidTableName());
}
@GET
@Path("{feedId}/roles")
@Produces(MediaType.APPLICATION_JSON)
@ApiOperation("Gets the list of assigned members the feed's roles")
@ApiResponses({
@ApiResponse(code = 200, message = "Returns the role memberships.", response = ActionGroup.class),
@ApiResponse(code = 404, message = "A feed with the given ID does not exist.", response = RestResponseStatus.class)
})
public Response getRoleMemberships(@PathParam("feedId") String feedIdStr, @QueryParam("verbose") @DefaultValue("false") boolean verbose) {
if (!verbose) {
return this.securityService.getFeedRoleMemberships(feedIdStr)
.map(m -> Response.ok(m).build())
.orElseThrow(() -> new WebApplicationException("A feed with the given ID does not exist: " + feedIdStr, Status.NOT_FOUND));
} else {
Optional<Map<String, RoleMembership>> memberships = this.securityService.getFeedRoleMemberships(feedIdStr);
if (memberships.isPresent()) {
List<EntityAccessRoleMembership>
entityAccessRoleMemberships =
memberships.get().values().stream().map(roleMembership -> accessControlledEntityTransform.toEntityAccessRoleMembership(roleMembership)).collect(Collectors.toList());
return Response.ok(entityAccessRoleMemberships).build();
} else {
throw new WebApplicationException("A feed with the given ID does not exist: " + feedIdStr, Status.NOT_FOUND);
}
}
}
@POST
@Path("{feedId}/roles")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@ApiOperation("Updates the members of one of a feed's roles.")
@ApiResponses({
@ApiResponse(code = 200, message = "The permissions were changed successfully.", response = ActionGroup.class),
@ApiResponse(code = 404, message = "No feed exists with the specified ID.", response = RestResponseStatus.class)
})
public Response postPermissionsChange(@PathParam("feedId") String feedIdStr,
RoleMembershipChange changes) {
return this.securityService.changeFeedRoleMemberships(feedIdStr, changes)
.map(m -> Response.ok(m).build())
.orElseThrow(() -> new WebApplicationException("Either a feed with the ID \"" + feedIdStr
+ "\" does not exist or it does not have a role the named \""
+ changes.getRoleName() + "\"", Status.NOT_FOUND));
}
@GET
@Path("{feedId}/actions/available")
@Produces(MediaType.APPLICATION_JSON)
@ApiOperation("Gets the list of available actions that may be permitted or revoked on a feed.")
@ApiResponses({
@ApiResponse(code = 200, message = "Returns the actions.", response = ActionGroup.class),
@ApiResponse(code = 404, message = "A feed with the given ID does not exist.", response = RestResponseStatus.class)
})
public Response getAvailableActions(@PathParam("feedId") String feedIdStr) {
log.debug("Get available actions for feed: {}", feedIdStr);
return this.securityService.getAvailableFeedActions(feedIdStr)
.map(g -> Response.ok(g).build())
.orElseThrow(() -> new WebApplicationException("A feed with the given ID does not exist: " + feedIdStr, Status.NOT_FOUND));
}
@GET
@Path("{feedId}/actions/allowed")
@Produces(MediaType.APPLICATION_JSON)
@ApiOperation("Gets the list of actions permitted for the given username and/or groups.")
@ApiResponses({
@ApiResponse(code = 200, message = "Returns the actions.", response = ActionGroup.class),
@ApiResponse(code = 404, message = "A feed with the given ID does not exist.", response = RestResponseStatus.class)
})
public Response getAllowedActions(@PathParam("feedId") String feedIdStr,
@QueryParam("user") Set<String> userNames,
@QueryParam("group") Set<String> groupNames) {
log.debug("Get allowed actions for feed: {}", feedIdStr);
Set<? extends Principal> users = Arrays.stream(this.actionsTransform.asUserPrincipals(userNames)).collect(Collectors.toSet());
Set<? extends Principal> groups = Arrays.stream(this.actionsTransform.asGroupPrincipals(groupNames)).collect(Collectors.toSet());
return this.securityService.getAllowedFeedActions(feedIdStr, Stream.concat(users.stream(), groups.stream()).collect(Collectors.toSet()))
.map(g -> Response.ok(g).build())
.orElseThrow(() -> new WebApplicationException("A feed with the given ID does not exist: " + feedIdStr, Status.NOT_FOUND));
}
@POST
@Path("{feedId}/actions/allowed")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@ApiOperation("Updates the permissions for a feed using the supplied permission change request.")
@ApiResponses({
@ApiResponse(code = 200, message = "The permissions were changed successfully.", response = ActionGroup.class),
@ApiResponse(code = 400, message = "The type is not valid.", response = RestResponseStatus.class),
@ApiResponse(code = 404, message = "No feed exists with the specified ID.", response = RestResponseStatus.class)
})
public Response postPermissionsChange(@PathParam("feedId") String feedIdStr,
PermissionsChange changes) {
return this.securityService.changeFeedPermissions(feedIdStr, changes)
.map(g -> Response.ok(g).build())
.orElseThrow(() -> new WebApplicationException("A feed with the given ID does not exist: " + feedIdStr, Status.NOT_FOUND));
}
@GET
@Path("{feedId}/actions/change")
@Produces(MediaType.APPLICATION_JSON)
@ApiOperation("Constructs and returns a permission change request for a set of users/groups containing the actions that the requester may permit or revoke.")
@ApiResponses({
@ApiResponse(code = 200, message = "Returns the change request that may be modified by the client and re-posted.", response = PermissionsChange.class),
@ApiResponse(code = 400, message = "The type is not valid.", response = RestResponseStatus.class),
@ApiResponse(code = 404, message = "No feed exists with the specified ID.", response = RestResponseStatus.class)
})
public PermissionsChange getAllowedPermissionsChange(@PathParam("feedId") String feedIdStr,
@QueryParam("type") String changeType,
@QueryParam("user") Set<String> userNames,
@QueryParam("group") Set<String> groupNames) {
if (StringUtils.isBlank(changeType)) {
throw new WebApplicationException("The query parameter \"type\" is required", Status.BAD_REQUEST);
}
Set<? extends Principal> users = Arrays.stream(this.actionsTransform.asUserPrincipals(userNames)).collect(Collectors.toSet());
Set<? extends Principal> groups = Arrays.stream(this.actionsTransform.asGroupPrincipals(groupNames)).collect(Collectors.toSet());
return this.securityService.createFeedPermissionChange(feedIdStr,
ChangeType.valueOf(changeType.toUpperCase()),
Stream.concat(users.stream(), groups.stream()).collect(Collectors.toSet()))
.orElseThrow(() -> new WebApplicationException("A feed with the given ID does not exist: " + feedIdStr, Status.NOT_FOUND));
}
private Response getPage(String processingdttm, int limit, String table) {
return getPage(processingdttm, limit, table, null);
}
private Response getPage(String processingdttm, int limit, String table, String filter) {
if (limit > MAX_LIMIT) {
limit = MAX_LIMIT;
} else if (limit < 1) {
limit = 1;
}
StringBuilder query = new StringBuilder("SELECT * from " + HiveUtils.quoteIdentifier(table) + " where processing_dttm = " + HiveUtils.quoteString(processingdttm) + " ");
if (StringUtils.isNotBlank(filter)) {
query.append(filter);
}
query.append(" limit ").append(limit);
QueryResult rows = hiveService.query(query.toString());
return Response.ok(rows.getRows()).build();
}
@GET
@Path("/possible-preconditions")
@Produces(MediaType.APPLICATION_JSON)
@ApiOperation("Gets the available preconditions for triggering a feed.")
@ApiResponses(
@ApiResponse(code = 200, message = "Returns the available precondition rules.", response = PreconditionRule.class, responseContainer = "List")
)
public Response getPossiblePreconditions() {
List<PreconditionRule> conditions = feedManagerPreconditionService.getPossiblePreconditions();
return Response.ok(conditions).build();
}
@GET
@Path("/{feedId}/sla")
@Produces(MediaType.APPLICATION_JSON)
@ApiOperation("Gets the service level agreements referenced by a feed.")
@ApiResponses({
@ApiResponse(code = 200, message = "Returns the service level agreements.", response = FeedServiceLevelAgreement.class, responseContainer = "List"),
@ApiResponse(code = 500, message = "The feed is unavailable.", response = RestResponseStatus.class)
})
public Response getSla(@PathParam("feedId") String feedId) {
List<FeedServiceLevelAgreement> sla = serviceLevelAgreementService.getFeedServiceLevelAgreements(feedId);
return Response.ok(sla).build();
}
@POST
@Path("/update-feed-lineage-styles")
@Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_FORM_URLENCODED})
@Produces(MediaType.APPLICATION_JSON)
@ApiOperation("Updates the feed lineage styles.")
@ApiResponses(
@ApiResponse(code = 200, message = "The styles were updated.", response = RestResponseStatus.class)
)
public Response updateFeedLineageStyles(Map<String, FeedLineageStyle> styles) {
datasourceService.refreshFeedLineageStyles(styles);
return Response.ok(new RestResponseStatus.ResponseStatusBuilder().buildSuccess()).build();
}
@POST
@Path("/update-datasource-definitions")
@Produces(MediaType.APPLICATION_JSON)
@ApiOperation("Updates the datasource definitions.")
@ApiResponses(
@ApiResponse(code = 200, message = "Returns the updated definitions..", response = DatasourceDefinitions.class)
)
public DatasourceDefinitions updateDatasourceDefinitions(DatasourceDefinitions definitions) {
if (definitions != null) {
Set<DatasourceDefinition> updatedDefinitions = datasourceService.updateDatasourceDefinitions(definitions.getDefinitions());
if (updatedDefinitions != null) {
return new DatasourceDefinitions(Lists.newArrayList(updatedDefinitions));
}
}
return new DatasourceDefinitions();
}
@POST
@Path("/{feedId}/upload-file")
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Produces(MediaType.APPLICATION_JSON)
@ApiOperation("Uploads a file to be ingested by a feed.")
@ApiResponses({
@ApiResponse(code = 200, message = "The file is ready to be ingested."),
@ApiResponse(code = 500, message = "The file could not be saved.", response = RestResponseStatus.class)
})
public Response uploadFile(@PathParam("feedId") String feedId,
@FormDataParam("file") InputStream fileInputStream,
@FormDataParam("file") FormDataContentDisposition fileMetaData) throws Exception {
FeedMetadata feed = getMetadataService().getFeedById(feedId, false);
// Derive path and file
List<NifiProperty> properties = feed.getProperties();
String dropzone = null;
String regexFileFilter = null;
for (NifiProperty property : properties) {
if (property.getProcessorType().equals("org.apache.nifi.processors.standard.GetFile")) {
if (property.getKey().equals("File Filter")) {
regexFileFilter = property.getValue();
} else if (property.getKey().equals("Input Directory")) {
dropzone = property.getValue();
}
}
}
if (StringUtils.isEmpty(regexFileFilter) || StringUtils.isEmpty(dropzone)) {
throw new IOException("Unable to upload file with empty dropzone and file");
}
File tempTarget = File.createTempFile("kylo-upload", "");
String fileName = "";
try {
Generex fileNameGenerator = new Generex(regexFileFilter);
fileName = fileNameGenerator.random();
// Cleanup oddball characters generated by generex
fileName = fileName.replaceAll("[^A-Za-z0-9\\.\\_\\+\\%\\-\\|]+", "\\.");
java.nio.file.Path dropZoneTarget = Paths.get(dropzone, fileName);
File dropZoneFile = dropZoneTarget.toFile();
if (dropZoneFile.exists()) {
throw new IOException("File with the name [" + fileName + "] already exists in [" + dropzone + "]");
}
Files.copy(fileInputStream, tempTarget.toPath(), StandardCopyOption.REPLACE_EXISTING);
Files.move(tempTarget.toPath(), dropZoneTarget);
// Set read, write
dropZoneFile.setReadable(true);
dropZoneFile.setWritable(true);
} catch (AccessDeniedException e) {
String errTemplate = "Permission denied attempting to write file [%s] to [%s]. Check with system administrator to ensure this application has write permissions to folder";
String err = String.format(errTemplate, fileName, dropzone);
log.error(err);
throw new InternalServerErrorException(err);
} catch (Exception e) {
String errTemplate = "Unexpected exception writing file [%s] to [%s].";
String err = String.format(errTemplate, fileName, dropzone);
log.error(err);
throw new InternalServerErrorException(err);
}
return Response.ok("").build();
}
}