| /* |
| * 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.nifi.web.api; |
| |
| import io.swagger.annotations.Api; |
| import io.swagger.annotations.ApiOperation; |
| import io.swagger.annotations.ApiParam; |
| import io.swagger.annotations.ApiResponse; |
| import io.swagger.annotations.ApiResponses; |
| import io.swagger.annotations.Authorization; |
| import org.apache.nifi.authorization.AccessDeniedException; |
| import org.apache.nifi.authorization.AuthorizableLookup; |
| import org.apache.nifi.authorization.AuthorizeParameterReference; |
| import org.apache.nifi.authorization.Authorizer; |
| import org.apache.nifi.authorization.ComponentAuthorizable; |
| import org.apache.nifi.authorization.RequestAction; |
| import org.apache.nifi.authorization.SnippetAuthorizable; |
| import org.apache.nifi.authorization.resource.Authorizable; |
| import org.apache.nifi.authorization.user.NiFiUser; |
| import org.apache.nifi.authorization.user.NiFiUserUtils; |
| import org.apache.nifi.groups.ProcessGroup; |
| import org.apache.nifi.web.NiFiServiceFacade; |
| import org.apache.nifi.web.Revision; |
| import org.apache.nifi.web.api.dto.SnippetDTO; |
| import org.apache.nifi.web.api.entity.ComponentEntity; |
| import org.apache.nifi.web.api.entity.SnippetEntity; |
| |
| import javax.servlet.http.HttpServletRequest; |
| import javax.ws.rs.Consumes; |
| import javax.ws.rs.DELETE; |
| import javax.ws.rs.DefaultValue; |
| import javax.ws.rs.HttpMethod; |
| 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.QueryParam; |
| import javax.ws.rs.core.Context; |
| import javax.ws.rs.core.MediaType; |
| import javax.ws.rs.core.Response; |
| import java.net.URI; |
| import java.util.Set; |
| import java.util.function.Consumer; |
| import java.util.stream.Collectors; |
| |
| /** |
| * RESTful endpoint for querying dataflow snippets. |
| */ |
| @Path("/snippets") |
| @Api( |
| value = "/snippets", |
| description = "Endpoint for accessing dataflow snippets." |
| ) |
| public class SnippetResource extends ApplicationResource { |
| private NiFiServiceFacade serviceFacade; |
| private Authorizer authorizer; |
| |
| /** |
| * Populate the uri's for the specified snippet. |
| * |
| * @param entity processors |
| * @return dtos |
| */ |
| private SnippetEntity populateRemainingSnippetEntityContent(SnippetEntity entity) { |
| if (entity.getSnippet() != null) { |
| populateRemainingSnippetContent(entity.getSnippet()); |
| } |
| return entity; |
| } |
| |
| /** |
| * Populates the uri for the specified snippet. |
| */ |
| private SnippetDTO populateRemainingSnippetContent(SnippetDTO snippet) { |
| String snippetGroupId = snippet.getParentGroupId(); |
| |
| // populate the snippet href |
| snippet.setUri(generateResourceUri("process-groups", snippetGroupId, "snippets", snippet.getId())); |
| |
| return snippet; |
| } |
| |
| // -------- |
| // snippets |
| // -------- |
| |
| /** |
| * Authorizes the specified snippet request with the specified request action. This method is used when creating a snippet. Because we do not know what |
| * the snippet will be used for, we just ensure the user has permissions to each selected component. Some actions may require additional permissions |
| * (including referenced services) but those will be enforced when the snippet is used. |
| * |
| * @param authorizer authorizer |
| * @param lookup lookup |
| * @param action action |
| */ |
| private void authorizeSnippetRequest(final SnippetDTO snippetRequest, final Authorizer authorizer, final AuthorizableLookup lookup, final RequestAction action) { |
| final Consumer<Authorizable> authorize = authorizable -> authorizable.authorize(authorizer, action, NiFiUserUtils.getNiFiUser()); |
| |
| // note - we are not authorizing templates or controller services as they are not considered when using this snippet |
| snippetRequest.getProcessGroups().keySet().stream().map(id -> lookup.getProcessGroup(id)).forEach(processGroupAuthorizable -> { |
| // we are not checking referenced services since we do not know how this snippet will be used. these checks should be performed |
| // in a subsequent action with this snippet |
| authorizeProcessGroup(processGroupAuthorizable, authorizer, lookup, action, false, false, false, false, false); |
| }); |
| snippetRequest.getRemoteProcessGroups().keySet().stream().map(id -> lookup.getRemoteProcessGroup(id)).forEach(authorize); |
| snippetRequest.getProcessors().keySet().stream().map(id -> lookup.getProcessor(id).getAuthorizable()).forEach(authorize); |
| snippetRequest.getInputPorts().keySet().stream().map(id -> lookup.getInputPort(id)).forEach(authorize); |
| snippetRequest.getOutputPorts().keySet().stream().map(id -> lookup.getOutputPort(id)).forEach(authorize); |
| snippetRequest.getConnections().keySet().stream().map(id -> lookup.getConnection(id).getAuthorizable()).forEach(authorize); |
| snippetRequest.getFunnels().keySet().stream().map(id -> lookup.getFunnel(id)).forEach(authorize); |
| snippetRequest.getLabels().keySet().stream().map(id -> lookup.getLabel(id)).forEach(authorize); |
| } |
| |
| /** |
| * Creates a snippet based off the specified configuration. |
| * |
| * @param httpServletRequest request |
| * @param requestSnippetEntity A snippetEntity |
| * @return A snippetEntity |
| */ |
| @POST |
| @Consumes(MediaType.APPLICATION_JSON) |
| @Produces(MediaType.APPLICATION_JSON) |
| @ApiOperation( |
| value = "Creates a snippet. The snippet will be automatically discarded if not used in a subsequent request after 1 minute.", |
| response = SnippetEntity.class, |
| authorizations = { |
| @Authorization(value = "Read or Write - /{component-type}/{uuid} - For every component (all Read or all Write) in the Snippet and their descendant components") |
| } |
| ) |
| @ApiResponses( |
| value = { |
| @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."), |
| @ApiResponse(code = 401, message = "Client could not be authenticated."), |
| @ApiResponse(code = 403, message = "Client is not authorized to make this request."), |
| @ApiResponse(code = 404, message = "The specified resource could not be found."), |
| @ApiResponse(code = 409, message = "The request was valid but NiFi was not in the appropriate state to process it. Retrying the same request later may be successful.") |
| } |
| ) |
| public Response createSnippet( |
| @Context HttpServletRequest httpServletRequest, |
| @ApiParam( |
| value = "The snippet configuration details.", |
| required = true |
| ) |
| final SnippetEntity requestSnippetEntity) { |
| |
| if (requestSnippetEntity == null || requestSnippetEntity.getSnippet() == null) { |
| throw new IllegalArgumentException("Snippet details must be specified."); |
| } |
| |
| if (requestSnippetEntity.getSnippet().getId() != null) { |
| throw new IllegalArgumentException("Snippet ID cannot be specified."); |
| } |
| |
| if (requestSnippetEntity.getSnippet().getParentGroupId() == null) { |
| throw new IllegalArgumentException("The parent Process Group of the snippet must be specified."); |
| } |
| |
| if (isReplicateRequest()) { |
| return replicate(HttpMethod.POST, requestSnippetEntity); |
| } else if (isDisconnectedFromCluster()) { |
| verifyDisconnectedNodeModification(requestSnippetEntity.isDisconnectedNodeAcknowledged()); |
| } |
| |
| return withWriteLock( |
| serviceFacade, |
| requestSnippetEntity, |
| lookup -> { |
| final SnippetDTO snippetRequest = requestSnippetEntity.getSnippet(); |
| |
| // the snippet being created may be used later for batch component modifications, |
| // copy/paste, or template creation. during those subsequent actions, the snippet |
| // will again be authorized accordingly (read or write). at this point we do not |
| // know what the snippet will be used for so we need to attempt to authorize as |
| // read OR write |
| |
| try { |
| authorizeSnippetRequest(snippetRequest, authorizer, lookup, RequestAction.READ); |
| } catch (final AccessDeniedException e) { |
| authorizeSnippetRequest(snippetRequest, authorizer, lookup, RequestAction.WRITE); |
| } |
| }, |
| null, |
| (snippetEntity) -> { |
| // set the processor id as appropriate |
| snippetEntity.getSnippet().setId(generateUuid()); |
| |
| // create the snippet |
| final SnippetEntity entity = serviceFacade.createSnippet(snippetEntity.getSnippet()); |
| populateRemainingSnippetEntityContent(entity); |
| |
| // build the response |
| return generateCreatedResponse(URI.create(entity.getSnippet().getUri()), entity).build(); |
| } |
| ); |
| } |
| |
| /** |
| * Move's the components in this Snippet into a new Process Group. |
| * |
| * @param httpServletRequest request |
| * @param snippetId The id of the snippet. |
| * @param requestSnippetEntity A snippetEntity |
| * @return A snippetEntity |
| */ |
| @PUT |
| @Consumes(MediaType.APPLICATION_JSON) |
| @Produces(MediaType.APPLICATION_JSON) |
| @Path("{id}") |
| @ApiOperation( |
| value = "Move's the components in this Snippet into a new Process Group and discards the snippet", |
| response = SnippetEntity.class, |
| authorizations = { |
| @Authorization(value = "Write Process Group - /process-groups/{uuid}"), |
| @Authorization(value = "Write - /{component-type}/{uuid} - For each component in the Snippet and their descendant components") |
| } |
| ) |
| @ApiResponses( |
| value = { |
| @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."), |
| @ApiResponse(code = 401, message = "Client could not be authenticated."), |
| @ApiResponse(code = 403, message = "Client is not authorized to make this request."), |
| @ApiResponse(code = 404, message = "The specified resource could not be found."), |
| @ApiResponse(code = 409, message = "The request was valid but NiFi was not in the appropriate state to process it. Retrying the same request later may be successful.") |
| } |
| ) |
| public Response updateSnippet( |
| @Context HttpServletRequest httpServletRequest, |
| @ApiParam( |
| value = "The snippet id.", |
| required = true |
| ) |
| @PathParam("id") String snippetId, |
| @ApiParam( |
| value = "The snippet configuration details.", |
| required = true |
| ) final SnippetEntity requestSnippetEntity) { |
| |
| if (requestSnippetEntity == null || requestSnippetEntity.getSnippet() == null) { |
| throw new IllegalArgumentException("Snippet details must be specified."); |
| } |
| |
| // ensure the ids are the same |
| final SnippetDTO requestSnippetDTO = requestSnippetEntity.getSnippet(); |
| if (!snippetId.equals(requestSnippetDTO.getId())) { |
| throw new IllegalArgumentException(String.format("The snippet id (%s) in the request body does not equal the " |
| + "snippet id of the requested resource (%s).", requestSnippetDTO.getId(), snippetId)); |
| } |
| |
| if (isReplicateRequest()) { |
| return replicate(HttpMethod.PUT, requestSnippetEntity); |
| } else if (isDisconnectedFromCluster()) { |
| verifyDisconnectedNodeModification(requestSnippetEntity.isDisconnectedNodeAcknowledged()); |
| } |
| |
| // get the revision from this snippet |
| final Set<Revision> requestRevisions = serviceFacade.getRevisionsFromSnippet(snippetId); |
| return withWriteLock( |
| serviceFacade, |
| requestSnippetEntity, |
| requestRevisions, |
| lookup -> { |
| final NiFiUser user = NiFiUserUtils.getNiFiUser(); |
| |
| // ensure write access to the target process group |
| if (requestSnippetDTO.getParentGroupId() != null) { |
| lookup.getProcessGroup(requestSnippetDTO.getParentGroupId()).getAuthorizable().authorize(authorizer, RequestAction.WRITE, user); |
| } |
| |
| // ensure write permission to every component in the snippet excluding referenced services |
| final SnippetAuthorizable snippet = lookup.getSnippet(snippetId); |
| |
| // Note: we are explicitly not authorizing parameter references here because they are being authorized below |
| authorizeSnippet(snippet, authorizer, lookup, RequestAction.WRITE, false, false, false); |
| |
| final ProcessGroup destinationGroup = lookup.getProcessGroup(requestSnippetDTO.getParentGroupId()).getProcessGroup(); |
| |
| for (final ComponentAuthorizable componentAuthorizable : snippet.getSelectedProcessors()) { |
| AuthorizeParameterReference.authorizeParameterReferences(destinationGroup, componentAuthorizable, authorizer, user); |
| } |
| }, |
| () -> serviceFacade.verifyUpdateSnippet(requestSnippetDTO, requestRevisions.stream().map(Revision::getComponentId).collect(Collectors.toSet())), |
| (revisions, snippetEntity) -> { |
| // update the snippet |
| final SnippetEntity entity = serviceFacade.updateSnippet(revisions, snippetEntity.getSnippet()); |
| populateRemainingSnippetEntityContent(entity); |
| return generateOkResponse(entity).build(); |
| } |
| ); |
| } |
| |
| /** |
| * Removes the specified snippet. |
| * |
| * @param httpServletRequest request |
| * @param snippetId The id of the snippet to remove. |
| * @return A entity containing the client id and an updated revision. |
| */ |
| @DELETE |
| @Consumes(MediaType.WILDCARD) |
| @Produces(MediaType.APPLICATION_JSON) |
| @Path("{id}") |
| @ApiOperation( |
| value = "Deletes the components in a snippet and discards the snippet", |
| response = SnippetEntity.class, |
| authorizations = { |
| @Authorization(value = "Write - /{component-type}/{uuid} - For each component in the Snippet and their descendant components"), |
| @Authorization(value = "Write - Parent Process Group - /process-groups/{uuid}"), |
| } |
| ) |
| @ApiResponses( |
| value = { |
| @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."), |
| @ApiResponse(code = 401, message = "Client could not be authenticated."), |
| @ApiResponse(code = 403, message = "Client is not authorized to make this request."), |
| @ApiResponse(code = 404, message = "The specified resource could not be found."), |
| @ApiResponse(code = 409, message = "The request was valid but NiFi was not in the appropriate state to process it. Retrying the same request later may be successful.") |
| } |
| ) |
| public Response deleteSnippet( |
| @Context final HttpServletRequest httpServletRequest, |
| @ApiParam( |
| value = "Acknowledges that this node is disconnected to allow for mutable requests to proceed.", |
| required = false |
| ) |
| @QueryParam(DISCONNECTED_NODE_ACKNOWLEDGED) @DefaultValue("false") final Boolean disconnectedNodeAcknowledged, |
| @ApiParam( |
| value = "The snippet id.", |
| required = true |
| ) |
| @PathParam("id") final String snippetId) { |
| |
| if (isReplicateRequest()) { |
| return replicate(HttpMethod.DELETE); |
| } else if (isDisconnectedFromCluster()) { |
| verifyDisconnectedNodeModification(disconnectedNodeAcknowledged); |
| } |
| |
| final ComponentEntity requestEntity = new ComponentEntity(); |
| requestEntity.setId(snippetId); |
| |
| // get the revision from this snippet |
| final Set<Revision> requestRevisions = serviceFacade.getRevisionsFromSnippet(snippetId); |
| return withWriteLock( |
| serviceFacade, |
| requestEntity, |
| requestRevisions, |
| lookup -> { |
| // ensure write permission to every component in the snippet excluding referenced services |
| final SnippetAuthorizable snippet = lookup.getSnippet(snippetId); |
| authorizeSnippet(snippet, authorizer, lookup, RequestAction.WRITE, true, false, false); |
| |
| // ensure write permission to the parent process group |
| snippet.getParentProcessGroup().getAuthorizable().authorize(authorizer, RequestAction.WRITE, NiFiUserUtils.getNiFiUser()); |
| }, |
| () -> serviceFacade.verifyDeleteSnippet(snippetId, requestRevisions.stream().map(rev -> rev.getComponentId()).collect(Collectors.toSet())), |
| (revisions, entity) -> { |
| // delete the specified snippet |
| final SnippetEntity snippetEntity = serviceFacade.deleteSnippet(revisions, entity.getId()); |
| return generateOkResponse(snippetEntity).build(); |
| } |
| ); |
| } |
| |
| /* setters */ |
| |
| public void setServiceFacade(NiFiServiceFacade serviceFacade) { |
| this.serviceFacade = serviceFacade; |
| } |
| |
| public void setAuthorizer(Authorizer authorizer) { |
| this.authorizer = authorizer; |
| } |
| } |