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.thinkbiganalytics.feedmgr.InvalidOperationException; import com.thinkbiganalytics.feedmgr.rest.beanvalidation.NewFeedCategory; import com.thinkbiganalytics.feedmgr.rest.model.EntityAccessRoleMembership; import com.thinkbiganalytics.feedmgr.rest.model.FeedCategory; import com.thinkbiganalytics.feedmgr.rest.model.FeedSummary; import com.thinkbiganalytics.feedmgr.rest.model.UserProperty; import com.thinkbiganalytics.feedmgr.service.AccessControlledEntityTransform; import com.thinkbiganalytics.feedmgr.service.MetadataService; import com.thinkbiganalytics.feedmgr.service.security.SecurityService; import com.thinkbiganalytics.metadata.api.feed.Feed; import com.thinkbiganalytics.rest.model.RestResponseStatus; import com.thinkbiganalytics.rest.model.beanvalidation.UUID; 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.RoleMembership; import com.thinkbiganalytics.security.rest.model.RoleMembershipChange; import com.thinkbiganalytics.security.rest.model.PermissionsChange.ChangeType; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.security.Principal; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Optional; 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.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.DefaultValue; import javax.ws.rs.GET; 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; /** * REST API for managing categories within the Feed Manager. */ @Api(tags = "Feed Manager - Categories", produces = "application/json") @Path(FeedCategoryRestController.BASE) @Component @SwaggerDefinition(tags = @Tag(name = "Feed Manager - Categories", description = "manages categories")) public class FeedCategoryRestController { private static final Logger log = LoggerFactory.getLogger(FeedCategoryRestController.class); public static final String BASE = "/v1/feedmgr/categories"; @Autowired MetadataService metadataService; @Inject private SecurityService securityService; @Inject private SecurityModelTransform actionsTransform; @Inject AccessControlledEntityTransform accessControlledEntityTransform; private MetadataService getMetadataService() { return metadataService; } @GET @Produces(MediaType.APPLICATION_JSON) @ApiOperation("Gets the list of categories.") @ApiResponses( @ApiResponse(code = 200, message = "Returns the list of categories.", response = FeedCategory.class, responseContainer = "List") ) public Response getCategories() { Collection<FeedCategory> categories = getMetadataService().getCategories(); return Response.ok(categories).build(); } @POST @Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_FORM_URLENCODED}) @Produces(MediaType.APPLICATION_JSON) @ApiOperation("Creates or updates a category.") @ApiResponses({ @ApiResponse(code = 200, message = "The category was saved successfully.", response = FeedCategory.class), @ApiResponse(code = 400, message = "The category name is invalid.", response = RestResponseStatus.class), @ApiResponse(code = 500, message = "The category could not be saved.", response = RestResponseStatus.class) }) public Response saveCategory(@NewFeedCategory FeedCategory feedCategory) { getMetadataService().saveCategory(feedCategory); //requery it to get the allowed actions FeedCategory savedCategory = getMetadataService().getCategoryBySystemName(feedCategory.getSystemName()); return Response.ok(savedCategory).build(); } @DELETE @Path("/{categoryId}") @Produces(MediaType.APPLICATION_JSON) @ApiOperation("Deletes the specified category.") @ApiResponses({ @ApiResponse(code = 200, message = "The category was deleted."), @ApiResponse(code = 400, message = "The categoryId is invalid.", response = RestResponseStatus.class), @ApiResponse(code = 500, message = "The category could not be deleted.", response = RestResponseStatus.class) }) public Response deleteCategory(@UUID @PathParam("categoryId") String categoryId) throws InvalidOperationException { getMetadataService().deleteCategory(categoryId); return Response.ok().build(); } @GET @Path("/{categoryId}/feeds") @Produces(MediaType.APPLICATION_JSON) @ApiOperation("Gets the feeds for the specified category.") @ApiResponses({ @ApiResponse(code = 200, message = "The list of related feeds."), @ApiResponse(code = 400, message = "The categoryId is invalid.", response = RestResponseStatus.class) }) public Response getCategory(@UUID @PathParam("categoryId") String categoryId) { List<FeedSummary> summaryList = getMetadataService().getFeedSummaryForCategory(categoryId); return Response.ok(summaryList).build(); } /** * Returns the user fields for categories. * * @return the user fields */ @GET @Path("user-fields") @Produces(MediaType.APPLICATION_JSON) @ApiOperation("Returns the user fields for categories.") @ApiResponses( @ApiResponse(code = 200, message = "Returns the user fields.", response = UserProperty.class, responseContainer = "List") ) @Nonnull public Response getCategoryUserFields() { final Set<UserProperty> userFields = getMetadataService().getCategoryUserFields(); return Response.ok(userFields).build(); } /** * Returns the user fields for feeds within the specified category. * * @param categoryId the category id * @return the user fields * @throws NotFoundException if the category does not exist */ @GET @Path("{categoryId}/user-fields") @Produces(MediaType.APPLICATION_JSON) @ApiOperation("Returns the user fields for feeds within the specified category.") @ApiResponses({ @ApiResponse(code = 200, message = "Returns the user fields.", response = UserProperty.class, responseContainer = "List"), @ApiResponse(code = 400, message = "The categoryId is invalid.", response = RestResponseStatus.class) }) @Nonnull public Response getFeedUserFields(@Nonnull @PathParam("categoryId") @UUID final String categoryId) { final Set<UserProperty> userFields = getMetadataService().getFeedUserFields(categoryId).orElseThrow(NotFoundException::new); return Response.ok(userFields).build(); } @GET @Path("{categoryId}/actions/available") @Produces(MediaType.APPLICATION_JSON) @ApiOperation("Gets the list of available actions that may be permitted or revoked on a category.") @ApiResponses({ @ApiResponse(code = 200, message = "Returns the actions.", response = ActionGroup.class), @ApiResponse(code = 404, message = "A category with the given ID does not exist.", response = RestResponseStatus.class) }) public Response getAvailableActions(@PathParam("categoryId") String categoryIdStr) { log.debug("Get available actions for category: {}", categoryIdStr); return this.securityService.getAvailableCategoryActions(categoryIdStr) .map(g -> Response.ok(g).build()) .orElseThrow(() -> new WebApplicationException("A category with the given ID does not exist: " + categoryIdStr, Response.Status.NOT_FOUND)); } @GET @Path("{categoryId}/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 category with the given ID does not exist.", response = RestResponseStatus.class) }) public Response getAllowedActions(@PathParam("categoryId") String categoryIdStr, @QueryParam("user") Set<String> userNames, @QueryParam("group") Set<String> groupNames) { log.debug("Get allowed actions for category: {}", categoryIdStr); 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.getAllowedCategoryActions(categoryIdStr, Stream.concat(users.stream(), groups.stream()).collect(Collectors.toSet())) .map(g -> Response.ok(g).build()) .orElseThrow(() -> new WebApplicationException("A category with the given ID does not exist: " + categoryIdStr, Status.NOT_FOUND)); } @POST @Path("{categoryId}/actions/allowed") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) @ApiOperation("Updates the permissions for a category 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 category exists with the specified ID.", response = RestResponseStatus.class) }) public Response postPermissionsChange(@PathParam("categoryId") String categoryIdStr, PermissionsChange changes) { return this.securityService.changeCategoryPermissions(categoryIdStr, changes) .map(g -> Response.ok(g).build()) .orElseThrow(() -> new WebApplicationException("A category with the given ID does not exist: " + categoryIdStr, Response.Status.NOT_FOUND)); } @GET @Path("{categoryId}/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 category exists with the specified ID.", response = RestResponseStatus.class) }) public Response getAllowedPermissionsChange(@PathParam("categoryId") String categoryIdStr, @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.createCategoryPermissionChange(categoryIdStr, ChangeType.valueOf(changeType.toUpperCase()), Stream.concat(users.stream(), groups.stream()).collect(Collectors.toSet())) .map(p -> Response.ok(p).build()) .orElseThrow(() -> new WebApplicationException("A category with the given ID does not exist: " + categoryIdStr, Status.NOT_FOUND)); } @GET @Path("{categoryId}/roles") @Produces(MediaType.APPLICATION_JSON) @ApiOperation("Gets the list of assigned members the category's roles") @ApiResponses({ @ApiResponse(code = 200, message = "Returns the role memberships.", response = ActionGroup.class), @ApiResponse(code = 404, message = "A category with the given ID does not exist.", response = RestResponseStatus.class) }) public Response getRoleMemberships(@PathParam("categoryId") String categoryIdStr,@QueryParam("verbose") @DefaultValue("false") boolean verbose) { if(!verbose) { return this.securityService.getCategoryRoleMemberships(categoryIdStr) .map(m -> Response.ok(m).build()) .orElseThrow(() -> new WebApplicationException("A category with the given ID does not exist: " + categoryIdStr, Status.NOT_FOUND)); } else { Optional<Map<String,RoleMembership>> memberships = this.securityService.getCategoryRoleMemberships(categoryIdStr); 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 category with the given ID does not exist: " + categoryIdStr, Status.NOT_FOUND); } } } @POST @Path("{categoryId}/roles") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) @ApiOperation("Updates the members of one of a category's roles.") @ApiResponses({ @ApiResponse(code = 200, message = "The permissions were changed successfully.", response = ActionGroup.class), @ApiResponse(code = 404, message = "No category exists with the specified ID.", response = RestResponseStatus.class) }) public Response postPermissionsChange(@PathParam("categoryId") String categoryIdStr, RoleMembershipChange changes) { return this.securityService.changeCategoryRoleMemberships(categoryIdStr, changes) .map(m -> Response.ok(m).build()) .orElseThrow(() -> new WebApplicationException("Either a category with the ID \"" + categoryIdStr + "\" does not exist or it does not have a role the named \"" + changes.getRoleName() + "\"", Status.NOT_FOUND)); } }