You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nifi.apache.org by kd...@apache.org on 2018/09/22 02:11:16 UTC
[17/51] [partial] nifi-registry git commit: NIFIREG-201 Refactoring
project structure to better isolate extensions
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AccessResource.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AccessResource.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AccessResource.java
new file mode 100644
index 0000000..d310d0c
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AccessResource.java
@@ -0,0 +1,508 @@
+/*
+ * 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.registry.web.api;
+
+import io.jsonwebtoken.JwtException;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiResponse;
+import io.swagger.annotations.ApiResponses;
+import io.swagger.annotations.Authorization;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.authorization.CurrentUser;
+import org.apache.nifi.registry.event.EventService;
+import org.apache.nifi.registry.exception.AdministrationException;
+import org.apache.nifi.registry.properties.NiFiRegistryProperties;
+import org.apache.nifi.registry.security.authentication.AuthenticationRequest;
+import org.apache.nifi.registry.security.authentication.AuthenticationResponse;
+import org.apache.nifi.registry.security.authentication.BasicAuthIdentityProvider;
+import org.apache.nifi.registry.security.authentication.IdentityProvider;
+import org.apache.nifi.registry.security.authentication.IdentityProviderUsage;
+import org.apache.nifi.registry.security.authentication.exception.IdentityAccessException;
+import org.apache.nifi.registry.security.authentication.exception.InvalidCredentialsException;
+import org.apache.nifi.registry.security.authorization.user.NiFiUser;
+import org.apache.nifi.registry.security.authorization.user.NiFiUserUtils;
+import org.apache.nifi.registry.service.AuthorizationService;
+import org.apache.nifi.registry.web.exception.UnauthorizedException;
+import org.apache.nifi.registry.web.security.authentication.jwt.JwtService;
+import org.apache.nifi.registry.web.security.authentication.kerberos.KerberosSpnegoIdentityProvider;
+import org.apache.nifi.registry.web.security.authentication.x509.X509IdentityProvider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.lang.Nullable;
+import org.springframework.stereotype.Component;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.WebApplicationException;
+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.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+@Component
+@Path("/access")
+@Api(
+ value = "access",
+ description = "Endpoints for obtaining an access token or checking access status."
+)
+public class AccessResource extends ApplicationResource {
+
+ private static final Logger logger = LoggerFactory.getLogger(AccessResource.class);
+
+ private NiFiRegistryProperties properties;
+ private AuthorizationService authorizationService;
+ private JwtService jwtService;
+ private X509IdentityProvider x509IdentityProvider;
+ private KerberosSpnegoIdentityProvider kerberosSpnegoIdentityProvider;
+ private IdentityProvider identityProvider;
+
+ @Autowired
+ public AccessResource(
+ NiFiRegistryProperties properties,
+ AuthorizationService authorizationService,
+ JwtService jwtService,
+ X509IdentityProvider x509IdentityProvider,
+ @Nullable KerberosSpnegoIdentityProvider kerberosSpnegoIdentityProvider,
+ @Nullable IdentityProvider identityProvider,
+ EventService eventService) {
+ super(eventService);
+ this.properties = properties;
+ this.jwtService = jwtService;
+ this.x509IdentityProvider = x509IdentityProvider;
+ this.kerberosSpnegoIdentityProvider = kerberosSpnegoIdentityProvider;
+ this.identityProvider = identityProvider;
+ this.authorizationService = authorizationService;
+ }
+
+ /**
+ * Gets the current client's identity and authorized permissions.
+ *
+ * @param httpServletRequest the servlet request
+ * @return An object describing the current client identity, as determined by the server, and it's permissions.
+ */
+ @GET
+ @Consumes(MediaType.WILDCARD)
+ @Produces(MediaType.APPLICATION_JSON)
+ @ApiOperation(
+ value = "Returns the current client's authenticated identity and permissions to top-level resources",
+ response = CurrentUser.class,
+ authorizations = {@Authorization(value = "Authorization")}
+ )
+ @ApiResponses({
+ @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409 + " The NiFi Registry might be running unsecured.") })
+ public Response getAccessStatus(@Context HttpServletRequest httpServletRequest) {
+
+ final NiFiUser user = NiFiUserUtils.getNiFiUser();
+ if (user == null) {
+ // Not expected to happen unless the nifi registry server has been seriously misconfigured.
+ throw new WebApplicationException(new Throwable("Unable to access details for current user."));
+ }
+
+ final CurrentUser currentUser = authorizationService.getCurrentUser();
+
+ return generateOkResponse(currentUser).build();
+ }
+
+
+ /**
+ * Creates a token for accessing the REST API.
+ *
+ * @param httpServletRequest the servlet request
+ * @return A JWT (string)
+ */
+ @POST
+ @Consumes(MediaType.WILDCARD)
+ @Produces(MediaType.TEXT_PLAIN)
+ @Path("/token")
+ @ApiOperation(
+ value = "Creates a token for accessing the REST API via auto-detected method of verifying client identity claim credentials",
+ notes = "The token returned is formatted as a JSON Web Token (JWT). The token is base64 encoded and comprised of three parts. The header, " +
+ "the body, and the signature. The expiration of the token is a contained within the body. The token can be used in the Authorization header " +
+ "in the format 'Authorization: Bearer <token>'.",
+ response = String.class
+ )
+ @ApiResponses({
+ @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400),
+ @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+ @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409 + " The NiFi Registry may not be configured to support login with username/password."),
+ @ApiResponse(code = 500, message = HttpStatusMessages.MESSAGE_500) })
+ public Response createAccessTokenByTryingAllProviders(@Context HttpServletRequest httpServletRequest) {
+
+ // only support access tokens when communicating over HTTPS
+ if (!httpServletRequest.isSecure()) {
+ throw new IllegalStateException("Access tokens are only issued over HTTPS");
+ }
+
+ List<IdentityProvider> identityProviderWaterfall = generateIdentityProviderWaterfall();
+
+ String token = null;
+ for (IdentityProvider provider : identityProviderWaterfall) {
+
+ AuthenticationRequest authenticationRequest = identityProvider.extractCredentials(httpServletRequest);
+ if (authenticationRequest == null) {
+ continue;
+ }
+ try {
+ token = createAccessToken(identityProvider, authenticationRequest);
+ break;
+ } catch (final InvalidCredentialsException ice){
+ logger.debug("{}: the supplied client credentials are invalid.", identityProvider.getClass().getSimpleName());
+ logger.debug("", ice);
+ }
+
+ }
+
+ if (StringUtils.isEmpty(token)) {
+ List<IdentityProviderUsage.AuthType> acceptableAuthTypes = identityProviderWaterfall.stream()
+ .map(IdentityProvider::getUsageInstructions)
+ .map(IdentityProviderUsage::getAuthType)
+ .filter(Objects::nonNull)
+ .distinct()
+ .collect(Collectors.toList());
+
+ throw new UnauthorizedException("Client credentials are missing or invalid according to all configured identity providers.")
+ .withAuthenticateChallenge(acceptableAuthTypes);
+ }
+
+ // build the response
+ final URI uri = URI.create(generateResourceUri("access", "token"));
+ return generateCreatedResponse(uri, token).build();
+ }
+
+ /**
+ * Creates a token for accessing the REST API.
+ *
+ * @param httpServletRequest the servlet request
+ * @return A JWT (string)
+ */
+ @POST
+ @Consumes(MediaType.WILDCARD)
+ @Produces(MediaType.TEXT_PLAIN)
+ @Path("/token/login")
+ @ApiOperation(
+ value = "Creates a token for accessing the REST API via username/password",
+ notes = "The user credentials must be passed in standard HTTP Basic Auth format. " +
+ "That is: 'Authorization: Basic <credentials>', where <credentials> is the base64 encoded value of '<username>:<password>'. " +
+ "The token returned is formatted as a JSON Web Token (JWT). The token is base64 encoded and comprised of three parts. The header, " +
+ "the body, and the signature. The expiration of the token is a contained within the body. The token can be used in the Authorization header " +
+ "in the format 'Authorization: Bearer <token>'.",
+ response = String.class,
+ authorizations = { @Authorization("BasicAuth") }
+ )
+ @ApiResponses({
+ @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400),
+ @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+ @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409 + " The NiFi Registry may not be configured to support login with username/password."),
+ @ApiResponse(code = 500, message = HttpStatusMessages.MESSAGE_500) })
+ public Response createAccessTokenUsingBasicAuthCredentials(@Context HttpServletRequest httpServletRequest) {
+
+ // only support access tokens when communicating over HTTPS
+ if (!httpServletRequest.isSecure()) {
+ throw new IllegalStateException("Access tokens are only issued over HTTPS");
+ }
+
+ // if not configured with custom identity provider, or if provider doesn't support HTTP Basic Auth, don't consider credentials
+ if (identityProvider == null) {
+ logger.debug("An Identity Provider must be configured to use this endpoint. Please consult the administration guide.");
+ throw new IllegalStateException("Username/Password login not supported by this NiFi. Contact System Administrator.");
+ }
+ if (!(identityProvider instanceof BasicAuthIdentityProvider)) {
+ logger.debug("An Identity Provider is configured, but it does not support HTTP Basic Auth authentication. " +
+ "The configured Identity Provider must extend {}", BasicAuthIdentityProvider.class);
+ throw new IllegalStateException("Username/Password login not supported by this NiFi. Contact System Administrator.");
+ }
+
+ // generate JWT for response
+ AuthenticationRequest authenticationRequest = identityProvider.extractCredentials(httpServletRequest);
+
+ if (authenticationRequest == null) {
+ throw new UnauthorizedException("The client credentials are missing from the request.")
+ .withAuthenticateChallenge(IdentityProviderUsage.AuthType.OTHER);
+ }
+
+ final String token;
+ try {
+ token = createAccessToken(identityProvider, authenticationRequest);
+ } catch (final InvalidCredentialsException ice){
+ throw new UnauthorizedException("The supplied client credentials are not valid.", ice)
+ .withAuthenticateChallenge(IdentityProviderUsage.AuthType.OTHER);
+ }
+
+ // form the response
+ final URI uri = URI.create(generateResourceUri("access", "token"));
+ return generateCreatedResponse(uri, token).build();
+ }
+
+ @POST
+ @Consumes(MediaType.WILDCARD)
+ @Produces(MediaType.TEXT_PLAIN)
+ @Path("/token/kerberos")
+ @ApiOperation(
+ value = "Creates a token for accessing the REST API via Kerberos Service Tickets or SPNEGO Tokens (which includes Kerberos Service Tickets)",
+ notes = "The token returned is formatted as a JSON Web Token (JWT). The token is base64 encoded and comprised of three parts. The header, " +
+ "the body, and the signature. The expiration of the token is a contained within the body. The token can be used in the Authorization header " +
+ "in the format 'Authorization: Bearer <token>'.",
+ response = String.class
+ )
+ @ApiResponses({
+ @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400),
+ @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+ @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409 + " The NiFi Registry may not be configured to support login Kerberos credentials."),
+ @ApiResponse(code = 500, message = HttpStatusMessages.MESSAGE_500) })
+ public Response createAccessTokenUsingKerberosTicket(@Context HttpServletRequest httpServletRequest) {
+
+ // only support access tokens when communicating over HTTPS
+ if (!httpServletRequest.isSecure()) {
+ throw new IllegalStateException("Access tokens are only issued over HTTPS");
+ }
+
+ // if not configured with custom identity provider, don't consider credentials
+ if (!properties.isKerberosSpnegoSupportEnabled() || kerberosSpnegoIdentityProvider == null) {
+ throw new IllegalStateException("Kerberos service ticket login not supported by this NiFi Registry");
+ }
+
+ AuthenticationRequest authenticationRequest = kerberosSpnegoIdentityProvider.extractCredentials(httpServletRequest);
+
+ if (authenticationRequest == null) {
+ throw new UnauthorizedException("The client credentials are missing from the request.")
+ .withAuthenticateChallenge(kerberosSpnegoIdentityProvider.getUsageInstructions().getAuthType());
+ }
+
+ final String token;
+ try {
+ token = createAccessToken(kerberosSpnegoIdentityProvider, authenticationRequest);
+ } catch (final InvalidCredentialsException ice){
+ throw new UnauthorizedException("The supplied client credentials are not valid.", ice)
+ .withAuthenticateChallenge(kerberosSpnegoIdentityProvider.getUsageInstructions().getAuthType());
+ }
+
+ // build the response
+ final URI uri = URI.create(generateResourceUri("access", "token"));
+ return generateCreatedResponse(uri, token).build();
+
+ }
+
+ /**
+ * Creates a token for accessing the REST API using a custom identity provider configured using NiFi Registry extensions.
+ *
+ * @param httpServletRequest the servlet request
+ * @return A JWT (string)
+ */
+ @POST
+ @Consumes(MediaType.WILDCARD)
+ @Produces(MediaType.TEXT_PLAIN)
+ @Path("/token/identity-provider")
+ @ApiOperation(
+ value = "Creates a token for accessing the REST API via a custom identity provider.",
+ notes = "The user credentials must be passed in a format understood by the custom identity provider, e.g., a third-party auth token in an HTTP header. " +
+ "The exact format of the user credentials expected by the custom identity provider can be discovered by 'GET /access/token/identity-provider/usage'. " +
+ "The token returned is formatted as a JSON Web Token (JWT). The token is base64 encoded and comprised of three parts. The header, " +
+ "the body, and the signature. The expiration of the token is a contained within the body. The token can be used in the Authorization header " +
+ "in the format 'Authorization: Bearer <token>'.",
+ response = String.class
+ )
+ @ApiResponses({
+ @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400),
+ @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+ @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409 + " The NiFi Registry may not be configured to support login with customized credentials."),
+ @ApiResponse(code = 500, message = HttpStatusMessages.MESSAGE_500) })
+ public Response createAccessTokenUsingIdentityProviderCredentials(@Context HttpServletRequest httpServletRequest) {
+
+ // only support access tokens when communicating over HTTPS
+ if (!httpServletRequest.isSecure()) {
+ throw new IllegalStateException("Access tokens are only issued over HTTPS");
+ }
+
+ // if not configured with custom identity provider, don't consider credentials
+ if (identityProvider == null) {
+ throw new IllegalStateException("Custom login not supported by this NiFi Registry");
+ }
+
+ AuthenticationRequest authenticationRequest = identityProvider.extractCredentials(httpServletRequest);
+
+ if (authenticationRequest == null) {
+ throw new UnauthorizedException("The client credentials are missing from the request.")
+ .withAuthenticateChallenge(identityProvider.getUsageInstructions().getAuthType());
+ }
+
+ final String token;
+ try {
+ token = createAccessToken(identityProvider, authenticationRequest);
+ } catch (InvalidCredentialsException ice) {
+ throw new UnauthorizedException("The supplied client credentials are not valid.", ice)
+ .withAuthenticateChallenge(identityProvider.getUsageInstructions().getAuthType());
+ }
+
+ // build the response
+ final URI uri = URI.create(generateResourceUri("access", "token"));
+ return generateCreatedResponse(uri, token).build();
+
+ }
+
+ /**
+ * Creates a token for accessing the REST API using a custom identity provider configured using NiFi Registry extensions.
+ *
+ * @param httpServletRequest the servlet request
+ * @return A JWT (string)
+ */
+ @GET
+ @Consumes(MediaType.WILDCARD)
+ @Produces(MediaType.TEXT_PLAIN)
+ @Path("/token/identity-provider/usage")
+ @ApiOperation(
+ value = "Provides a description of how the currently configured identity provider expects credentials to be passed to POST /access/token/identity-provider",
+ response = String.class
+ )
+ @ApiResponses({
+ @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400),
+ @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409 + " The NiFi Registry may not be configured to support login with customized credentials."),
+ @ApiResponse(code = 500, message = HttpStatusMessages.MESSAGE_500) })
+ public Response getIdentityProviderUsageInstructions(@Context HttpServletRequest httpServletRequest) {
+
+ // if not configuration for login, don't consider credentials
+ if (identityProvider == null) {
+ throw new IllegalStateException("Custom login not supported by this NiFi Registry");
+ }
+
+ Class ipClazz = identityProvider.getClass();
+ String identityProviderName = StringUtils.isNotEmpty(ipClazz.getSimpleName()) ? ipClazz.getSimpleName() : ipClazz.getName();
+
+ try {
+ String usageInstructions = "Usage Instructions for '" + identityProviderName + "': ";
+ usageInstructions += identityProvider.getUsageInstructions().getText();
+ return generateOkResponse(usageInstructions).build();
+
+ } catch (Exception e) {
+ // If, for any reason, this identity provider does not support getUsageInstructions(), e.g., returns null or throws NotImplementedException.
+ return Response.status(Response.Status.NOT_IMPLEMENTED)
+ .entity("The currently configured identity provider, '" + identityProvider.getClass().getName() + "' does not provide usage instructions.")
+ .build();
+ }
+
+ }
+
+ /**
+ * Creates a token for accessing the REST API using a custom identity provider configured using NiFi Registry extensions.
+ *
+ * @param httpServletRequest the servlet request
+ * @return A JWT (string)
+ */
+ @POST
+ @Consumes(MediaType.WILDCARD)
+ @Produces(MediaType.TEXT_PLAIN)
+ @Path("/token/identity-provider/test")
+ @ApiOperation(
+ value = "Tests the format of the credentials against this identity provider without preforming authentication on the credentials to validate them.",
+ notes = "The user credentials should be passed in a format understood by the custom identity provider as defined by 'GET /access/token/identity-provider/usage'.",
+ response = String.class
+ )
+ @ApiResponses({
+ @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400),
+ @ApiResponse(code = 401, message = "The format of the credentials were not recognized by the currently configured identity provider."),
+ @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409 + " The NiFi Registry may not be configured to support login with customized credentials."),
+ @ApiResponse(code = 500, message = HttpStatusMessages.MESSAGE_500) })
+ public Response testIdentityProviderRecognizesCredentialsFormat(@Context HttpServletRequest httpServletRequest) {
+
+ // only support access tokens when communicating over HTTPS
+ if (!httpServletRequest.isSecure()) {
+ throw new IllegalStateException("Access tokens are only issued over HTTPS");
+ }
+
+ // if not configured with custom identity provider, don't consider credentials
+ if (identityProvider == null) {
+ throw new IllegalStateException("Custom login not supported by this NiFi Registry");
+ }
+
+ final Class ipClazz = identityProvider.getClass();
+ final String identityProviderName = StringUtils.isNotEmpty(ipClazz.getSimpleName()) ? ipClazz.getSimpleName() : ipClazz.getName();
+
+ // attempt to extract client credentials without authenticating them
+ AuthenticationRequest authenticationRequest = identityProvider.extractCredentials(httpServletRequest);
+
+ if (authenticationRequest == null) {
+ throw new UnauthorizedException("The format of the credentials were not recognized by the currently configured identity provider " +
+ "'" + identityProviderName + "'. " + identityProvider.getUsageInstructions().getText())
+ .withAuthenticateChallenge(identityProvider.getUsageInstructions().getAuthType());
+ }
+
+
+ final String successMessage = identityProviderName + " recognized the format of the credentials in the HTTP request.";
+ return generateOkResponse(successMessage).build();
+
+ }
+
+ private String createAccessToken(IdentityProvider identityProvider, AuthenticationRequest authenticationRequest)
+ throws InvalidCredentialsException, AdministrationException {
+
+ final AuthenticationResponse authenticationResponse;
+
+ try {
+ authenticationResponse = identityProvider.authenticate(authenticationRequest);
+ final String token = jwtService.generateSignedToken(authenticationResponse);
+ return token;
+ } catch (final IdentityAccessException | JwtException e) {
+ throw new AdministrationException(e.getMessage());
+ }
+
+ }
+
+ /**
+ * A helper function that generates a prioritized list of IdentityProviders to use to
+ * attempt client authentication.
+ *
+ * Note: This is currently a hard-coded list order consisting of:
+ *
+ * - X509IdentityProvider (if available)
+ * - KerberosProvider (if available)
+ * - User-defined IdentityProvider (if available)
+ *
+ * However, in the future it could be entirely user-configurable
+ *
+ * @return a list of providers to use in order to authenticate the client.
+ */
+ private List<IdentityProvider> generateIdentityProviderWaterfall() {
+ List<IdentityProvider> identityProviderWaterfall = new ArrayList<>();
+
+ // if configured with an X509IdentityProvider, add it to the list of providers to try
+ if (x509IdentityProvider != null) {
+ identityProviderWaterfall.add(x509IdentityProvider);
+ }
+
+ // if configured with an KerberosSpnegoIdentityProvider, add it to the end of the list of providers to try
+ if (kerberosSpnegoIdentityProvider != null) {
+ identityProviderWaterfall.add(kerberosSpnegoIdentityProvider);
+ }
+
+ // if configured with custom identity provider, add it to the end of the list of providers to try
+ if (identityProvider != null) {
+ identityProviderWaterfall.add(identityProvider);
+ }
+
+ return identityProviderWaterfall;
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ApplicationResource.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ApplicationResource.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ApplicationResource.java
new file mode 100644
index 0000000..776a693
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ApplicationResource.java
@@ -0,0 +1,194 @@
+/*
+ * 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.registry.web.api;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.Validate;
+import org.apache.nifi.registry.event.EventService;
+import org.apache.nifi.registry.hook.Event;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.core.CacheControl;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriBuilderException;
+import javax.ws.rs.core.UriInfo;
+import java.net.URI;
+import java.net.URISyntaxException;
+
+public class ApplicationResource {
+
+ public static final String PROXY_SCHEME_HTTP_HEADER = "X-ProxyScheme";
+ public static final String PROXY_HOST_HTTP_HEADER = "X-ProxyHost";
+ public static final String PROXY_PORT_HTTP_HEADER = "X-ProxyPort";
+ public static final String PROXY_CONTEXT_PATH_HTTP_HEADER = "X-ProxyContextPath";
+
+ public static final String FORWARDED_PROTO_HTTP_HEADER = "X-Forwarded-Proto";
+ public static final String FORWARDED_HOST_HTTP_HEADER = "X-Forwarded-Server";
+ public static final String FORWARDED_PORT_HTTP_HEADER = "X-Forwarded-Port";
+ public static final String FORWARDED_CONTEXT_HTTP_HEADER = "X-Forwarded-Context";
+
+ protected static final String NON_GUARANTEED_ENDPOINT = "Note: This endpoint is subject to change as NiFi Registry and its REST API evolve.";
+
+ private static final Logger logger = LoggerFactory.getLogger(ApplicationResource.class);
+
+ @Context
+ private HttpServletRequest httpServletRequest;
+
+ @Context
+ private UriInfo uriInfo;
+
+ private final EventService eventService;
+
+ public ApplicationResource(final EventService eventService) {
+ this.eventService = eventService;
+ Validate.notNull(this.eventService);
+ }
+
+ // We don't want an error creating/publishing an event to cause the overall request to fail, so catch all throwables here
+ protected void publish(final Event event) {
+ try {
+ eventService.publish(event);
+ } catch (Throwable t) {
+ logger.error("Unable to publish event: " + t.getMessage(), t);
+ }
+ }
+
+ protected String generateResourceUri(final String... path) {
+ final UriBuilder uriBuilder = uriInfo.getBaseUriBuilder();
+ uriBuilder.segment(path);
+ URI uri = uriBuilder.build();
+ try {
+
+ // check for proxy settings
+ final String scheme = getFirstHeaderValue(PROXY_SCHEME_HTTP_HEADER, FORWARDED_PROTO_HTTP_HEADER);
+ final String host = getFirstHeaderValue(PROXY_HOST_HTTP_HEADER, FORWARDED_HOST_HTTP_HEADER);
+ final String port = getFirstHeaderValue(PROXY_PORT_HTTP_HEADER, FORWARDED_PORT_HTTP_HEADER);
+ String baseContextPath = getFirstHeaderValue(PROXY_CONTEXT_PATH_HTTP_HEADER, FORWARDED_CONTEXT_HTTP_HEADER);
+
+ // if necessary, prepend the context path
+ String resourcePath = uri.getPath();
+ if (baseContextPath != null) {
+ // normalize context path
+ if (!baseContextPath.startsWith("/")) {
+ baseContextPath = "/" + baseContextPath;
+ }
+
+ if (baseContextPath.endsWith("/")) {
+ baseContextPath = StringUtils.substringBeforeLast(baseContextPath, "/");
+ }
+
+ // determine the complete resource path
+ resourcePath = baseContextPath + resourcePath;
+ }
+
+ // determine the port uri
+ int uriPort = uri.getPort();
+ if (port != null) {
+ if (StringUtils.isWhitespace(port)) {
+ uriPort = -1;
+ } else {
+ try {
+ uriPort = Integer.parseInt(port);
+ } catch (final NumberFormatException nfe) {
+ logger.warn(String.format("Unable to parse proxy port HTTP header '%s'. Using port from request URI '%s'.", port, uriPort));
+ }
+ }
+ }
+
+ // construct the URI
+ uri = new URI(
+ (StringUtils.isBlank(scheme)) ? uri.getScheme() : scheme,
+ uri.getUserInfo(),
+ (StringUtils.isBlank(host)) ? uri.getHost() : host,
+ uriPort,
+ resourcePath,
+ uri.getQuery(),
+ uri.getFragment());
+
+ } catch (final URISyntaxException use) {
+ throw new UriBuilderException(use);
+ }
+ return uri.toString();
+ }
+
+ /**
+ * Edit the response headers to indicating no caching.
+ *
+ * @param response response
+ * @return builder
+ */
+ protected Response.ResponseBuilder noCache(final Response.ResponseBuilder response) {
+ final CacheControl cacheControl = new CacheControl();
+ cacheControl.setPrivate(true);
+ cacheControl.setNoCache(true);
+ cacheControl.setNoStore(true);
+ return response.cacheControl(cacheControl);
+ }
+
+ /**
+ * Generates an OK response with the specified content.
+ *
+ * @param entity The entity
+ * @return The response to be built
+ */
+ protected Response.ResponseBuilder generateOkResponse(final Object entity) {
+ final Response.ResponseBuilder response = Response.ok(entity);
+ return noCache(response);
+ }
+
+ /**
+ * Generates a 201 Created response with the specified content.
+ *
+ * @param uri The URI
+ * @param entity entity
+ * @return The response to be built
+ */
+ protected Response.ResponseBuilder generateCreatedResponse(final URI uri, final Object entity) {
+ // generate the response builder
+ return Response.created(uri).entity(entity);
+ }
+
+ /**
+ * Returns the value for the first key discovered when inspecting the current request. Will
+ * return null if there are no keys specified or if none of the specified keys are found.
+ *
+ * @param keys http header keys
+ * @return the value for the first key found
+ */
+ private String getFirstHeaderValue(final String... keys) {
+ if (keys == null) {
+ return null;
+ }
+
+ for (final String key : keys) {
+ final String value = httpServletRequest.getHeader(key);
+
+ // if we found an entry for this key, return the value
+ if (value != null) {
+ return value;
+ }
+ }
+
+ // unable to find any matching keys
+ return null;
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AuthorizableApplicationResource.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AuthorizableApplicationResource.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AuthorizableApplicationResource.java
new file mode 100644
index 0000000..83240c7
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AuthorizableApplicationResource.java
@@ -0,0 +1,81 @@
+/*
+ * 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.registry.web.api;
+
+import org.apache.nifi.registry.authorization.Resource;
+import org.apache.nifi.registry.bucket.BucketItem;
+import org.apache.nifi.registry.event.EventService;
+import org.apache.nifi.registry.security.authorization.AuthorizableLookup;
+import org.apache.nifi.registry.security.authorization.RequestAction;
+import org.apache.nifi.registry.security.authorization.resource.Authorizable;
+import org.apache.nifi.registry.security.authorization.resource.ResourceType;
+import org.apache.nifi.registry.service.AuthorizationService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public class AuthorizableApplicationResource extends ApplicationResource {
+
+ private static final Logger logger = LoggerFactory.getLogger(AuthorizableApplicationResource.class);
+
+ protected final AuthorizationService authorizationService;
+ protected final AuthorizableLookup authorizableLookup;
+
+ protected AuthorizableApplicationResource(
+ AuthorizationService authorizationService,
+ EventService eventService) {
+ super(eventService);
+ this.authorizationService = authorizationService;
+ this.authorizableLookup = authorizationService.getAuthorizableLookup();
+ }
+
+ protected void authorizeBucketAccess(RequestAction actionType, String bucketIdentifier) {
+ final Authorizable bucketAuthorizable = authorizableLookup.getBucketAuthorizable(bucketIdentifier);
+ authorizationService.authorize(bucketAuthorizable, actionType);
+ }
+
+ protected void authorizeBucketItemAccess(RequestAction actionType, BucketItem bucketItem) {
+ authorizeBucketAccess(actionType, bucketItem.getBucketIdentifier());
+ }
+
+ protected Set<String> getAuthorizedBucketIds(RequestAction actionType) {
+ return authorizationService
+ .getAuthorizedResources(actionType, ResourceType.Bucket)
+ .stream()
+ .map(AuthorizableApplicationResource::extractBucketIdFromResource)
+ .filter(Objects::nonNull)
+ .distinct()
+ .collect(Collectors.toSet());
+ }
+
+ private static String extractBucketIdFromResource(Resource resource) {
+
+ if (resource == null || resource.getIdentifier() == null || !resource.getIdentifier().startsWith("/buckets/")) {
+ return null;
+ }
+
+ String[] pathComponents = resource.getIdentifier().split("/");
+ if (pathComponents.length < 3) {
+ return null;
+ }
+ return pathComponents[2];
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketFlowResource.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketFlowResource.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketFlowResource.java
new file mode 100644
index 0000000..942a3d4
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketFlowResource.java
@@ -0,0 +1,600 @@
+/*
+ * 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.registry.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 io.swagger.annotations.Extension;
+import io.swagger.annotations.ExtensionProperty;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.bucket.BucketItem;
+import org.apache.nifi.registry.diff.VersionedFlowDifference;
+import org.apache.nifi.registry.event.EventFactory;
+import org.apache.nifi.registry.event.EventService;
+import org.apache.nifi.registry.flow.VersionedFlow;
+import org.apache.nifi.registry.flow.VersionedFlowSnapshot;
+import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata;
+import org.apache.nifi.registry.security.authorization.RequestAction;
+import org.apache.nifi.registry.security.authorization.user.NiFiUserUtils;
+import org.apache.nifi.registry.service.AuthorizationService;
+import org.apache.nifi.registry.service.RegistryService;
+import org.apache.nifi.registry.web.link.LinkService;
+import org.apache.nifi.registry.web.security.PermissionsService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import javax.validation.constraints.NotNull;
+import javax.ws.rs.BadRequestException;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+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 javax.ws.rs.core.Response;
+import java.util.List;
+import java.util.SortedSet;
+
+@Component
+@Path("/buckets/{bucketId}/flows")
+@Api(
+ value = "bucket_flows",
+ description = "Create flows scoped to an existing bucket in the registry.",
+ authorizations = { @Authorization("Authorization") }
+)
+public class BucketFlowResource extends AuthorizableApplicationResource {
+
+ private static final Logger logger = LoggerFactory.getLogger(BucketFlowResource.class);
+
+ private final RegistryService registryService;
+ private final LinkService linkService;
+ private final PermissionsService permissionsService;
+
+ @Autowired
+ public BucketFlowResource(
+ final RegistryService registryService,
+ final LinkService linkService,
+ final PermissionsService permissionsService,
+ final AuthorizationService authorizationService,
+ final EventService eventService) {
+ super(authorizationService, eventService);
+ this.registryService = registryService;
+ this.linkService = linkService;
+ this.permissionsService =permissionsService;
+ }
+
+ @POST
+ @Consumes(MediaType.APPLICATION_JSON)
+ @Produces(MediaType.APPLICATION_JSON)
+ @ApiOperation(
+ value = "Creates a flow",
+ notes = "The flow id is created by the server and populated in the returned entity.",
+ response = VersionedFlow.class,
+ extensions = {
+ @Extension(name = "access-policy", properties = {
+ @ExtensionProperty(name = "action", value = "write"),
+ @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") })
+ }
+ )
+ @ApiResponses({
+ @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400),
+ @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+ @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403),
+ @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404),
+ @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) })
+ public Response createFlow(
+ @PathParam("bucketId")
+ @ApiParam("The bucket identifier")
+ final String bucketId,
+ @ApiParam(value = "The details of the flow to create.", required = true)
+ final VersionedFlow flow) {
+
+ authorizeBucketAccess(RequestAction.WRITE, bucketId);
+ verifyPathParamsMatchBody(bucketId, flow);
+
+ final VersionedFlow createdFlow = registryService.createFlow(bucketId, flow);
+ publish(EventFactory.flowCreated(createdFlow));
+
+ permissionsService.populateItemPermissions(createdFlow);
+ linkService.populateFlowLinks(createdFlow);
+ return Response.status(Response.Status.OK).entity(createdFlow).build();
+ }
+
+ @GET
+ @Consumes(MediaType.WILDCARD)
+ @Produces(MediaType.APPLICATION_JSON)
+ @ApiOperation(
+ value = "Gets all flows in the given bucket",
+ response = VersionedFlow.class,
+ responseContainer = "List",
+ extensions = {
+ @Extension(name = "access-policy", properties = {
+ @ExtensionProperty(name = "action", value = "read"),
+ @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") })
+ }
+ )
+ @ApiResponses({
+ @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400),
+ @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+ @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403),
+ @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404),
+ @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) })
+ public Response getFlows(
+ @PathParam("bucketId")
+ @ApiParam("The bucket identifier")
+ final String bucketId) {
+
+ authorizeBucketAccess(RequestAction.READ, bucketId);
+
+ final List<VersionedFlow> flows = registryService.getFlows(bucketId);
+ permissionsService.populateItemPermissions(flows);
+ linkService.populateFlowLinks(flows);
+
+ return Response.status(Response.Status.OK).entity(flows).build();
+ }
+
+ @GET
+ @Path("{flowId}")
+ @Consumes(MediaType.WILDCARD)
+ @Produces(MediaType.APPLICATION_JSON)
+ @ApiOperation(
+ value = "Gets a flow",
+ response = VersionedFlow.class,
+ extensions = {
+ @Extension(name = "access-policy", properties = {
+ @ExtensionProperty(name = "action", value = "read"),
+ @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") })
+ }
+ )
+ @ApiResponses({
+ @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400),
+ @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+ @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403),
+ @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404),
+ @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) })
+ public Response getFlow(
+ @PathParam("bucketId")
+ @ApiParam("The bucket identifier")
+ final String bucketId,
+ @PathParam("flowId")
+ @ApiParam("The flow identifier")
+ final String flowId) {
+
+ authorizeBucketAccess(RequestAction.READ, bucketId);
+
+ final VersionedFlow flow = registryService.getFlow(bucketId, flowId);
+ permissionsService.populateItemPermissions(flow);
+ linkService.populateFlowLinks(flow);
+
+ return Response.status(Response.Status.OK).entity(flow).build();
+ }
+
+ @PUT
+ @Path("{flowId}")
+ @Consumes(MediaType.APPLICATION_JSON)
+ @Produces(MediaType.APPLICATION_JSON)
+ @ApiOperation(
+ value = "Updates a flow",
+ response = VersionedFlow.class,
+ extensions = {
+ @Extension(name = "access-policy", properties = {
+ @ExtensionProperty(name = "action", value = "write"),
+ @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") })
+ }
+ )
+ @ApiResponses({
+ @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400),
+ @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+ @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403),
+ @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404),
+ @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) })
+ public Response updateFlow(
+ @PathParam("bucketId")
+ @ApiParam("The bucket identifier")
+ final String bucketId,
+ @PathParam("flowId")
+ @ApiParam("The flow identifier")
+ final String flowId,
+ @ApiParam(value = "The updated flow", required = true)
+ final VersionedFlow flow) {
+
+ verifyPathParamsMatchBody(bucketId, flowId, flow);
+ authorizeBucketAccess(RequestAction.WRITE, bucketId);
+
+ // bucketId and flowId fields are optional in the body parameter, but required before calling the service layer
+ setBucketItemMetadataIfMissing(bucketId, flowId, flow);
+
+ final VersionedFlow updatedFlow = registryService.updateFlow(flow);
+ publish(EventFactory.flowUpdated(updatedFlow));
+ permissionsService.populateItemPermissions(updatedFlow);
+ linkService.populateFlowLinks(updatedFlow);
+
+ return Response.status(Response.Status.OK).entity(updatedFlow).build();
+ }
+
+ @DELETE
+ @Path("{flowId}")
+ @Consumes(MediaType.WILDCARD)
+ @Produces(MediaType.APPLICATION_JSON)
+ @ApiOperation(
+ value = "Deletes a flow, including all saved versions of that flow.",
+ response = VersionedFlow.class,
+ extensions = {
+ @Extension(name = "access-policy", properties = {
+ @ExtensionProperty(name = "action", value = "delete"),
+ @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") })
+ }
+ )
+ @ApiResponses({
+ @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+ @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403),
+ @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404),
+ @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) })
+ public Response deleteFlow(
+ @PathParam("bucketId")
+ @ApiParam("The bucket identifier")
+ final String bucketId,
+ @PathParam("flowId")
+ @ApiParam("The flow identifier")
+ final String flowId) {
+
+ authorizeBucketAccess(RequestAction.DELETE, bucketId);
+ final VersionedFlow deletedFlow = registryService.deleteFlow(bucketId, flowId);
+ publish(EventFactory.flowDeleted(deletedFlow));
+ return Response.status(Response.Status.OK).entity(deletedFlow).build();
+ }
+
+ @POST
+ @Path("{flowId}/versions")
+ @Consumes(MediaType.WILDCARD)
+ @Produces(MediaType.APPLICATION_JSON)
+ @ApiOperation(
+ value = "Creates the next version of a flow",
+ notes = "The version number of the object being created must be the next available version integer. " +
+ "Flow versions are immutable after they are created.",
+ response = VersionedFlowSnapshot.class,
+ extensions = {
+ @Extension(name = "access-policy", properties = {
+ @ExtensionProperty(name = "action", value = "write"),
+ @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") })
+ }
+ )
+ @ApiResponses({
+ @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400),
+ @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+ @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403),
+ @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404),
+ @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) })
+ public Response createFlowVersion(
+ @PathParam("bucketId")
+ @ApiParam("The bucket identifier")
+ final String bucketId,
+ @PathParam("flowId")
+ @ApiParam(value = "The flow identifier")
+ final String flowId,
+ @ApiParam(value = "The new versioned flow snapshot.", required = true)
+ final VersionedFlowSnapshot snapshot) {
+
+ verifyPathParamsMatchBody(bucketId, flowId, snapshot);
+ authorizeBucketAccess(RequestAction.WRITE, bucketId);
+
+ // bucketId and flowId fields are optional in the body parameter, but required before calling the service layer
+ setSnaphotMetadataIfMissing(bucketId, flowId, snapshot);
+
+ final String userIdentity = NiFiUserUtils.getNiFiUserIdentity();
+ snapshot.getSnapshotMetadata().setAuthor(userIdentity);
+
+ final VersionedFlowSnapshot createdSnapshot = registryService.createFlowSnapshot(snapshot);
+ publish(EventFactory.flowVersionCreated(createdSnapshot));
+
+ if (createdSnapshot.getSnapshotMetadata() != null) {
+ linkService.populateSnapshotLinks(createdSnapshot.getSnapshotMetadata());
+ }
+ if (createdSnapshot.getBucket() != null) {
+ permissionsService.populateBucketPermissions(createdSnapshot.getBucket());
+ linkService.populateBucketLinks(createdSnapshot.getBucket());
+ }
+ return Response.status(Response.Status.OK).entity(createdSnapshot).build();
+ }
+
+ @GET
+ @Path("{flowId}/versions")
+ @Consumes(MediaType.WILDCARD)
+ @Produces(MediaType.APPLICATION_JSON)
+ @ApiOperation(
+ value = "Gets summary information for all versions of a flow. Versions are ordered newest->oldest.",
+ response = VersionedFlowSnapshotMetadata.class,
+ responseContainer = "List",
+ extensions = {
+ @Extension(name = "access-policy", properties = {
+ @ExtensionProperty(name = "action", value = "read"),
+ @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") })
+ }
+ )
+ @ApiResponses({
+ @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+ @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403),
+ @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404),
+ @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) })
+ public Response getFlowVersions(
+ @PathParam("bucketId")
+ @ApiParam("The bucket identifier")
+ final String bucketId,
+ @PathParam("flowId")
+ @ApiParam("The flow identifier")
+ final String flowId) {
+
+ authorizeBucketAccess(RequestAction.READ, bucketId);
+
+ final SortedSet<VersionedFlowSnapshotMetadata> snapshots = registryService.getFlowSnapshots(bucketId, flowId);
+ if (snapshots != null ) {
+ linkService.populateSnapshotLinks(snapshots);
+ }
+
+ return Response.status(Response.Status.OK).entity(snapshots).build();
+ }
+
+ @GET
+ @Path("{flowId}/versions/latest")
+ @Consumes(MediaType.WILDCARD)
+ @Produces(MediaType.APPLICATION_JSON)
+ @ApiOperation(
+ value = "Get the latest version of a flow",
+ response = VersionedFlowSnapshot.class,
+ extensions = {
+ @Extension(name = "access-policy", properties = {
+ @ExtensionProperty(name = "action", value = "read"),
+ @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") })
+ }
+ )
+ @ApiResponses({
+ @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+ @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403),
+ @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404),
+ @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) })
+ public Response getLatestFlowVersion(
+ @PathParam("bucketId")
+ @ApiParam("The bucket identifier")
+ final String bucketId,
+ @PathParam("flowId")
+ @ApiParam("The flow identifier")
+ final String flowId) {
+
+ authorizeBucketAccess(RequestAction.READ, bucketId);
+
+ final VersionedFlowSnapshotMetadata latestMetadata = registryService.getLatestFlowSnapshotMetadata(bucketId, flowId);
+ final VersionedFlowSnapshot lastSnapshot = registryService.getFlowSnapshot(bucketId, flowId, latestMetadata.getVersion());
+ populateLinksAndPermissions(lastSnapshot);
+
+ return Response.status(Response.Status.OK).entity(lastSnapshot).build();
+ }
+
+ @GET
+ @Path("{flowId}/versions/latest/metadata")
+ @Consumes(MediaType.WILDCARD)
+ @Produces(MediaType.APPLICATION_JSON)
+ @ApiOperation(
+ value = "Get the metadata for the latest version of a flow",
+ response = VersionedFlowSnapshotMetadata.class,
+ extensions = {
+ @Extension(name = "access-policy", properties = {
+ @ExtensionProperty(name = "action", value = "read"),
+ @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") })
+ }
+ )
+ @ApiResponses({
+ @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+ @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403),
+ @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404),
+ @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) })
+ public Response getLatestFlowVersionMetadata(
+ @PathParam("bucketId")
+ @ApiParam("The bucket identifier")
+ final String bucketId,
+ @PathParam("flowId")
+ @ApiParam("The flow identifier")
+ final String flowId) {
+
+ authorizeBucketAccess(RequestAction.READ, bucketId);
+
+ final VersionedFlowSnapshotMetadata latest = registryService.getLatestFlowSnapshotMetadata(bucketId, flowId);
+ linkService.populateSnapshotLinks(latest);
+
+ return Response.status(Response.Status.OK).entity(latest).build();
+ }
+
+ @GET
+ @Path("{flowId}/versions/{versionNumber: \\d+}")
+ @Consumes(MediaType.WILDCARD)
+ @Produces(MediaType.APPLICATION_JSON)
+ @ApiOperation(
+ value = "Gets the given version of a flow",
+ response = VersionedFlowSnapshot.class,
+ extensions = {
+ @Extension(name = "access-policy", properties = {
+ @ExtensionProperty(name = "action", value = "read"),
+ @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") })
+ }
+ )
+ @ApiResponses({
+ @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400),
+ @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+ @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403),
+ @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404),
+ @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) })
+ public Response getFlowVersion(
+ @PathParam("bucketId")
+ @ApiParam("The bucket identifier")
+ final String bucketId,
+ @PathParam("flowId")
+ @ApiParam("The flow identifier")
+ final String flowId,
+ @PathParam("versionNumber")
+ @ApiParam("The version number")
+ final Integer versionNumber) {
+ authorizeBucketAccess(RequestAction.READ, bucketId);
+
+ final VersionedFlowSnapshot snapshot = registryService.getFlowSnapshot(bucketId, flowId, versionNumber);
+ populateLinksAndPermissions(snapshot);
+
+ return Response.status(Response.Status.OK).entity(snapshot).build();
+ }
+
+ @GET
+ @Path("{flowId}/diff/{versionA: \\d+}/{versionB: \\d+}")
+ @Consumes(MediaType.WILDCARD)
+ @Produces(MediaType.APPLICATION_JSON)
+ @ApiOperation(
+ value = "Returns a list of differences between 2 versions of a flow",
+ response = VersionedFlowDifference.class,
+ extensions = {
+ @Extension(name = "access-policy", properties = {
+ @ExtensionProperty(name = "action", value = "read"),
+ @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") })
+ }
+ )
+ @ApiResponses({
+ @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400),
+ @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+ @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403),
+ @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404),
+ @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409)})
+ public Response getFlowDiff(
+ @PathParam("bucketId")
+ @ApiParam("The bucket identifier")
+ final String bucketId,
+ @PathParam("flowId")
+ @ApiParam("The flow identifier")
+ final String flowId,
+ @PathParam("versionA")
+ @ApiParam("The first version number")
+ final Integer versionNumberA,
+ @PathParam("versionB")
+ @ApiParam("The second version number")
+ final Integer versionNumberB) {
+ authorizeBucketAccess(RequestAction.READ, bucketId);
+ VersionedFlowDifference result = registryService.getFlowDiff(bucketId, flowId, versionNumberA, versionNumberB);
+ return Response.status(Response.Status.OK).entity(result).build();
+ }
+
+ private void populateLinksAndPermissions(VersionedFlowSnapshot snapshot) {
+ if (snapshot.getSnapshotMetadata() != null) {
+ linkService.populateSnapshotLinks(snapshot.getSnapshotMetadata());
+ }
+
+ if (snapshot.getFlow() != null) {
+ linkService.populateFlowLinks(snapshot.getFlow());
+ }
+
+ if (snapshot.getBucket() != null) {
+ permissionsService.populateBucketPermissions(snapshot.getBucket());
+ linkService.populateBucketLinks(snapshot.getBucket());
+ }
+
+ }
+
+ private static void verifyPathParamsMatchBody(String bucketIdParam, BucketItem bodyBucketItem) throws BadRequestException {
+ if (StringUtils.isBlank(bucketIdParam)) {
+ throw new BadRequestException("Bucket id path parameter cannot be blank");
+ }
+
+ if (bodyBucketItem == null) {
+ throw new BadRequestException("Object in body cannot be null");
+ }
+
+ if (bodyBucketItem.getBucketIdentifier() != null && !bucketIdParam.equals(bodyBucketItem.getBucketIdentifier())) {
+ throw new BadRequestException("Bucket id in path param must match bucket id in body");
+ }
+ }
+
+ private static void verifyPathParamsMatchBody(String bucketIdParam, String flowIdParam, BucketItem bodyBucketItem) throws BadRequestException {
+ verifyPathParamsMatchBody(bucketIdParam, bodyBucketItem);
+
+ if (StringUtils.isBlank(flowIdParam)) {
+ throw new BadRequestException("Flow id path parameter cannot be blank");
+ }
+
+ if (bodyBucketItem.getIdentifier() != null && !flowIdParam.equals(bodyBucketItem.getIdentifier())) {
+ throw new BadRequestException("Item id in path param must match item id in body");
+ }
+ }
+
+ private static void verifyPathParamsMatchBody(String bucketIdParam, String flowIdParam, VersionedFlowSnapshot flowSnapshot) throws BadRequestException {
+ if (StringUtils.isBlank(bucketIdParam)) {
+ throw new BadRequestException("Bucket id path parameter cannot be blank");
+ }
+
+ if (StringUtils.isBlank(flowIdParam)) {
+ throw new BadRequestException("Flow id path parameter cannot be blank");
+ }
+
+ if (flowSnapshot == null) {
+ throw new BadRequestException("VersionedFlowSnapshot cannot be null in body");
+ }
+
+ final VersionedFlowSnapshotMetadata metadata = flowSnapshot.getSnapshotMetadata();
+ if (metadata != null && metadata.getBucketIdentifier() != null && !bucketIdParam.equals(metadata.getBucketIdentifier())) {
+ throw new BadRequestException("Bucket id in path param must match bucket id in body");
+ }
+ if (metadata != null && metadata.getFlowIdentifier() != null && !flowIdParam.equals(metadata.getFlowIdentifier())) {
+ throw new BadRequestException("Flow id in path param must match flow id in body");
+ }
+ }
+
+ private static void setBucketItemMetadataIfMissing(
+ @NotNull String bucketIdParam,
+ @NotNull String bucketItemIdParam,
+ @NotNull BucketItem bucketItem) {
+ if (bucketItem.getBucketIdentifier() == null) {
+ bucketItem.setBucketIdentifier(bucketIdParam);
+ }
+
+ if (bucketItem.getIdentifier() == null) {
+ bucketItem.setIdentifier(bucketItemIdParam);
+ }
+ }
+
+ private static void setSnaphotMetadataIfMissing(
+ @NotNull String bucketIdParam,
+ @NotNull String flowIdParam,
+ @NotNull VersionedFlowSnapshot flowSnapshot) {
+
+ VersionedFlowSnapshotMetadata metadata = flowSnapshot.getSnapshotMetadata();
+ if (metadata == null) {
+ metadata = new VersionedFlowSnapshotMetadata();
+ }
+
+ if (metadata.getBucketIdentifier() == null) {
+ metadata.setBucketIdentifier(bucketIdParam);
+ }
+
+ if (metadata.getFlowIdentifier() == null) {
+ metadata.setFlowIdentifier(flowIdParam);
+ }
+
+ flowSnapshot.setSnapshotMetadata(metadata);
+ }
+}
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketResource.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketResource.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketResource.java
new file mode 100644
index 0000000..e905973
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketResource.java
@@ -0,0 +1,293 @@
+/*
+ * 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.registry.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 io.swagger.annotations.Extension;
+import io.swagger.annotations.ExtensionProperty;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.bucket.Bucket;
+import org.apache.nifi.registry.bucket.BucketItem;
+import org.apache.nifi.registry.event.EventFactory;
+import org.apache.nifi.registry.event.EventService;
+import org.apache.nifi.registry.field.Fields;
+import org.apache.nifi.registry.security.authorization.RequestAction;
+import org.apache.nifi.registry.security.authorization.exception.AccessDeniedException;
+import org.apache.nifi.registry.security.authorization.resource.Authorizable;
+import org.apache.nifi.registry.service.AuthorizationService;
+import org.apache.nifi.registry.service.RegistryService;
+import org.apache.nifi.registry.web.link.LinkService;
+import org.apache.nifi.registry.web.security.PermissionsService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import javax.ws.rs.BadRequestException;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+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.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+@Component
+@Path("/buckets")
+@Api(
+ value = "buckets",
+ description = "Create named buckets in the registry to store NiFi objects such flows and extensions. " +
+ "Search for and retrieve existing buckets.",
+ authorizations = { @Authorization("Authorization") }
+)
+public class BucketResource extends AuthorizableApplicationResource {
+
+ private static final Logger logger = LoggerFactory.getLogger(BucketResource.class);
+
+ @Context
+ UriInfo uriInfo;
+
+ private final LinkService linkService;
+
+ private final RegistryService registryService;
+
+ private final PermissionsService permissionsService;
+
+ @Autowired
+ public BucketResource(
+ final RegistryService registryService,
+ final LinkService linkService,
+ final PermissionsService permissionsService,
+ final AuthorizationService authorizationService,
+ final EventService eventService) {
+ super(authorizationService, eventService);
+ this.registryService = registryService;
+ this.linkService = linkService;
+ this.permissionsService = permissionsService;
+ }
+
+ @POST
+ @Consumes(MediaType.APPLICATION_JSON)
+ @Produces(MediaType.APPLICATION_JSON)
+ @ApiOperation(
+ value = "Creates a bucket",
+ response = Bucket.class,
+ extensions = {
+ @Extension(name = "access-policy", properties = {
+ @ExtensionProperty(name = "action", value = "write"),
+ @ExtensionProperty(name = "resource", value = "/buckets") })
+ }
+ )
+ @ApiResponses({
+ @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400),
+ @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+ @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403) })
+ public Response createBucket(
+ @ApiParam(value = "The bucket to create", required = true)
+ final Bucket bucket) {
+ authorizeAccess(RequestAction.WRITE);
+
+ final Bucket createdBucket = registryService.createBucket(bucket);
+ publish(EventFactory.bucketCreated(createdBucket));
+
+ permissionsService.populateBucketPermissions(createdBucket);
+ linkService.populateBucketLinks(createdBucket);
+ return Response.status(Response.Status.OK).entity(createdBucket).build();
+ }
+
+ @GET
+ @Consumes(MediaType.WILDCARD)
+ @Produces(MediaType.APPLICATION_JSON)
+ @ApiOperation(
+ value = "Gets all buckets",
+ notes = "The returned list will include only buckets for which the user is authorized." +
+ "If the user is not authorized for any buckets, this returns an empty list.",
+ response = Bucket.class,
+ responseContainer = "List"
+ )
+ @ApiResponses({ @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401) })
+ public Response getBuckets() {
+
+ // Note: We don't explicitly check for access to (READ, /buckets) because
+ // a user might have access to individual buckets without top-level access.
+ // For example, a user that has (READ, /buckets/bucket-id-1) but not access
+ // to /buckets should not get a 403 error returned from this endpoint.
+ // This has the side effect that a user with no access to any buckets
+ // gets an empty array returned from this endpoint instead of 403 as one
+ // might expect.
+
+ final Set<String> authorizedBucketIds = getAuthorizedBucketIds(RequestAction.READ);
+
+ if (authorizedBucketIds == null || authorizedBucketIds.isEmpty()) {
+ // not authorized for any bucket, return empty list of items
+ return Response.status(Response.Status.OK).entity(new ArrayList<BucketItem>()).build();
+ }
+
+ final List<Bucket> buckets = registryService.getBuckets(authorizedBucketIds);
+ permissionsService.populateBucketPermissions(buckets);
+ linkService.populateBucketLinks(buckets);
+
+ return Response.status(Response.Status.OK).entity(buckets).build();
+ }
+
+ @GET
+ @Path("{bucketId}")
+ @Consumes(MediaType.WILDCARD)
+ @Produces(MediaType.APPLICATION_JSON)
+ @ApiOperation(
+ value = "Gets a bucket",
+ response = Bucket.class,
+ extensions = {
+ @Extension(name = "access-policy", properties = {
+ @ExtensionProperty(name = "action", value = "read"),
+ @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") })
+ }
+ )
+ @ApiResponses({
+ @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+ @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403),
+ @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404) })
+ public Response getBucket(
+ @PathParam("bucketId")
+ @ApiParam("The bucket identifier")
+ final String bucketId) {
+
+ authorizeBucketAccess(RequestAction.READ, bucketId);
+ final Bucket bucket = registryService.getBucket(bucketId);
+ permissionsService.populateBucketPermissions(bucket);
+ linkService.populateBucketLinks(bucket);
+
+ return Response.status(Response.Status.OK).entity(bucket).build();
+ }
+
+ @PUT
+ @Path("{bucketId}")
+ @Consumes(MediaType.APPLICATION_JSON)
+ @Produces(MediaType.APPLICATION_JSON)
+ @ApiOperation(
+ value = "Updates a bucket",
+ response = Bucket.class,
+ extensions = {
+ @Extension(name = "access-policy", properties = {
+ @ExtensionProperty(name = "action", value = "write"),
+ @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") })
+ }
+ )
+ @ApiResponses({
+ @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400),
+ @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+ @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403),
+ @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404),
+ @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) })
+ public Response updateBucket(
+ @PathParam("bucketId")
+ @ApiParam("The bucket identifier")
+ final String bucketId,
+ @ApiParam(value = "The updated bucket", required = true)
+ final Bucket bucket) {
+
+ if (StringUtils.isBlank(bucketId)) {
+ throw new BadRequestException("Bucket id cannot be blank");
+ }
+
+ if (bucket == null) {
+ throw new BadRequestException("Bucket cannot be null");
+ }
+
+ if (bucket.getIdentifier() != null && !bucketId.equals(bucket.getIdentifier())) {
+ throw new BadRequestException("Bucket id in path param must match bucket id in body");
+ } else {
+ bucket.setIdentifier(bucketId);
+ }
+
+ authorizeBucketAccess(RequestAction.WRITE, bucketId);
+
+ final Bucket updatedBucket = registryService.updateBucket(bucket);
+ publish(EventFactory.bucketUpdated(updatedBucket));
+
+ permissionsService.populateBucketPermissions(updatedBucket);
+ linkService.populateBucketLinks(updatedBucket);
+ return Response.status(Response.Status.OK).entity(updatedBucket).build();
+ }
+
+ @DELETE
+ @Path("{bucketId}")
+ @Consumes(MediaType.WILDCARD)
+ @Produces(MediaType.APPLICATION_JSON)
+ @ApiOperation(
+ value = "Deletes a bucket along with all objects stored in the bucket",
+ response = Bucket.class,
+ extensions = {
+ @Extension(name = "access-policy", properties = {
+ @ExtensionProperty(name = "action", value = "delete"),
+ @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") })
+ }
+ )
+ @ApiResponses({
+ @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400),
+ @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+ @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403),
+ @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404) })
+ public Response deleteBucket(
+ @PathParam("bucketId")
+ @ApiParam("The bucket identifier")
+ final String bucketId) {
+
+ if (StringUtils.isBlank(bucketId)) {
+ throw new BadRequestException("Bucket id cannot be blank");
+ }
+ authorizeBucketAccess(RequestAction.DELETE, bucketId);
+
+ final Bucket deletedBucket = registryService.deleteBucket(bucketId);
+ publish(EventFactory.bucketDeleted(deletedBucket));
+
+ return Response.status(Response.Status.OK).entity(deletedBucket).build();
+ }
+
+ @GET
+ @Path("fields")
+ @Consumes(MediaType.WILDCARD)
+ @Produces(MediaType.APPLICATION_JSON)
+ @ApiOperation(
+ value = "Retrieves field names for searching or sorting on buckets.",
+ response = Fields.class
+ )
+ public Response getAvailableBucketFields() {
+ final Set<String> bucketFields = registryService.getBucketFields();
+ final Fields fields = new Fields(bucketFields);
+ return Response.status(Response.Status.OK).entity(fields).build();
+ }
+
+ private void authorizeAccess(RequestAction actionType) throws AccessDeniedException {
+ final Authorizable bucketsAuthorizable = authorizableLookup.getBucketsAuthorizable();
+ authorizationService.authorize(bucketsAuthorizable, actionType);
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ConfigResource.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ConfigResource.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ConfigResource.java
new file mode 100644
index 0000000..a600a11
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ConfigResource.java
@@ -0,0 +1,108 @@
+/*
+ * 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.registry.web.api;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiResponse;
+import io.swagger.annotations.ApiResponses;
+import io.swagger.annotations.Authorization;
+import io.swagger.annotations.Extension;
+import io.swagger.annotations.ExtensionProperty;
+import org.apache.nifi.registry.RegistryConfiguration;
+import org.apache.nifi.registry.event.EventService;
+import org.apache.nifi.registry.security.authorization.Authorizer;
+import org.apache.nifi.registry.security.authorization.AuthorizerCapabilityDetection;
+import org.apache.nifi.registry.security.authorization.RequestAction;
+import org.apache.nifi.registry.security.authorization.exception.AccessDeniedException;
+import org.apache.nifi.registry.security.authorization.resource.Authorizable;
+import org.apache.nifi.registry.service.AuthorizationService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+@Component
+@Path("/config")
+@Api(
+ value = "config",
+ description = "Retrieves the configuration for this NiFi Registry.",
+ authorizations = { @Authorization("Authorization") }
+)
+public class ConfigResource extends AuthorizableApplicationResource {
+
+ @Autowired
+ public ConfigResource(
+ final AuthorizationService authorizationService,
+ final EventService eventService) {
+ super(authorizationService, eventService);
+ }
+
+ @GET
+ @Consumes(MediaType.WILDCARD)
+ @Produces(MediaType.APPLICATION_JSON)
+ @ApiOperation(
+ value = "Gets NiFi Registry configurations",
+ response = RegistryConfiguration.class,
+ extensions = {
+ @Extension(name = "access-policy", properties = {
+ @ExtensionProperty(name = "action", value = "read"),
+ @ExtensionProperty(name = "resource", value = "/policies,/tenants") })
+ }
+ )
+ @ApiResponses({
+ @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+ @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401) })
+ public Response getConfiguration() {
+
+ final RegistryConfiguration config = new RegistryConfiguration();
+
+ boolean hasAnyConfigurationAccess = false;
+ AccessDeniedException lastAccessDeniedException = null;
+ final Authorizer authorizer = authorizationService.getAuthorizer();
+ try {
+ final Authorizable policyAuthorizer = authorizableLookup.getPoliciesAuthorizable();
+ authorizationService.authorize(policyAuthorizer, RequestAction.READ);
+ config.setSupportsManagedAuthorizer(AuthorizerCapabilityDetection.isManagedAuthorizer(authorizer));
+ config.setSupportsConfigurableAuthorizer(AuthorizerCapabilityDetection.isConfigurableAccessPolicyProvider(authorizer));
+ hasAnyConfigurationAccess = true;
+ } catch (AccessDeniedException e) {
+ lastAccessDeniedException = e;
+ }
+
+ try {
+ authorizationService.authorize(authorizableLookup.getTenantsAuthorizable(), RequestAction.READ);
+ config.setSupportsConfigurableUsersAndGroups(AuthorizerCapabilityDetection.isConfigurableUserGroupProvider(authorizer));
+ hasAnyConfigurationAccess = true;
+ } catch (AccessDeniedException e) {
+ lastAccessDeniedException = e;
+ }
+
+ if (!hasAnyConfigurationAccess) {
+ // If the user doesn't have access to any configuration, then throw the exception.
+ // Otherwise, return what they can access.
+ throw lastAccessDeniedException;
+ }
+
+ return Response.status(Response.Status.OK).entity(config).build();
+ }
+}