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();
+    }
+}