org.apache.nifi.web.api.UserResource.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.nifi.web.api.UserResource.java

Source

/*
 * 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 com.sun.jersey.api.Responses;
import com.wordnik.swagger.annotations.Api;
import com.wordnik.swagger.annotations.ApiOperation;
import com.wordnik.swagger.annotations.ApiParam;
import com.wordnik.swagger.annotations.ApiResponse;
import com.wordnik.swagger.annotations.ApiResponses;
import com.wordnik.swagger.annotations.Authorization;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
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.MultivaluedMap;
import javax.ws.rs.core.Response;
import org.apache.nifi.cluster.manager.NodeResponse;
import org.apache.nifi.cluster.manager.impl.WebClusterManager;
import org.apache.nifi.util.NiFiProperties;
import org.apache.nifi.web.api.dto.UserDTO;
import org.apache.nifi.web.api.dto.search.UserGroupSearchResultDTO;
import org.apache.nifi.web.api.dto.search.UserSearchResultDTO;
import org.apache.nifi.web.api.entity.UserEntity;
import org.apache.nifi.web.api.entity.UserSearchResultsEntity;
import org.apache.nifi.web.api.entity.UsersEntity;
import org.apache.nifi.web.api.request.ClientIdParameter;
import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.user.NiFiUser;
import org.apache.nifi.web.NiFiServiceFacade;
import static org.apache.nifi.web.api.ApplicationResource.CLIENT_ID;
import org.apache.nifi.web.api.dto.RevisionDTO;
import org.apache.nifi.web.security.user.NiFiUserUtils;
import org.springframework.security.access.prepost.PreAuthorize;

/**
 * RESTful endpoint for managing this Controller's users.
 */
@Api(hidden = true)
public class UserResource extends ApplicationResource {

    /*
     * Developer Note: Clustering assumes a centralized security provider. The
     * cluster manager will manage user accounts when in clustered mode and
     * interface with the authorization provider. However, when nodes perform
     * Site-to-Site, the authorization details of the remote NiFi will be cached
     * locally. These details need to be invalidated when certain actions are
     * performed (revoking/deleting accounts, changing user authorities, user
     * group, etc).
     */
    private WebClusterManager clusterManager;
    private NiFiProperties properties;
    private NiFiServiceFacade serviceFacade;

    /**
     * Creates a new user account request.
     *
     * @return A string
     */
    @POST
    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
    @Produces(MediaType.TEXT_PLAIN)
    @Path("") // necessary due to a bug in swagger
    @ApiOperation(value = "Creates a user", response = String.class)
    public Response createUser() {
        if (!properties.getSupportNewAccountRequests()) {
            return Responses.notFound().entity("This NiFi does not support new account requests.").build();
        }

        final NiFiUser nifiUser = NiFiUserUtils.getNiFiUser();
        if (nifiUser != null) {
            throw new IllegalArgumentException("User account already created " + nifiUser.getIdentity());
        }

        // create an account request for the current user
        final UserDTO user = serviceFacade.createUser();

        final String uri = generateResourceUri("controller", "users", user.getId());
        return generateCreatedResponse(URI.create(uri),
                "Not authorized. User account created. Authorization pending.").build();
    }

    /**
     * Gets all users that are registered within this Controller.
     *
     * @param clientId Optional client id. If the client id is not specified, a new one will be generated. This value (whether specified or generated) is included in the response.
     * @param grouped Whether to return the users in their groups.
     * @return A usersEntity.
     */
    @GET
    @Consumes(MediaType.WILDCARD)
    @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
    @Path("") // necessary due to a bug in swagger
    @PreAuthorize("hasRole('ROLE_ADMIN')")
    @ApiOperation(value = "Gets all users", response = UsersEntity.class, authorizations = {
            @Authorization(value = "Administrator", type = "ROLE_ADMIN") })
    @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 = 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 getUsers(
            @ApiParam(value = "If the client id is not specified, new one will be generated. This value (whether specified or generated) is included in the response.", required = false) @QueryParam(CLIENT_ID) @DefaultValue(StringUtils.EMPTY) ClientIdParameter clientId,
            @ApiParam(value = "Whether to return the users in their respective groups.", required = false) @QueryParam("grouped") @DefaultValue("false") Boolean grouped) {

        // get the users
        final Collection<UserDTO> users = serviceFacade.getUsers(grouped);

        // create the revision
        final RevisionDTO revision = new RevisionDTO();
        revision.setClientId(clientId.getClientId());

        // create the response entity
        final UsersEntity usersEntity = new UsersEntity();
        usersEntity.setRevision(revision);
        usersEntity.setUsers(users);
        usersEntity.setGenerated(new Date());

        // build the response
        return generateOkResponse(usersEntity).build();
    }

    /**
     * Gets the details for the specified user.
     *
     * @param clientId Optional client id. If the client id is not specified, a new one will be generated. This value (whether specified or generated) is included in the response.
     * @param id The user id.
     * @return A userEntity.
     */
    @GET
    @Consumes(MediaType.WILDCARD)
    @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
    @PreAuthorize("hasRole('ROLE_ADMIN')")
    @Path("/{id}")
    @ApiOperation(value = "Gets a user", response = UserEntity.class, authorizations = {
            @Authorization(value = "Administrator", type = "ROLE_ADMIN") })
    @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 getUser(
            @ApiParam(value = "If the client id is not specified, new one will be generated. This value (whether specified or generated) is included in the response.", required = false) @QueryParam(CLIENT_ID) @DefaultValue(StringUtils.EMPTY) ClientIdParameter clientId,
            @ApiParam(value = "The user id.", required = true) @PathParam("id") String id) {

        // get the specified user
        final UserDTO userDTO = serviceFacade.getUser(id);

        // create the revision
        final RevisionDTO revision = new RevisionDTO();
        revision.setClientId(clientId.getClientId());

        // create the response entity
        final UserEntity userEntity = new UserEntity();
        userEntity.setRevision(revision);
        userEntity.setUser(userDTO);

        // build the response
        return generateOkResponse(userEntity).build();
    }

    /**
     * Searches for users with match the specified query.
     *
     * @param value Search value that will be matched against users
     * @return A userSearchResultsEntity
     */
    @GET
    @Consumes(MediaType.WILDCARD)
    @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
    @Path("/search-results")
    @PreAuthorize("hasAnyRole('ROLE_DFM', 'ROLE_ADMIN')")
    @ApiOperation(value = "Searches for users", response = UserSearchResultsEntity.class, authorizations = {
            @Authorization(value = "Data Flow Manager", type = "ROLE_DFM"),
            @Authorization(value = "Administrator", type = "ROLE_ADMIN") })
    @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 = 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 searchUsers(
            @ApiParam(value = "The search terms.", required = true) @QueryParam("q") @DefaultValue(StringUtils.EMPTY) String value) {

        final List<UserSearchResultDTO> userMatches = new ArrayList<>();
        final List<UserGroupSearchResultDTO> userGroupMatches = new ArrayList<>();

        // get the users
        final Collection<UserDTO> users = serviceFacade.getUsers(Boolean.FALSE);
        final Collection<String> matchedGroups = new HashSet<>();

        // check each to see if it matches the search term
        for (UserDTO user : users) {
            // count the user if there is no search or it matches the address
            if (StringUtils.isBlank(value)) {
                // record the group match if there is one and it hasn't already been encountered
                if (user.getUserGroup() != null && !matchedGroups.contains(user.getUserGroup())) {
                    // add the matched group
                    matchedGroups.add(user.getUserGroup());

                    // record the group match
                    final UserGroupSearchResultDTO userGroupMatch = new UserGroupSearchResultDTO();
                    userGroupMatch.setGroup(user.getUserGroup());
                    userGroupMatches.add(userGroupMatch);
                }

                // record the user match
                final UserSearchResultDTO userMatch = new UserSearchResultDTO();
                userMatch.setUserDn(user.getDn());
                userMatch.setUserName(user.getUserName());
                userMatches.add(userMatch);
            } else {
                // look for a user match
                if (StringUtils.containsIgnoreCase(user.getDn(), value)
                        || StringUtils.containsIgnoreCase(user.getUserName(), value)) {
                    // record the user match
                    final UserSearchResultDTO userMatch = new UserSearchResultDTO();
                    userMatch.setUserDn(user.getDn());
                    userMatch.setUserName(user.getUserName());
                    userMatches.add(userMatch);
                }

                // look for a dn match
                if (StringUtils.containsIgnoreCase(user.getUserGroup(), value)) {
                    // record the group match if it hasn't already been encountered
                    if (!matchedGroups.contains(user.getUserGroup())) {
                        // add the matched group
                        matchedGroups.add(user.getUserGroup());

                        // record the group match
                        final UserGroupSearchResultDTO userGroupMatch = new UserGroupSearchResultDTO();
                        userGroupMatch.setGroup(user.getUserGroup());
                        userGroupMatches.add(userGroupMatch);
                    }
                }
            }
        }

        // sort the user matches
        Collections.sort(userMatches, new Comparator<UserSearchResultDTO>() {
            @Override
            public int compare(UserSearchResultDTO user1, UserSearchResultDTO user2) {
                return user1.getUserName().compareTo(user2.getUserName());
            }
        });

        // sort the user group matches
        Collections.sort(userGroupMatches, new Comparator<UserGroupSearchResultDTO>() {
            @Override
            public int compare(UserGroupSearchResultDTO userGroup1, UserGroupSearchResultDTO userGroup2) {
                return userGroup1.getGroup().compareTo(userGroup2.getGroup());
            }
        });

        // build the response
        final UserSearchResultsEntity results = new UserSearchResultsEntity();
        results.setUserResults(userMatches);
        results.setUserGroupResults(userGroupMatches);

        // generate an 200 - OK response
        return noCache(Response.ok(results)).build();
    }

    /**
     * Updates the specified user.
     *
     * @param httpServletRequest request
     * @param clientId Optional client id. If the client id is not specified, a new one will be generated. This value (whether specified or generated) is included in the response.
     * @param id The id of the user to update.
     * @param rawAuthorities Array of authorities to assign to the specified user.
     * @param status The status of the specified users account.
     * @param formParams form params
     * @return A userEntity
     */
    @PUT
    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
    @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
    @PreAuthorize("hasRole('ROLE_ADMIN')")
    @Path("/{id}")
    public Response updateUser(@Context HttpServletRequest httpServletRequest,
            @FormParam(CLIENT_ID) @DefaultValue(StringUtils.EMPTY) ClientIdParameter clientId,
            @PathParam("id") String id, @FormParam("authorities[]") Set<String> rawAuthorities,
            @FormParam("status") String status, MultivaluedMap<String, String> formParams) {

        // create the user
        final UserDTO userDTO = new UserDTO();
        userDTO.setId(id);
        userDTO.setStatus(status);

        // get the collection of specified authorities
        final Set<String> authorities = new HashSet<>();
        for (String authority : rawAuthorities) {
            if (StringUtils.isNotBlank(authority)) {
                authorities.add(authority);
            }
        }

        // set the authorities
        if (!authorities.isEmpty() || formParams.containsKey("authorities")) {
            userDTO.setAuthorities(authorities);
        }

        // create the revision
        final RevisionDTO revision = new RevisionDTO();
        revision.setClientId(clientId.getClientId());

        // create the user entity
        UserEntity userEntity = new UserEntity();
        userEntity.setRevision(revision);
        userEntity.setUser(userDTO);

        // update the user
        return updateUser(httpServletRequest, id, userEntity);
    }

    /**
     * Updates the specified user.
     *
     * @param httpServletRequest request
     * @param id The id of the user to update.
     * @param userEntity A userEntity
     * @return A userEntity
     */
    @PUT
    @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
    @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
    @PreAuthorize("hasRole('ROLE_ADMIN')")
    @Path("/{id}")
    @ApiOperation(value = "Updates a user", response = UserEntity.class, authorizations = {
            @Authorization(value = "Administrator", type = "ROLE_ADMIN") })
    @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 updateUser(@Context HttpServletRequest httpServletRequest,
            @ApiParam(value = "The user id.", required = true) @PathParam("id") String id,
            @ApiParam(value = "The user configuration details.", required = true) UserEntity userEntity) {

        if (userEntity == null || userEntity.getUser() == null) {
            throw new IllegalArgumentException("User details must be specified.");
        }

        // ensure the same user id is being used
        final UserDTO userDTO = userEntity.getUser();
        if (!id.equals(userDTO.getId())) {
            throw new IllegalArgumentException(String.format("The user id (%s) in the request body does "
                    + "not equal the user id of the requested resource (%s).", userDTO.getId(), id));
        }

        // create the revision
        final RevisionDTO revision = new RevisionDTO();
        if (userEntity.getRevision() == null) {
            revision.setClientId(new ClientIdParameter().getClientId());
        } else {
            revision.setClientId(userEntity.getRevision().getClientId());
        }

        // this user is being modified, replicate to the nodes to invalidate this account
        // so that it will be re-authorized during the next attempted access - if this wasn't
        // done the account would remain stale for up to the configured cache duration. this
        // is acceptable sometimes but when updating a users authorities or groups via the UI
        // they shouldn't have to wait for the changes to take effect`
        if (properties.isClusterManager()) {
            // change content type to JSON for serializing entity
            final Map<String, String> headersToOverride = new HashMap<>();
            headersToOverride.put("content-type", MediaType.APPLICATION_JSON);

            // identify yourself as the NCM attempting to invalidate the user
            final Map<String, String> headers = getHeaders(headersToOverride);
            headers.put(WebClusterManager.CLUSTER_INVALIDATE_USER_HEADER, Boolean.TRUE.toString());

            final RevisionDTO invalidateUserRevision = new RevisionDTO();
            revision.setClientId(revision.getClientId());

            final UserDTO invalidateUser = new UserDTO();
            invalidateUser.setId(userDTO.getId());

            final UserEntity invalidateUserEntity = new UserEntity();
            invalidateUserEntity.setRevision(invalidateUserRevision);
            invalidateUserEntity.setUser(userDTO);

            // replicate the invalidate request to each node - if this request is not successful return that fact,
            // otherwise continue with the desired user modification
            final NodeResponse response = clusterManager.applyRequest(HttpMethod.PUT, getAbsolutePath(),
                    invalidateUserEntity, headers);
            if (!response.is2xx()) {
                return response.getResponse();
            }
        }

        // handle expects request (usually from the cluster manager)
        final String expects = httpServletRequest.getHeader(WebClusterManager.NCM_EXPECTS_HTTP_HEADER);
        if (expects != null) {
            return generateContinueResponse().build();
        }

        // handle an invalidate request from the NCM
        final String invalidateRequest = httpServletRequest
                .getHeader(WebClusterManager.CLUSTER_INVALIDATE_USER_HEADER);
        if (invalidateRequest != null) {
            serviceFacade.invalidateUser(id);
            return generateOkResponse().build();
        }

        // update the user
        final UserDTO reponseUserDTO = serviceFacade.updateUser(userDTO);

        // create the response entity
        UserEntity responseUserEntity = new UserEntity();
        responseUserEntity.setRevision(revision);
        responseUserEntity.setUser(reponseUserDTO);

        // build the response
        return generateOkResponse(responseUserEntity).build();
    }

    /**
     * Deletes the specified user.
     *
     * @param httpServletRequest request
     * @param id The user id
     * @param clientId Optional client id. If the client id is not specified, a new one will be generated. This value (whether specified or generated) is included in the response.
     * @return A userEntity.
     */
    @DELETE
    @Consumes(MediaType.WILDCARD)
    @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
    @Path("/{id}")
    @PreAuthorize("hasRole('ROLE_ADMIN')")
    @ApiOperation(value = "Deletes a user", response = UserEntity.class, authorizations = {
            @Authorization(value = "Administrator", type = "ROLE_ADMIN") })
    @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 deleteUser(@Context HttpServletRequest httpServletRequest,
            @ApiParam(value = "The user id.", required = true) @PathParam("id") String id,
            @ApiParam(value = "If the client id is not specified, new one will be generated. This value (whether specified or generated) is included in the response.", required = false) @QueryParam(CLIENT_ID) @DefaultValue(StringUtils.EMPTY) ClientIdParameter clientId) {

        // this user is being modified, replicate to the nodes to invalidate this account
        // so that it will be re-authorized during the next attempted access - if this wasn't
        // done the account would remain stale for up to the configured cache duration. this
        // is acceptable sometimes but when removing a user via the UI they shouldn't have to
        // wait for the changes to take effect
        if (properties.isClusterManager()) {
            // identify yourself as the NCM attempting to invalidate the user
            final Map<String, String> headers = getHeaders();
            headers.put(WebClusterManager.CLUSTER_INVALIDATE_USER_HEADER, Boolean.TRUE.toString());

            // replicate the invalidate request to each node - if this request is not successful return that fact,
            // otherwise continue with the desired user modification
            final NodeResponse response = clusterManager.applyRequest(HttpMethod.DELETE, getAbsolutePath(),
                    getRequestParameters(true), headers);
            if (!response.is2xx()) {
                return response.getResponse();
            }
        }

        // handle expects request (usually from the cluster manager)
        final String expects = httpServletRequest.getHeader(WebClusterManager.NCM_EXPECTS_HTTP_HEADER);
        if (expects != null) {
            return generateContinueResponse().build();
        }

        // handle an invalidate request from the NCM
        final String invalidateRequest = httpServletRequest
                .getHeader(WebClusterManager.CLUSTER_INVALIDATE_USER_HEADER);
        if (invalidateRequest != null) {
            serviceFacade.invalidateUser(id);
            return generateOkResponse().build();
        }

        // ungroup the specified user
        serviceFacade.deleteUser(id);

        // create the revision
        final RevisionDTO revision = new RevisionDTO();
        revision.setClientId(clientId.getClientId());

        // create the response entity
        final UserEntity entity = new UserEntity();
        entity.setRevision(revision);

        // generate ok response
        return generateOkResponse(entity).build();
    }

    /* setters */
    public void setServiceFacade(NiFiServiceFacade serviceFacade) {
        this.serviceFacade = serviceFacade;
    }

    public void setProperties(NiFiProperties properties) {
        this.properties = properties;
    }

    public void setClusterManager(WebClusterManager clusterManager) {
        this.clusterManager = clusterManager;
    }
}